AnsiString (#185)


#1

-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-

The three rules of Ruby Q. 2:

  1. Please do not post any solutions or spoiler discussion for this
    quiz until 48 hours have passed from the time on this message.

  2. Support Ruby Q. 2 by submitting ideas as often as you can!
    Visit http://splatbang.com/rubyquiz/.

  3. Enjoy!

Suggestion: A [QUIZ] in the subject of emails about the problem
helps everyone on Ruby T. follow the discussion. Please reply to
the original quiz message, if you can.

-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-

AnsiString (#185)

Quiz description provided by Transfire

Make a subclass of String (or delegate) that tracks “embedded” ANSI
codes along with the text. The class should add methods for wrapping
the text in ANSI codes. Implement as much of the core String API as
possible. So for example:

s1 = AnsiString.new("Hi")
s2 = AnsiString.new("there!)

s1.red    # wrap text in red/escape ANSI codes
s1.blue   # wrap text in blue/escape ANSI codes

s3 = s1 + ' ' + s2  #=> New AnsiString
s3.to_str           #=> "\e[31mHi\e[0m \e[34mthere!\e[0m"

I have an ANSICode module (it’s in Facets) that you are
welcome to provide for the ANSI backend, if desired. It is easy enough
to use; the literal equivalent of the above would be:

ANSICode.red('Hi') + ' ' + ANSICode.blue('there!')

Bonus points for being able to use ANSIStrings in a gsub block:

ansi_string.gsub(pattern){ |s| s.red }

#2

Since the Facets ANSICode was mentioned, I just thought that I’d also
make everyone aware of the term-ansicolor gem by Florian F.
removed_email_address@domain.invalid
which does this type of thing for colors.

http://term-ansicolor.rubyforge.org/
gem install term-ansicolor

-Rob

Rob B. http://agileconsultingllc.com
removed_email_address@domain.invalid


#3

On Dec 5, 2008, at 1:23 PM, Rob B. wrote:

Since the Facets ANSICode was mentioned, I just thought that I’d
also make everyone aware of the term-ansicolor gem by Florian F. removed_email_address@domain.invalid
which does this type of thing for colors.

HighLine also does this:

require “highline/import”
=> true

say("<%= color ‘I want to be red!’, :red %>")
I want to be red!
=> nil

You can use color() without printing with code like:

red_str = HighLine.new.color(“I want to be red!”, :red)

And if you use that you can drop the /import in the require.

James Edward G. II


#4

Make a subclass of String (or delegate) that tracks “embedded” ANSI
codes along with the text. The class should add methods for wrapping
the text in ANSI codes. Implement as much of the core String API as
possible. So for example:

s1 = AnsiString.new("Hi")
s2 = AnsiString.new("there!)

s1.red    # wrap text in red/escape ANSI codes
s1.blue   # wrap text in blue/escape ANSI codes

Make that ‘s2.blue’.

s3 = s1 + ' ' + s2  #=> New AnsiString
s3.to_str           #=> "\e[31mHi\e[0m \e[34mthere!\e[0m"

I’ve found it pretty challenging to keep the ANSI codes in sync with
the text while still being able to manipulate the text like a normal
string. I’m curious to see how other people approach it.

T.


#5

This was a nice quiz, maybe not too challenging, but as Tom put it
correctly the different codes can easily get mangled up. I have found
a solution that seems to handle the few testcases well, we will see if
it pleases ;).
My additional challenge was to make my solution work with HighLine,
Facets and Term::Ansicolor, and that is what I did :).

The solution can be found here too: http://pastie.org/333319

And to test all libraries simply type

for i in term/ansicolor facets/ansicode highline; do ruby1.9
rd-185-sol.rb -f $i; done

ruby1.9 rd-185-sol.rb

runs, transparently, with a random library of all registered

HYLI

Robert

P.S.
I do not recall ever have answered 3 RQ in a row before :stuck_out_tongue:


#6

AnsiString (#185)

Summary and new quiz tomorrow.


#7

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…


#8

On Fri, Dec 12, 2008 at 7:06 PM, Matthew M. removed_email_address@domain.invalid wrote:
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.)
Exactly, it was the multi library approach which interested me more
than the ANSIString implementation, hence
the sloppy implementation :(.

def add_reverse an_ansi_str
end
(and I couldn’t imagine it being called by the user, despite the public

     other.class::new( *( __end__ + other.send(:__end__) ) )
   def + other

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.
Not really I was quite sloppy, it took me same time to re-understand
my code, always a bad sign.
Sorry for giving you so much work :(.

Cheers R.

#9

On Dec 12, 1:06 pm, Matthew M. removed_email_address@domain.invalid wrote:

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.)

Yep. That’s too bad. The holidays have had me occupied as well, and it
is a tricky problem. I was hoping someone brighter than I would come
up with a really clever way of doing it.

Robert’s implementation using an array is string and symbol is much
like my first stab at it. In some ways I think maybe it’s the better
way to go, although more limited in scope, one can at least wrap ones
head around the implementation without too much trouble. Thanks for
taking a stab at the quiz Robert!

My implementation on the other hand tries to go the full nine-yards
toward drop-in compatibility with the core String class. It’s not
complete by any means, but the foundation is in place for doing so,
ie. the #shift_marks method. The downside though is that the algorithm
is somewhat complex and, worse still, time consuming, not to mention
imperfect – hence my hope someone else might have a brighter idea for
going about this.

require ‘facets/ansicode’
require ‘facets/string’

ANSIStrings stores a regular string (@text) and

a Hash mapping character index to ansicodes (@marks).

For example is we has the string:

“Big Apple”

And applied the color red to it, the marks hash would be:

{ 0=>[:red] , 9=>[:clear] }

class ANSIString

CLR = ANSICode.clear

attr :text
attr :marks

def initialize(text=nil, marks=nil)
  @text  = (text  || '').to_s
  @marks = marks  || []
  yield(self) if block_given?
end

def to_s
  s = text.dup
  m = marks.sort do |(a,b)|
    v = b[0] <=> a[0]
    if v == 0
      (b[1] == :clear or b[1] == :reset) ? -1 : 1
    else
      v
    end
  end
  m.each do |(index, code)|
    s.insert(index, ANSICode.__send__(code))
  end
  #s << CLR unless s =~ /#{Regexp.escape(CLR)}$/  # always end

with a clear
s
end

#
alias_method :to_str, :to_s

def size ; text.size ; end

def upcase  ; self.class.new(text.upcase, marks) ; end
def upcase! ; text.upcase! ; end

def downcase  ; self.class.new(text.upcase, marks) ; end
def downcase! ; text.upcase! ; end

def +(other)
  case other
  when String
    ntext  = text + other.text
    nmarks = marks.dup
    omarks = shift_marks(0, text.size, other.marks)
    omarks.each{ |(i, c)| nmarks << [i,c] }
  else
    ntext  = text + other.to_s
    nmarks = marks.dup
  end
  self.class.new(ntext, nmarks)
end

def slice(*args)
  if args.size == 2
    index, len = *args
    endex  = index+len
    new_text  = text[index, len]
    new_marks = []
    marks.each do |(i, v)|
      new_marks << [i, v] if i >= index && i < endex
    end
    self.class.new(new_text, new_marks)
  elsif args.size == 1
    rng = args.first
    case rng
    when Range
      index, endex = rng.begin, rng.end
      new_text  = text[rng]
      new_marks = []
      marks.each do |(i, v)|
        new_marks << [i, v] if i >= index && i < endex
      end
      self.class.new(new_text, new_marks)
    else
      nm = marks.select do |(i,c)|
        marks[0] == rng or ( marks[0] == rng + 1 &&

[:clear, :reset].include?(marks[1]) )
end
self.class.new(text[rng,1], nm)
end
else
raise ArgumentError
end
end

alias_method :[], :slice

# This is more limited than the normal String method.
# It does not yet support a block, and +replacement+
# won't substitute for \1, \2, etc.
#
# TODO: block support.
def sub!(pattern, replacement=nil, &block)
  mark_changes = []
  text = @text.sub(pattern) do |s|
    index  = $~.begin(0)
    replacement = block.call(s) if block_given?
    delta  = (replacement.size - s.size)
    mark_changes << [index, delta]
    replacement
  end
  marks = @marks
  mark_changes.each do |index, delta|
    marks = shift_marks(index, delta, marks)
  end
  @text  = text
  @marks = marks
  self
end

#
def sub(pattern,replacement=nil, &block)
  dup.sub!(pattern, replacement, &block)
end

#
def gsub!(pattern, replacement=nil, &block)
  mark_changes   = []
  mark_additions = []
  text = @text.gsub(pattern) do |s|
    index = $~.begin(0)
    replacement = block.call(self.class.new(s)) if block_given?
    if self.class===replacement
      adj_marks = replacement.marks.map{ |(i,c)| [i+index,c] }
      mark_additions.concat(adj_marks)
      replacement = replacement.text
    end
    delta = (replacement.size - s.size)
    mark_changes << [index, delta]
    replacement
  end
  marks = @marks
  mark_changes.each do |(index, delta)|
    marks = shift_marks(index, delta, marks)
  end
  marks.concat(mark_additions)
  @text  = text
  @marks = marks
  self
end

#
def gsub(pattern, replacement=nil, &block)
  dup.gsub!(pattern, replacement, &block)
end

#
def ansi(code)
  m = marks.dup
  m.unshift([0, code])
  m.push([size, :clear])
  self.class.new(text, m)
end
alias_method :color, :ansi

#
def ansi!(code)
  marks.unshift([0, ansicolor])
  marks.push([size, :clear])
end
alias_method :color!, :ansi!

def red      ; color(:red)      ; end
def green    ; color(:green)    ; end
def blue     ; color(:blue)     ; end
def black    ; color(:black)    ; end
def magenta  ; color(:magenta)  ; end
def yellow   ; color(:yellow)   ; end
def cyan     ; color(:cyan)     ; end

def bold       ; ansi(:bold)       ; end
def underline  ; ansi(:underline)  ; end

def red!     ; color!(:red)     ; end
def green!   ; color!(:green)   ; end
def blue!    ; color!(:blue)    ; end
def black!   ; color!(:black)   ; end
def magenta! ; color!(:magenta) ; end
def yellow!  ; color!(:yellow)  ; end
def cyan!    ; color!(:cyan)    ; end

def bold!      ; ansi!(:bold)      ; end
def underline! ; ansi!(:underline) ; end

private

#
def shift_marks(index, delta, marks=nil)
  new_marks = []
  (marks || @marks).each do |(i, c)|
    case i <=> index
    when -1
      new_marks << [i, c]
    when 0, 1
      new_marks << [i+delta, c]
    end
  end
  new_marks
end

#
def shift_marks!(index, delta)
 @marks.replace(shift_marks(index, delta))
end

end

Sorry for my late post. I’m only now starting to get settled back into
the routine of things.