[SUMMARY] AnsiString (#185)



It would seem that writing Transfire's desired `ANSIString` class is
more difficult that it appears. (Or, perhaps, y'all are busy preparing
for the holidays.) The sole submission for this quiz comes from
_Robert Dober_; it's not completely to specification nor handles the
bonus, but it is a good start. (More appropriately, it might be better
to say that the specification isn't entirely clear, and that Robert's
implementation didn't match *my* interpretation of the spec; a proper
`ANSIString` module would need to provide more details on a number of
things.)

Robert relies on other libraries to provide the actual ANSI codes;
seeing as there are at least three libraries that do, Robert provides
a mechanism to choose between them based on user request and/or
availability. Let's take a quick look at this mechanism. (Since this
quiz doesn't use the Module mechanism in Robert's `register_lib`
routine, I've removed the related references for clarity. I suspect
those are for a larger set of library management routines.)

@use_lib =
( ARGV.first == '-f' || ARGV.first == '--force' ) &&
ARGV[1]

def register_lib lib_path, &blk
return if @use_lib && lib_path != @use_lib
require lib_path
Libraries[ lib_path ] = blk
end

register_lib "facets/ansicode" do | color |
ANSICode.send color
end

# similar register_lib calls for "highline" and "term/ansicolor"

class ANSIString
used_lib_name = Libraries.keys[ rand( Libraries.keys.size ) ]
lib = Libraries[ used_lib_name ]
case lib
when Proc
define_method :__color__, &lib
else
raise RuntimeError, "Nooooo I have explained exactly how to
register libraries, has I not?"
end

# ... rest of ANSIString ...
end

First, we check if the user has requested (via `--force`) a particular
library. This is used in the first line of `register_lib`, which exits
early if we try to register a library other than the one specified.
Then `register_lib` loads the matching library (or all if the user did
not specify) via `require` as is typical. Finally, a reference to the
provided code block is kept, indexed by the library name.

This seems, perhaps, part of a larger set of library management
routines; its use in this quiz is rather simple, as can be seen in the
calls to `register_lib` immediately following. While registering
"facets/ansicode", a block is provided to call `ANSICode.send color`.
This is then used below in `ANSIString`, when we choose one of the
libraries to use, recall the corresponding code block, and define a
new method `__color__` that calls that code block.

Altogether, this is a reasonable technique for putting a façade around
similar functionality in different libraries and choosing between
available libraries, perhaps if one or another is not available. It
seems to me that such library management – at least the general
mechanisms – might be worthy of its own gem.

Given that we now have a way to access ANSI codes via
`ANSIString#__color__`, let's now move onto the code related to the
task, starting with initialization and conversion to `String`:

class ANSIString
ANSIEnd = "\e[0m"

def initialize *strings
@strings = strings.dup
end

def to_s
@strings.map do |s|
case s
when String
s
when :end
ANSIEnd
else
__color__ s
end
end.join
end
end

Internally, `ANSIString` keeps an array of strings, its initial value
set to a copy of the initialization parameters. So we can create ANSI
string objects in a couple of ways:

s1 = ANSIString.new "Hello, world!"
s2 = ANSIString.new :green, "Merry ", :red, "Christmas!", :end

When converting with `to_s`, each member of that array is
appropriately converted to a `String`. It is assumed that members of
the array are either already `String` objects (so are mapped to
themselves), the `:end` symbol (so mapped to constant string
`ANSIEnd`), or appropriate color symbols available in the previously
loaded library (mapped to the corresponding ANSI string available
through method `__color__`). Once all items in the array are converted
to strings, a simple call to `join` binds them together into one,
final string.

Let's look at string concatenation:

class ANSIString
def + other
other.add_reverse self
rescue NoMethodError
self.class::new( *( __end__ << other ) )
end

def add_reverse an_ansi_str
self.class::new( *(
an_ansi_str.send( :__end__ ) + __end__
) )
end

private
def __end__
@strings.reverse.find{ |x| Symbol === x} == :end ?
@strings.dup : @strings.dup << :end
end
end

Before we get to the concatenation itself, take a quick look at helper
method `__end__`. It looks for the last symbol and compares it against
`:end`. Whether true or false, the `@string` array is duplicated (and
so protects the instance variable from change). Only, `__end__` does
not append another `:end` symbol if unnecessary.

I was a little confused, at first, about the implementation of
`ANSIString` concatenation. Perhaps Robert had other plans in mind,
but it seemed to me this work could be simplified. Since `add_reverse`
is called nowhere else (and I couldn't imagine it being called by the
user, despite the public interface), I tried inserting `add_reverse`
inline to `+` (fixing names along the way):

def + other
other.class::new( *(self.send(:__end__) + other.__end__) )
rescue NoMethodError
self.class::new( *( __end__ << other ) )
end

And, with further simplification:

def + other
other.class::new( *( __end__ + other.send(:__end__) ) )
rescue NoMethodError
self.class::new( *( __end__ << other ) )
end

