[QUIZ] AnsiString (#185)

Since the Facets ANSICode was mentioned, I just thought that I'd also make everyone aware of the term-ansicolor gem by Florian Frank flori@ping.de which does this type of thing for colors.

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

-Rob

Rob Biedenharn http://agileconsultingllc.com
Rob@AgileConsultingLLC.com

···

On Dec 5, 2008, at 1:03 PM, Matthew Moss wrote:

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

The three rules of Ruby Quiz 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 Quiz 2 by submitting ideas as often as you can!
Visit <http://splatbang.com/rubyquiz/&gt;\.

3. Enjoy!

Suggestion: A [QUIZ] in the subject of emails about the problem
helps everyone on Ruby Talk 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][1] module (it's in [Facets][2]) 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 }

[1]: http://facets.rubyforge.org/doc/api/more/classes/ANSICode.html
[2]: http://facets.rubyforge.org/

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\(&quot;Hi&quot;\)
s2 = AnsiString\.new\(&quot;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 \+ &#39; &#39; \+ s2  \#=&gt; New AnsiString
s3\.to\_str           \#=&gt; &quot;\\e\[31mHi\\e\[0m \\e\[34mthere\!\\e\[0m&quot;

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.

## AnsiString (#185)

Summary and new quiz tomorrow.

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.

Since the Facets ANSICode was mentioned, I just thought that I'd also make everyone aware of the term-ansicolor gem by Florian Frank flori@ping.de 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 Gray II

···

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

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:

rd-185-sol.rb (3.39 KB)

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 :(.
<snip>

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.

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 :(.

<snip>
Cheers
R.

···

On Fri, Dec 12, 2008 at 7:06 PM, Matthew Moss <matt@moss.name> wrote:

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:

···

On Dec 12, 1:06 pm, Matthew Moss <m...@moss.name> 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.)

  #
  # "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.