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 D.; 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 Q…