Forum: Ruby AnsiString (#185)

Announcement (2017-05-07): www.ruby-forum.com is now read-only since I unfortunately do not have the time to support and maintain the forum any more. Please see rubyonrails.org/community and ruby-lang.org/en/community for other Rails- und Ruby-related community platforms.
A61ecce13ed142622f24a5ca3a123922?d=identicon&s=25 Matthew Moss (Guest)
on 2008-12-05 19:10
(Received via mailing list)
-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-

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

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/A...
[2]: http://facets.rubyforge.org/
Ef3aa7f7e577ea8cd620462724ddf73b?d=identicon&s=25 Rob Biedenharn (Guest)
on 2008-12-05 20:29
(Received via mailing list)
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
4299e35bacef054df40583da2d51edea?d=identicon&s=25 James Gray (bbazzarrakk)
on 2008-12-05 20:47
(Received via mailing list)
On Dec 5, 2008, at 1:23 PM, Rob Biedenharn wrote:

> 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
45196398e9685000d195ec626d477f0e?d=identicon&s=25 Thomas Sawyer (7rans)
on 2008-12-05 23:01
(Received via mailing list)
> 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.
703fbc991fd63e0e1db54dca9ea31b53?d=identicon&s=25 Robert Dober (Guest)
on 2008-12-07 20:49
(Received via mailing list)
Attachment: rd-185-sol.rb (3 KB)
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 :-P
A61ecce13ed142622f24a5ca3a123922?d=identicon&s=25 Matthew Moss (Guest)
on 2008-12-11 17:28
(Received via mailing list)
> ## AnsiString (#185)


Summary and new quiz tomorrow.
A61ecce13ed142622f24a5ca3a123922?d=identicon&s=25 Matthew Moss (Guest)
on 2008-12-12 19:13
(Received via mailing list)
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.
703fbc991fd63e0e1db54dca9ea31b53?d=identicon&s=25 Robert Dober (Guest)
on 2008-12-12 21:34
(Received via mailing list)
On Fri, Dec 12, 2008 at 7:06 PM, Matthew Moss <matt@moss.name> 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 :(.
<snip>
>          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 :(.
>
<snip>
Cheers
R.
45196398e9685000d195ec626d477f0e?d=identicon&s=25 Thomas Sawyer (7rans)
on 2008-12-31 04:15
(Received via mailing list)
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.)

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.
This topic is locked and can not be replied to.