views:

134

answers:

3

I'm writing a library to humanize bytes in Ruby (e.g. to turn the byte count 1025 into the string 1.1K), and I'm stuck on one element of the design.

The plan is to extend Numeric with ahumanize method that returns a human-friendly string when called on a number. After looking at the source of Number::Bytes::Human (a Perl module that I like a lot for this), I decided to add two options to the method: one to use 1000 byte blocks and one to use floor rather than ceil for the default rounding function.

In order to be maximally flexible, the method's definition uses a hash for the parameters, so that users can change one or both of the options. If no parameters are passed, a default hash is used. That gives me something like this:

def humanize(params = {})
  params = {:block => 1024, :r_func => lambda }.merge params
  # yada yada
end

Ideally, I would like to let the user pass a function as the value of params[:r_func], but I can't figure out how to validate that it's either ceil or floor. Because I can't get a handle on this, I've ended up doing the following, which feels pretty clumsy:

  def humanize(params = {})
    params = {:block => 1024, :r_func => 'ceil' }.merge params
    if params[:r_func].eql? 'ceil'
      params[:r_func] = lambda { |x| x.ceil }
    elsif params[:r_func].eql? 'floor'
      params[:r_func] = lambda { |x| x.floor }
    else 
      raise BadRound, "Rounding method must be 'ceil' or 'floor'."
    end
    # blah blah blah
  end

If anyone knows a trick for peeking at the method that a Ruby lamda contains, I would love to hear it. (I'm also happy to hear any other design advice.) Thanks.

+2  A: 

Why have them pass a function pointer instead of a boolean? That way you avoid the problem of having to validate the function.

Yuliy
I hadn't thought of it, but it's a good idea if I leave it at two choices.
Telemachus
+3  A: 

I don't see any point in having the caller pass a lambda if you're not going to actually call the thing. Make it a symbol instead and you can do something like:

raise BadRound, "Rounding method must be :ceil or :floor." unless [:ceil, :floor].include? params[:r_func]
op = lambda {|x| x.send params[:r_func]}
# blah blah blah
Chuck
I kept trying to use symbols and failing. The `send` method is what I was missing. Thanks. (For the record, I do call the lambda - just later, after validation in a private worker method.)
Telemachus
+7  A: 

There's no reason to let the user pass a method in if you're going to be that draconian about what they are allowed to pass (you know there are other rounding schemes besides ceiling and floor, right?)

If you want to restrict the user to ceiling and floor, just allow them to pass the symbol :ceiling or :floor in. A more flexible design would be to allow the method to take a block which receives a single parameter, the number to be rounded. Then the user could use whatever rounding algorithm they prefer, including custom ones.

By the way, Numeric#humanize falls into that category of monkeypatches with such a popular name that you are likely to run into namespace collisions (and resulting subtle bugs) in anything but a small, personal project.

Avdi
It's funny: what you see (probably correctly) as me being 'draconian' was actually me trying to be careful. I wanted to prevent the user from (say), passing in something pointless or harmful to the rest of the method. I like the idea of just accepting a block. I'm going to play with that and symbols and see what makes sense to me. As for the name, I'm open to suggestions if you have a better idea. I suppose that the benefit of `humanize` (that it's so obvious) is also a drawback here.
Telemachus
In general I do not advocate adding methods to the Ruby built-in classes. There are a number of alternatives, such as making it a utility method in an includable module (`humanize_number(42.3)`), or making a wrapper class which uses delegation (`n = HumaneNumber.new(42.3); n.to_s`).
Avdi
I'm very much a beginner in Ruby, but I thought it was one of the strengths of the language that its built-in classes were open to this kind of thing. (Introductory books certainly go out of their way to advertise it.) I'm not really disagreeing, and a utility method is fine - it was my first thought, actually. I decided extending `Numeric` was more idiomatic for Ruby. Again, thanks for the feedback.
Telemachus
@Telemachus: Avdi's opinion is his own. The problem with extending built-in classes is that there can be conflicts if several libraries are all trying to patch the same thing at once, but that's just a drawback that you need to keep in mind, not something that absolutely means you mustn't do it.
Chuck
It is indeed just my opinion. However in this case it's also true that I've seen (and written) more than one `#humanize` method in the wild. It's a popular extension.
Avdi
@Chuck: I'm glad to get all opinions since I'm still getting to know Ruby. @Avdi: if it's so common, that says people want it. (I know I did.) There's something like this in the `richunits` gem, but it's somewhat limited (and `richunits` is probably overkill if you just want this). I suppose it may fall into the category of "too easy to make a gem on its own", but "hard enough that I would rather do it right once and have it saved."
Telemachus
I think it's also a case of a method where a lot of people want it, but a lot of people have a different idea of what they want when they call `#humanize` on a number. You'll also find some helpers in ActiveSupport that do various numeric humanizations.
Avdi