Nested arrays and hashes

Rows of hashes

Most APIs answer with a list of objects, and Ruby reads that as an array of hashes. One row per record, one key per column, and the work is almost always grouping or sorting:

rows = [ { n: "a", v: 2 }, { n: "b", v: 1 }, { n: "c", v: 2 } ]

The block variable is a hash, so row[:n] reads a column and row[:v] reads the one to group by. Ruby 3.4 lets _1 stand for the first block argument, which keeps the one-liners short.

group_by

group_by returns a hash of lists keyed by the block result, and transform_values turns each list into the values the report needs:

ruby -e 'rows = [{n: "a", v: 2}, {n: "b", v: 1}, {n: "c", v: 2}]; p rows.group_by { _1[:v] }.transform_values { |r| r.map { _1[:n] } }'

{2 => ["a", "c"], 1 => ["b"]}

The keys keep the order of first appearance, not a sorted order. Sort the result when a report should be stable: sort_by { |k, _| k } is enough for numeric keys.

Sorting by a key

sort_by with one key is the readable form; a second key in the block decides ties:

ruby -e 'rows = [{n: "a", v: 2}, {n: "b", v: 1}]; p rows.sort_by { _1[:v] }.map { _1[:n] }'

["b", "a"]

For more than one key, return an array from the block: sort_by { |row| [row[:v], row[:n]] }. Ruby 3.x sorts arrays element by element, so the first key decides and the second breaks the tie, which is what a report header implies.

What to watch for

A missing key in a row raises at the first row that lacks it, and the error names the nil. Validate the columns once, before the loop (rows.each { |row| row.fetch(:n) }), or use dig when a column is optional.

Deeply nested rows are the usual place where a small class is better than a hash. When three levels of [:a][:b][:c] appear in a method, a Struct or a class with named readers says the same thing and fails earlier.

Next: Enumerable by task

Article author: Arthur Isaev