I need to convert an array of string paths into an array of symbols, hashes and arrays dependant upon the length of the string path
Given the following array:
array = ["info", "services", "about/company", "about/history/part1", "about/history/part2"]
I would like to produce the following output, grouping the different levels, using a mixture of symbols and objects depending on the structure at the level.
Produce the following output:
[
:info,
:services,
about: [
:company,
history: [
:part1,
:part2
]
]
]
# alt syntax
[
:info,
:services,
{
:about => [
:company,
{
:history => [
:part1,
:part2
]
}
]
}
]
If a path has sub paths, it would become an object If there is no sub paths, the path is converted to a symbol.
I am struggling with supporting infinite recursion deciding when and how to make the object.
This will give you what you need:
array.each.with_object([]) do |path_string, result|
path = path_string.split('/').map(&:to_sym)
node = path[0..-2].reduce(result) do |memo, next_node|
memo << {} unless memo.last.is_a?(Hash)
memo.last[next_node] ||= []
end
node.unshift(path[-1])
end
#=> [:services, :info, {:about=>[:company, {:history=>[:part2, :part1]}]}]
I don't know what you want to use the result for, but I think you'll probably find it's a bit unwieldy. If it works for your situation I suggest a structure like this instead:
Node = Struct.new(:name, :children)
array.each.with_object(Node.new(nil, [])) do |path_string, root_node|
path = path_string.split('/').map(&:to_sym)
path.reduce(root_node) do |node, next_node_name|
next_node = node.children.find { |c| c.name == next_node_name }
if next_node.nil?
next_node = Node.new(next_node_name, [])
node.children << next_node
end
next_node
end
end
#=> #<struct Node name=nil, children=[#<struct Node name=:info, children=[]>, #<struct Node name=:services, children=[]>, #<struct Node name=:about, children=[#<struct Node name=:company, children=[]>, #<struct Node name=:history, children=[#<struct Node name=:part1, children=[]>, #<struct Node name=:part2, children=[]>]>]>]>
Code
def recurse(arr)
w_slash, wo_slash = arr.partition { |s| s.include?('/') }
a = w_slash.group_by { |s| s[/[^\/]+/] }.
map { |k,v| { k.to_sym=>recurse(v.map { |s| s[/(?<=\/).+/] }) } }
wo_slash.map(&:to_sym) + a
end
Examples
recurse array
#=> [:info,
# :services,
# {:about=>[:company, {:history=>[:part1, :part2]}]}]
arr = (array + ["a/b", "a/b/c", "a/b/c/d", "a/b/c/d/e"]).shuffle
#=> ["a/b/c", "services", "about/company", "about/history/part1", "info",
# "a/b", "about/history/part2", "a/b/c/d/e", "a/b/c/d"]
recurse arr
#=> [:services,
# :info,
# {:a=>[:b, {:b=>[:c, {:c=>[:d, {:d=>[:e]}]}]}]},
# {:about=>[:company, {:history=>[:part1, :part2]}]}]
Explanation
See Enumerable#partition and Enumerable#group_by.
The regular expression /[^\/]+/
reads, "match one or more characters that are not forward slashes". This could alternatively be written, s[0, s.index('/')]
.
The regular expression /(?<=\/).+/
reads, "match all characters following the first forward slash", (?<=\/)
being a positive lookbehind. This could alternatively be written, s[s.index('/')+1..-1]
.
The initial steps are as follows.
w_slash, wo_slash = array.partition { |s| s.include?('/') }
#=> [["about/company", "about/history/part1", "about/history/part2"],
# ["info", "services"]]
w_slash
#=> ["about/company", "about/history/part1", "about/history/part2"]
wo_slash
#=> ["info", "services"]
h = w_slash.group_by { |s| s[/\A[^\/]+/] }
#=> {"about"=>["about/company", "about/history/part1", "about/history/part2"]}
a = h.map { |k,v| { k.to_sym=>recurse(v.map { |s| s[/(?<=\/).+/] }) } }
#=> [{:about=>[:company, {:history=>[:part1, :part2]}]}]
b = wo_slash.map(&:to_sym)
#=> [:info, :services]
b + a
#=> <as shown above>
In computing a
, the first (and only) key-value pair of h
is passed to the block and the two block variables are assigned values:
k,v = h.first
#=> ["about", ["about/company", "about/history/part1", "about/history/part2"]]
k #=> "about"
v #=> ["about/company", "about/history/part1", "about/history/part2"]
and the block calculations are preformed:
c = v.map { |s| s[/(?<=\/).+/] }
#=> ["company", "history/part1", "history/part2"]
{ k.to_sym=>recurse(c) }
#=> {:about=>[:company, {:history=>[:part1, :part2]}]}
and so on.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With