Iterating both hashes and arrays in ruby is charming. One might chain iterators, map, reduce, filter, select, reject, zip… Everybody having at least eight hours of experience with ruby has definitely seen (and even maybe written) something like this:
%w[aleksei saverio].map do |name|
name.capitalize
end.each do |capitalized_name|
puts "Hello, #{capitalized_name}!"
end
That is really handy. The things gets cumbersome when it comes to deeply nested structures, like a hash having nested hashes, arrays etc. The good example of that would be any configuration file, loaded from YAML.
So, welcome the library that makes the iteration of any nested hash/array
combination almost as easy as the natural ruby map
and each
.
Intro
Since the library monkeypatches core classes, it uses spanish names for iteration methods. There is a plan to make it better memorizable, see more about it in the end of this post.
Features
cada
(sp.each
) iterates through all the levels of the nestedEnumerable
, yieldingparent, element
tuple; parent is returned as a delimiter-joined stringmapa
(sp.map
) iterates all the elements, yieldingparent, (key, value)
; the mapper should return either[key, value]
array ornil
to remove this element;- NB this method always maps to
Hash
, to map toArray
useplana_mapa
- NB this method will raise if the returned value is neither
[key, value]
tuple nornil
- NB this method always maps to
plana_mapa
iterates yieldingkey, value
, maps to the yielded value, whatever it is;nil
s are not treated in some special wayaplanar
(sp.flatten
) the analogue ofArray#flatten
, but flattens the deep enumerable intoHash
instancerecoger
(sp.harvest
,collect
) the opposite toaplanar
, it builds the nested structure out of flattened hashsegar
(sp.yield
), aliasescoger
(sp.select
) allows to filter and collect elelementsrechazar
(sp.reject
) allows to filter out and collect elelementscompactar
(sp.compact
), allows to filter out allnil
s.
Words are cheap, show me the code
▶ require 'iteraptor'
#⇒ true
▶ hash = {company: {name: "Me", currencies: ["A", "B", "C"],
▷ password: "12345678",
▷ details: {another_password: "QWERTYUI"}}}
#⇒ {:company=>{:name=>"Me", :currencies=>["A", "B", "C"],
# :password=>"12345678",
# :details=>{:another_password=>"QWERTYUI"}}}
▶ hash.segar(/password/i) { "*" * 8 }
#⇒ {"company"=>{"password"=>"********",
# "details"=>{"another_password"=>"********"}}}
▶ hash.segar(/password/i) { |*args| puts args.inspect }
["company.password", "12345678"]
["company.details.another_password", "QWERTYUI"]
#⇒ {"company"=>{"password"=>nil, "details"=>{"another_password"=>nil}}}
▶ hash.rechazar(/password/)
#⇒ {"company"=>{"name"=>"Me", "currencies"=>["A", "B", "C"]}}
▶ hash.aplanar
#⇒ {"company.name"=>"Me",
# "company.currencies.0"=>"A",
# "company.currencies.1"=>"B",
# "company.currencies.2"=>"C",
# "company.password"=>"12345678",
# "company.details.another_password"=>"QWERTYUI"}
▶ hash.aplanar.recoger
#⇒ {"company"=>{"name"=>"Me", "currencies"=>["A", "B", "C"],
# "password"=>"12345678",
# "details"=>{"another_password"=>"QWERTYUI"}}}
▶ hash.aplanar.recoger(symbolize_keys: true)
#⇒ {:company=>{:name=>"Me", :currencies=>["A", "B", "C"],
# :password=>"12345678",
# :details=>{:another_password=>"QWERTYUI"}}}
In Details
Simple Iterating
Iteraptor#cada(**params, &λ)
— iterates the nested structure, yielding
the keys (concatenated with Iteraptor::DELIMITER
or whatever is passed
as delimiter
keyword argument.) The returned from the block value is discarded.
block arguments: key, value
Example:
▶ {foo1: 42, foo2: %i[bar1 bar2], foo3: {foo4: {foo5: 3.14, foo6: :baz}}}.
▷ .cada { |key, value| puts [key, value].inspect }
# ["foo1", 42]
# ["foo2", [:bar1, :bar2]]
# ["foo2.0", :bar1]
# ["foo2.1", :bar2]
# ["foo3", {:foo4=>{:foo5=>3.14, :foo6=>:baz}}]
# ["foo3.foo4", {:foo5=>3.14, :foo6=>:baz}]
# ["foo3.foo4.foo5", 3.14]
# ["foo3.foo4.foo6", :baz]
#⇒ {:foo1=>42, :foo2=>[:bar1, :bar2], :foo3=>{:foo4=>{:foo5=>3.14, :foo6=>:baz}}}
Simple Mapping
Iteraptor#mapa(**params, &λ)
— iterates the nested structure,
yielding the parent key, key and value. The value, returned from the block
should be a single value (while iterating through arrays, value
block
argument is nil
,) of either [key, value]
tuple or nil
while
iterating over hashes. In the latter case if nil
is returned, the resulting
value is removed from the result (NB: this behaviour might change.)
block arguments: parent, (key, value)
Example:
▶ {foo1: 42, foo2: %i[bar1 bar2], foo3: {foo4: {foo5: 3.14, foo6: :baz}}}.
▷ mapa { |parent, (k, v)| puts parent; v ? [k, "==#{v}=="] : k }
# foo1
# foo2.0
# foo2.1
# foo3.foo4.foo5
# foo3.foo4.foo6
#⇒ {:foo1=>"==42==", :foo2=>[:bar1, :bar2],
# :foo3=>{:foo4=>{:foo5=>"==3.14==", :foo6=>"==baz=="}}}
Filtering
Iteraptor#escoger(*filters, **params, &λ)
— filters the receiver
according to the set of filters given (filters use case-equality) and,
optionally, iterates the resulting structure if the block was given. Might
be treated an extended analogue of Enumerable#select
.
alias: segar
, block arguments: key, value
Example:
▶ {foo1: 42, foo2: %i[bar1 bar2], foo3: {foo4: {foo5: 3.14, foo6: :baz}}}.
▷ escoger(/foo4/)
#⇒ {"foo3"=>{"foo4"=>{"foo5"=>3.14, "foo6"=>:baz}}}
▷ escoger(->(k) { k == "foo3" })
#⇒ {"foo3"=>{"foo4"=>{"foo5"=>3.14, "foo6"=>:baz}}}
▶ {foo1: 42, foo2: %i[bar1 bar2], foo3: {foo4: {foo5: 3.14, foo6: :baz}}}.
▷ segar(/foo[16]/) { |key, value| 3.14 }
#⇒ {"foo1"=>3.14, "foo3"=>{"foo4"=>{"foo6"=>3.14}}}
Iteraptor#rechazar(*filters, **params, &λ)
— the exactly opposite to
Iteraptor#escoger
. Might be treated an extended analogue of Enumerable#select
.
Example:
▶ {foo1: 42, foo2: %i[bar1 bar2], foo3: {foo4: {foo5: 3.14, foo6: :baz}}}.
▷ rechazar(/[15]/)
#⇒ {"foo2"=>[:bar1], "foo3"=>{"foo4"=>{"foo6"=>:baz}}}
Iteraptor#compactar(**params)
— the analogue of Array#compact
to some
extent. Iteraptor#compactar
removes all the deeply nested keys having nil
value.
Example:
▶ {foo1: nil, foo2: %i[nil bar2], foo3: {foo4: {foo5: nil, foo6: :baz}}}.
▷ compactar()
#⇒ {"foo2"=>[:bar2], "foo3"=>{"foo4"=>{"foo6"=>:baz}}}
Flattening
Iteraptor#aplanar(**params, &λ)
— flattens the receiver, concatenating
keys with Iteraptor::DELIMITER
or whatever is passed as delimiter
keyword
argument. Might be treated an extended analogue of Enumerable#flatten
.
If block passed, key, value
pair is yielded to it. The returned value
is discarded.
block arguments: key, value
Example:
▶ {foo1: 42, foo2: %i[bar1 bar2], foo3: {foo4: {foo5: 3.14, foo6: :baz}}}.
▷ aplanar(delimiter: "_")
#⇒ {"foo1"=>42, "foo2_0"=>:bar1, "foo2_1"=>:bar2,
# "foo3_foo4_foo5"=>3.14, "foo3_foo4_foo6"=>:baz}
Iteraptor#recoger(**params)
— de-flattens the receiver, building
the nested structure back. Knows now to deal with arrays.
Example:
▶ {"foo1"=>42, "foo2_0"=>:bar1, "foo2_1"=>:bar2, "foo3_foo4_foo5"=>3.14,
▷ "foo3_foo4_foo6"=>:baz}.recoger(delimiter: "_", symbolize_keys: true)
#⇒ {:foo1=>42, :foo2=>[:bar1, :bar2], :foo3=>{:foo4=>{:foo5=>3.14, :foo6=>:baz}}}
The source code is linked above, the gem
is available through rubygems.