I believed Robert had a bug, neglecting to call `__end__` in the
second case, until I realized my mistake: `other` is not necessarily
of the `ANSIString` class, and so would not have the `__end__` method.
My attempt to fix my mistake was to rewrite again as this:

def + other
ANSIString::new( *( __end__ + other.to_s ) )
end

But that has its own problems if `other` *is* an `ANSIString`; it
neglects to end the string and converts it to a simple `String` rather
than maintaining its components. Clearly undesirable. Obviously,
Robert's implementation is the right way... or is it? Going back to
this version:

def + other
other.class::new( *( __end__ + other.send(:__end__) ) )
rescue NoMethodError
self.class::new( *( __end__ << other ) )
end

Ignoring the redundancy, this actually works. My simplification will
throw the `NoMethodError` exception, because `String` does not define
`__end__`, just as Robert's version throws that exception if either
`add_reverse` or `__end__` is not defined. So, removing redundancy, I
believe concatenation can be simplified correctly as:

def + other
self.class::new( *(
__end__ + (other.send(:__end__) rescue [other] )
) )
end

For me, this reduces concatenation to something more quickly
understandable.

One last point on concatenation; Robert's version will create an
object of class `other.class` if that class has both methods
`add_reverse` and `__end__`, whereas my simplification does not.
However, it seems unlikely to me that any class other than
`ANSIString` will have those methods. I recognize that my assumption
here may be flawed; Robert will have to provide further details on his
reasoning or other uses of the code.

Finally, we deal with adding ANSI codes to the ANSI strings (aside
from at initialization):

class ANSIString
def end
self.class::new( * __end__ )
end

def method_missing name, *args, &blk
super( name, *args, &blk ) unless args.empty? && blk.nil?
class << self; self end.module_eval do
define_method name do
self.class::new( *([name.to_sym] + @strings).flatten )
end
end
send name
end
end

Method `end` simply appends the symbol `:end` to the `@strings` array
by making use of the existing `__end__` method. Reusing `__end__` (as
opposed to just doing `@strings << :end`) ensures that we don't have
unnecessary `:end` symbols in the string.

Finally, `method_missing` catches all other calls, such as `bold` or
`red`. Any calls with arguments or a code block are passed up first to
the superclass, though considering the parent class is `Object`, any
such call is likely to generate a `NoMethodError` exception (since, if
the method was in `Object`, `method_missing` would not have been
called). Also note that whether "handled" by the superclass or not,
all missing methods are *also* handled by the rest of the code in
`method_missing`. I don't know if that is intentional or accidental.
In general, this seems prone to error, and it would seem a better
tactic either to discern the ANSI code methods from the loaded module
or to be explicit about such codes.

In any case, calling `red` on `ANSIString` the first time actually
generates a new method, by way of the `define_method` call located in
`method_missing`. Further calls to `red` (and the first call, via the
last line `send name`) will actually use that new method, which
prepends `red.to_sym` (that is, `:red`) to the string in question.

At this point, `ANSIString` handles basic initialization,
concatenation, ANSI codes and output; it does not handle the rest of
the capabilities of `String` (such as substrings, `gsub`, and others),
so it is not a drop-in replacement for strings. I believe it could be,
with time and effort, but that is certainly a greater challenge than
is usually attempted on Ruby Quiz.


.



Relevant Pages

  • Re: Ascii und Ansi tauschen - Codeproblem
    ... eine Syntax "CharToOem AnsiString, AnsiString" die als Parameter 2x die selbe Variable bemüht für logisch, progammtechnisch zunmindest, unsinnig finde]. ... Was immer 'CharToOem AnsiString' bis hierhin tut erscheint logisch: der Funktionalität CharToOem wird ein Quelltext übergeben. ... Public Function ASCIItoANSI(ByVal AsciiString As String) As String ...
    (microsoft.public.de.word.vba)
  • Re: Did Borland doing well in Q4? Listen to the Earning CC
    ... Changing an type does not necessarily affect the application code. ... Because a String is *very* different from any ordinal type. ... to "ANSIString" etc. ... Not long after Tiburon is released you might hope to get a Unicode ...
    (borland.public.delphi.non-technical)
  • Re: Fastcode AnsiStringReplace
    ... function AnsiStringReplace(const S, OldPattern, NewPattern: ... Flags: TReplaceFlags): AnsiString; ... TReplaceFlags): string; ...
    (borland.public.delphi.language.basm)
  • Re: Fastcode Memory Read Rules
    ... The two rules are for different string types ... How far beyond the end of the ansistring can allways safely be read and is garuanteed to not violate the general rule? ... I can't image that the allocator would waste more then maybe 8 bytes? ... hmz i only allign on 8 bytes in my own paging allocator, so this could run into problems with fastcode? ...
    (borland.public.delphi.language.basm)
  • Re: [SUMMARY] AnsiString (#185)
    ... than the ANSIString implementation, hence ... def add_reverse an_ansi_str ... Before we get to the concatenation itself, take a quick look at helper ... And, with further simplification: ...
    (comp.lang.ruby)

Loading