tags:

views:

56

answers:

1

What's an efficient, rubyesque way of testing if a collection supports string indexing?

Long version: I'd like for a class of mine to normalize certain values. To achieve this, an instance takes a collection of values as its 'values' attribute. I'd like values= to accept both lists (integer indexed collections, including the built-in Array) and associative arrays (object indexed collections, such as Hash). If passed a list, it turns the list into an associative array by inverting keys & values. This means the method needs to distinguish lists from associative arrays. It should also work on any indexed collection, not just descendants of Array and Hash, so any sort of type sniffing the collection type is considered ugly and wrong. Type sniffing the index type, however...

Currently, I'm using exceptions to tell the difference, but I prefer to use exceptions for, well, exceptional circumstances rather than a general control structure. It's just a personal preference, one I'm not too attached to. If exceptions are the ruby way to solve this problem, please let me know.

def values=(values)
    begin
        values['']
        @values = values.dup
    rescue TypeError
        @values = Hash[ values.zip((0..values.length-1).to_a) ]
    end
    @values.each_value { |v| @values[v] = v}
end

Note: a complete solution would take the transitive closure of values, but for now I can assume the keys & values of values are from different domains.

The point of all this is to enable code like:

toggle.values = [:off, :on]
toggle.normalise(:off) == 0
toggle.normalise(1) == 1

bool.values = {:off => 0, :false => 0, :no => 0,
               :on => 1,  :true => 1,  :yes => 1}
bool.normalise(:yes) == 1
bool.normalise(0) == 0

PS. This is for a personal project, so elegance and the Ruby way are paramount. I'm looking for interesting approaches, especially if they illustrate an interesting concept (such as "exceptions can be used as behavioral tests").

+2  A: 

Duck typing to the rescue!

 hash = if collection.respond_to? :to_hash
          collection.to_hash
        elsif collection.respond_to? :to_ary
          collection.to_ary.inject({}) { |_hash,(key,value)| _hash.merge!(key => value) }
        else if collection.respond_to? :inject
          collection.inject({}) { |_hash,(key,value)| _hash.merge!(key => value) }
        else
          raise ArgumentError, "not a collection type I understand"
        end
 if want_dupe and collection.object_id == hash.object_id
    hash = hash.dup
 end
rampion
Yay duck typing! Any clean way of ensuring that collection.to_hash is a duplicate without creating an intermediate, temporary duplicate (as collection.to_hash.dup might do)?
outis
"Look, up in the sky! It's a bird. It's a plane. No, I was right the first time, it's a bird."
outis
You could compare object_ids. It's not like magically clean, but it works.
rampion