It would seem that writing Transfire's desired `ANSIString` class is =20
more difficult that it appears. (Or, perhaps, y'all are busy preparing =20=
for the holidays.) The sole submission for this quiz comes from =20
_Robert Dober_; it's not completely to specification nor handles the =20
bonus, but it is a good start. (More appropriately, it might be better =20=
to say that the specification isn't entirely clear, and that Robert's =20=
implementation didn't match *my* interpretation of the spec; a proper =20=
`ANSIString` module would need to provide more details on a number of =20=
things.)
Robert relies on other libraries to provide the actual ANSI codes; =20
seeing as there are at least three libraries that do, Robert provides =20=
a mechanism to choose between them based on user request and/or =20
availability. Let's take a quick look at this mechanism. (Since this =20
quiz doesn't use the Module mechanism in Robert's `register_lib` =20
routine, I've removed the related references for clarity. I suspect =20
those are for a larger set of library management routines.)
@use_lib =3D
( ARGV.first =3D=3D '-f' || ARGV.first =3D=3D '--force' ) &&
ARGV[1]
=09
def register_lib lib_path, &blk
return if @use_lib && lib_path !=3D @use_lib
require lib_path
Libraries[ lib_path ] =3D 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 =3D Libraries.keys[ rand( Libraries.keys.size ) =
]
lib =3D Libraries[ used_lib_name ]
case lib
when Proc
define_method :__color__, &lib
else
raise RuntimeError, "Nooooo I have explained exactly how to =20=
register libraries, has I not?"
end
# ... rest of ANSIString ...
end
=09
First, we check if the user has requested (via `--force`) a particular =20=
library. This is used in the first line of `register_lib`, which exits =20=
early if we try to register a library other than the one specified. =20
Then `register_lib` loads the matching library (or all if the user did =20=
not specify) via `require` as is typical. Finally, a reference to the =20=
provided code block is kept, indexed by the library name.
This seems, perhaps, part of a larger set of library management =20
routines; its use in this quiz is rather simple, as can be seen in the =20=
calls to `register_lib` immediately following. While registering =20
"facets/ansicode", a block is provided to call `ANSICode.send color`. =20=
This is then used below in `ANSIString`, when we choose one of the =20
libraries to use, recall the corresponding code block, and define a =20
new method `__color__` that calls that code block.
Altogether, this is a reasonable technique for putting a fa=E7ade around =
=20
similar functionality in different libraries and choosing between =20
available libraries, perhaps if one or another is not available. It =20
seems to me that such library management =96 at least the general =20
mechanisms =96 might be worthy of its own gem.
Given that we now have a way to access ANSI codes via =20
`ANSIString#__color__`, let's now move onto the code related to the =20
task, starting with initialization and conversion to `String`:
class ANSIString
ANSIEnd =3D "\e[0m"
def initialize *strings
@strings =3D 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
=09
Internally, `ANSIString` keeps an array of strings, its initial value =20=
set to a copy of the initialization parameters. So we can create ANSI =20=
string objects in a couple of ways:
s1 =3D ANSIString.new "Hello, world!"
s2 =3D ANSIString.new :green, "Merry ", :red, "Christmas!", :end
=09
When converting with `to_s`, each member of that array is =20
appropriately converted to a `String`. It is assumed that members of =20
the array are either already `String` objects (so are mapped to =20
themselves), the `:end` symbol (so mapped to constant string =20
`ANSIEnd`), or appropriate color symbols available in the previously =20
loaded library (mapped to the corresponding ANSI string available =20
through method `__color__`). Once all items in the array are converted =20=
to strings, a simple call to `join` binds them together into one, =20
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 =3D=3D=3D x} =3D=3D :end ?
@strings.dup : @strings.dup << :end
end
end
Before we get to the concatenation itself, take a quick look at helper =20=
method `__end__`. It looks for the last symbol and compares it against =20=
`:end`. Whether true or false, the `@string` array is duplicated (and =20=
so protects the instance variable from change). Only, `__end__` does =20
not append another `:end` symbol if unnecessary.
I was a little confused, at first, about the implementation of =20
`ANSIString` concatenation. Perhaps Robert had other plans in mind, =20
but it seemed to me this work could be simplified. Since `add_reverse` =20=
is called nowhere else (and I couldn't imagine it being called by the =20=
user, despite the public interface), I tried inserting `add_reverse` =20
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 =20
second case, until I realized my mistake: `other` is not necessarily =20
of the `ANSIString` class, and so would not have the `__end__` method. =20=
My attempt to fix my mistake was to rewrite again as this:
def + other
ANSIString::new( *( __end__ + other.to_s ) )
end
=09
But that has its own problems if `other` *is* an `ANSIString`; it =20
neglects to end the string and converts it to a simple `String` rather =20=
than maintaining its components. Clearly undesirable. Obviously, =20
Robert's implementation is the right way... or is it? Going back to =20
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 =20
throw the `NoMethodError` exception, because `String` does not define =20=
`__end__`, just as Robert's version throws that exception if either =20
`add_reverse` or `__end__` is not defined. So, removing redundancy, I =20=
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 =20
understandable.
One last point on concatenation; Robert's version will create an =20
object of class `other.class` if that class has both methods =20
`add_reverse` and `__end__`, whereas my simplification does not. =20
However, it seems unlikely to me that any class other than =20
`ANSIString` will have those methods. I recognize that my assumption =20
here may be flawed; Robert will have to provide further details on his =20=
reasoning or other uses of the code.
Finally, we deal with adding ANSI codes to the ANSI strings (aside =20
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
=09
Method `end` simply appends the symbol `:end` to the `@strings` array =20=
by making use of the existing `__end__` method. Reusing `__end__` (as =20=
opposed to just doing `@strings << :end`) ensures that we don't have =20
unnecessary `:end` symbols in the string.
Finally, `method_missing` catches all other calls, such as `bold` or =20
`red`. Any calls with arguments or a code block are passed up first to =20=
the superclass, though considering the parent class is `Object`, any =20
such call is likely to generate a `NoMethodError` exception (since, if =20=
the method was in `Object`, `method_missing` would not have been =20
called). Also note that whether "handled" by the superclass or not, =20
all missing methods are *also* handled by the rest of the code in =20
`method_missing`. I don't know if that is intentional or accidental. =20
In general, this seems prone to error, and it would seem a better =20
tactic either to discern the ANSI code methods from the loaded module =20=
or to be explicit about such codes.
In any case, calling `red` on `ANSIString` the first time actually =20
generates a new method, by way of the `define_method` call located in =20=
`method_missing`. Further calls to `red` (and the first call, via the =20=
last line `send name`) will actually use that new method, which =20
prepends `red.to_sym` (that is, `:red`) to the string in question.
At this point, `ANSIString` handles basic initialization, =20
concatenation, ANSI codes and output; it does not handle the rest of =20
the capabilities of `String` (such as substrings, `gsub`, and others), =20=
so it is not a drop-in replacement for strings. I believe it could be, =20=
with time and effort, but that is certainly a greater challenge than =20
is usually attempted on Ruby Quiz.