Friday, November 15, 2013

Recursively Sort Arrays and Hashes by to_s in Ruby

Just wrote the following to recursively sort arrays and hashes in Ruby. It's not perfect, but it is one way to attempt to canonicalize some types of unordered data, e.g. for comparison:


def recursively_sort_arrays_and_hashes!(obj)
  case obj
  when Array
    obj.map!{|v| recursively_sort_arrays_and_hashes!(v)}.sort_by!{|v| (v.to_s rescue nil) }
  when Hash
    obj = Hash[Hash[obj.map{|k,v| [recursively_sort_arrays_and_hashes!(k),recursively_sort_arrays_and_hashes!(v)]}].sort_by{|k,v| [(k.to_s rescue nil), (v.to_s rescue nil)]}]
  else
    obj
  end
end

More often, you just want to order hashes and leave arrays ordered as they are, in which case use:


def recursively_sort_hashes!(obj)
  case obj
  when Array
    obj.map!{|v| recursively_sort_hashes!(v)}
  when Hash
    obj = Hash[Hash[obj.map{|k,v| [recursively_sort_hashes!(k),recursively_sort_hashes!(v)]}].sort_by{|k,v| [(k.to_s rescue nil), (v.to_s rescue nil)]}]
  else
    obj
  end
end

E.g. This can be used for json equality in MiniTest::Spec:


class Object
  def must_be_json_equal(b)
    JSON.pretty_generate(recursively_sort_hashes!(self)).must_equal(JSON.pretty_generate(recursively_sort_hashes!(JSON.parse(b.to_json))))
  end
end

# then in a test...
JSON.parse(response.body).must_be_json_equal {b: 1, a: 2}

Note: a much better way to do json comparisons as of 2013-12-11 is json_expressions.

No comments: