2008/1/31, Leif Eriksen <leif.eriksen / bigpond.com>: > Some background - bear with me > > So I'm writing my first little library (module) in ruby, for a > play-project in rails. > > The library is for color management related functions, and its first > method is to > provide a color 'gradient', which I use for images where the colors smoothly > change from a to b in as many steps as I require > > for example, to get from html rgb color #010101 to #444444 in 5 'steps', > we would do > > ColorManagement.gradient(#010101, #444444, 5) # will used named params > in version 2! > > and it would return > > ["#010101", "#0e0e0e", "#1b1b1b", "#282828", "#353535", "#444444"] > > The method supports leading '0x' chars as well, and prepends the leading > chars (if any) to the > entries in the result array. > > And this is all cool, until I did this > > start = '#010101' > finish = '#444444' > ColorGradient.gradient(start, finish , 5) > pp start "#{start}" > pp "finish #{finish }" > > and I see this > > "start 010101" > "finish 444444" > > The '#''s are gone. Internally the library is stripping these off to make > splitting into RGB channels easier, and it does the stripping like this > > def self.gradient(hex_start="000000", hex_end="FFFFFF", steps=256) > [hex_start,hex_end].each do |hex| > hex.sub!(/^(0[xX]|#)/) do |match| > match = '' # remove any leading # or Ox > > So the strings that are passed in are permanently munged by the sub! . > OK I get that, and to solve it I did this > > def self.gradient(param_start="000000", param_end="FFFFFF", steps=256) > #make local copies of parameters > hex_start = String.new(param_start) > hex_end = String.new(param_end) > > [hex_start,hex_end].each do |hex| > hex.sub!(/^(0[xX]|#)/) do |match| > ... > > So in effect I copy the parameter strings into local vars, to avoid > munging what the user passes me and annoying them. > > But is this the ruby way ? Is this the idiomatic way to avoid > side-effects on objects passed in as parameters ? I'd say, this is generally considered bad OO. A better solution would be to create a class Color that provides methods to do all the conversions. E.g. Color = Struct.new :red, :green, :blue do # will accept either # a single string with hex number # a single fixnum (0x000000 - 0xFFFFFF, larger values are truncated) # three fixnums # three strings def initialize(*a) case a.length when 3 self.red, self.green, self.blue = a.map {|v| arg2bin(v) % 0x100} when 1 tmp = arg2bin a.first raise ArgumentError, "Negative value" if tmp < 0 self.red = (tmp >> 16) % 0x100 self.green = (tmp >> 8) % 0x100 self.blue = tmp % 0x100 else raise ArgumentError, "Need either a single string with hex number, a single Fixnum or three separate Fixnums or hex Strings" end end def to_hex sprintf "%02x%02x%02x", red, green, blue end def to_s sprintf "#%02x%02x%02x", red, green, blue end def &(color) Color.new red & color.red, green & color.green, blue & color.blue end def |(color) Color.new red | color.red, green | color.green, blue | color.blue end private def arg2bin(a) case a when Fixnum a when String a.to_i 16 end end end This is just a sample implementation. You might as well change internal representation to a single Fixnum etc. Then add methods as you need them. Kind regards robert -- use.inject do |as, often| as.you_can - without end