Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How could I convert an array of paths into nested array or hash dependant upon length

Tags:

ruby

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.

like image 493
Rob Avatar asked Aug 08 '19 13:08

Rob


2 Answers

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=[]>]>]>]>
like image 169
George Avatar answered Nov 15 '22 07:11

George


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.

like image 27
Cary Swoveland Avatar answered Nov 15 '22 09:11

Cary Swoveland