ruby中的递归哈希变换函数

时间:2017-10-02 22:46:29

标签: ruby recursion hashmap iteration

我有一个响应模式的swagger(openAPI)定义:

h = { "type"=>"object",
      "properties"=>{
        "books"=>{
          "type"=>"array",
          "items"=>{
            "type"=>"object",
            "properties"=>{
              "urn"  =>{ "type"=>"string" },
              "title"=>{ "type"=>"string" }
            }
          }
        }
      }
    }

并希望将其转换为以下格式,以便能够将此响应显示为树:

{ "name"=>"200",
  "children"=> [
    {
      "name"=>"books (array)",
      "children"=> [
        {"name"=>"urn (string)" },
        {"name"=>"title (string)" }
      ]
    }
  ]
}

在swagger模式格式中,节点可以是对象(具有属性),也可以是项目数组,它们本身就是对象。这是我编写的函数:schema参数是上面显示的swagger格式的散列,而tree变量包含{name: "200"}

      def build_tree(schema, tree)
        if schema.class == ActiveSupport::HashWithIndifferentAccess
          case schema[:type]
          when 'object'
            tree[:children] = []
            schema[:properties].each do |property_name, property_schema|
               tree[:children] <<
                 { name: property_name, children: build_tree(property_schema, tree) }
            end
          when 'array'
            schema[:items].each do |property_name, property_schema|
              tree[:children] <<
                { name: property_name, children: build_tree(property_schema, tree) }
            end
          when nil
            tree[:name] == schema
          end
          else
            tree[:name] == schema
          end
        end

不幸的是我觉得我在某个地方犯了错误,因为这会返回以下Hash:

{ :name=>"200",
  :children=>[
    { :name=>"type", :children=>false },
    { :name=>"properties", :children=>false },
    { :name=>"books",
      :children=>{
        "type"=>"object",
        "properties"=>{
          "urn"=>{"type"=>"string"},
          "title"=>{"type"=>"string"}
        }
      }
    }
  ]
}

我必须错过递归中的一步或以错误的方式传递树,但我担心我没有足够的脑力来弄清楚:)也许是一个善良的灵魂与写美丽的红宝石的礼物代码会帮我一把!

3 个答案:

答案 0 :(得分:1)

递归!尝试Elixir: - )

要跟踪递归方法,我会写出大量puts并添加一个级别编号。

因为我没有Rails,所以我删除了Rails的东西。通过这种轻微的修改,您的输入(书籍数组不是数组!)和您的代码:

schema = 
    { "type"=>"object",
      "properties"=>{
        "books"=>{
          "type"=>"array",
          "items"=>{
            "type"=>"object",
            "properties"=>{
              "urn"  =>{ "type"=>"string" },
              "title"=>{ "type"=>"string" }
            }
          }
        }
      }
    }

tree = {}

def build_tree(schema, tree, level)
    puts "level=#{level} schema[:type]=#{schema['type'].inspect}, schema class is #{schema.class}"
    case schema['type']
    when 'object'
        puts "in when object for #{schema['properties'].size} properties :"
        i = 0
        schema['properties'].each_key{ | name | puts "#{i+=1}. #{name}" }
        tree[:children] = []
        schema['properties'].each do | property_name, property_schema |
            puts "level=#{level} property_name=#{property_name}"
            tree[:children] << { name: property_name, children: build_tree(property_schema, tree, level + 1) }
        end
    when 'array'
        puts "in when array for #{schema['items'].size} items will process following items :"
        i = 0
        schema['items'].each_key{ | name | puts "#{i+=1}. #{name}" }
        schema['items'].each do | property_name, property_schema |
            puts "level=#{level} property_name=#{property_name}, property_schema=#{property_schema.inspect}"
            tree[:children] << { name: property_name, children: build_tree(property_schema, tree, level + 1) }
        end
    when nil
        puts "in when nil"
        tree[:name] == schema
    end
end

build_tree(schema, tree, 1)
puts tree

结果就是你得到的:

$ ruby -w t_a.rb 
level=1 schema[:type]="object", schema class is Hash
in when object for 1 properties :
1. books
level=1 property_name=books
level=2 schema[:type]="array", schema class is Hash
in when array for 2 items will process following items :
1. type
2. properties
level=2 property_name=type, property_schema="object"
level=3 schema[:type]=nil, schema class is String
in when nil
level=2 property_name=properties, property_schema={"urn"=>{"type"=>"string"}, "title"=>{"type"=>"string"}}
level=3 schema[:type]=nil, schema class is Hash
in when nil
{:children=>[
    { :name=>"type", :children=>false}, 
    { :name=>"properties", :children=>false}, 
    { :name=>"books", 
      :children=>{
        "type"=>"object", 
        "properties"=>{
            "urn"=>{"type"=>"string"}, 
            "title"=>{"type"=>"string"}
        }
      }
    }
  ]
}

(注意:我手动打印了生成的树)。

跟踪显示正在发生的事情:在when 'array'编写schema['items'].each时,您可能希望迭代多个项目。但是没有项目,只有一个哈希。所以schema['items'].each会迭代密钥。然后,您使用没有'type'键的模式重复,因此case schema['type']属于when nil

请注意,如果递归调用when 'object'而不是when nil,则tree[:children] = []会删除之前的结果,因为您始终使用相同的初始tree。要堆叠中间结果,您需要在递归调用中提供新变量。

理解递归的最好方法不是循环到方法的开头,而是想象一连串的调用:

method_1
   |
   +------> method_2
               |
               +------> method_3

如果将与参数相同的初始参数传递给递归调用,则会被最后返回的值删除。但是如果你传递一个新变量,你可以在累积操作中使用它。

如果您检查过schema['items']实际上是一个数组,就像我在解决方案中那样,您会看到输入与预期不符:

$ ruby -w t.rb 
level=1 schema[:type]="object", schema class is Hash
in when object for 1 properties :
1. books
level=1 property_name=books
level=2 schema[:type]="array", schema class is Hash
in when array
oops ! Array expected
{:children=>[{:name=>"books", :children=>"oops ! Array expected"}]}

现在我的解决方案。我给你留下化妆品细节。

schema = 
    { "type"=>"object",
      "properties"=>{
        "books"=>{
          "type"=>"array",
          "items"=> [ # <----- added [
            { "type"=>"object",
              "properties" => {
                "urn"   => { "type"=>"string" },
                "title" => { "type"=>"string" }
                              }
            },
            { "type"=>"object",
              "properties" => {
                "urn2"   => { "type"=>"string" },
                "title2" => { "type"=>"string" }
                              }
            }
                    ] # <----- added ]
        } # end books
      } # end properties
    } # end schema

tree = {"name"=>"200", children: []}

def build_tree(schema, tree, level)
    puts
    puts "level=#{level} schema[:type]=#{schema['type'].inspect}, schema class is #{schema.class}"
    puts "level=#{level} tree=#{tree}"
    case schema['type']
    when 'object'
        puts "in when object for #{schema['properties'].size} properties :"
        i = 0
        schema['properties'].each_key{ | name | puts "#{i+=1}. #{name}" }
        schema['properties'].each do | property_name, property_schema |
            puts "object level=#{level}, property_name=#{property_name}"
            type, sub_tree = build_tree(property_schema, {children: []}, level + 1)
            puts "object level=#{level} after recursion, type=#{type} sub_tree=#{sub_tree}"
            child = { name: property_name + type }
            child[:children] = sub_tree unless sub_tree.empty?
            tree[:children] << child
        end
        puts "object level=#{level} about to return tree=#{tree}"
        tree
    when 'array'
        puts "in when array"
        case schema['items']
        when Array
            puts "in when Array for #{schema['items'].size} items"
            i     = 0
            items = []
            schema['items'].each do | a_hash |
                puts "item #{i+=1} has #{a_hash.keys.size} keys :"
                a_hash.keys.each{ | key | puts key }
                # if the item has "type"=>"object" and "properties"=>{ ... }, then
                # the whole item must be passed as argument to the next recursion
                puts "level=#{level} about to recurs for item #{i}"
                answer = build_tree(a_hash, {children: []}, level + 1)
                puts "level=#{level} after recurs, answer=#{answer}"
                items << { "item #{i}" => answer }
            end
            return ' (array)', items
        else
            puts "oops ! Array expected"
            "oops ! Array expected"
        end
    when 'string'
        puts "in when string, schema=#{schema}"
        return ' (string)', []
    else
        puts "in else"
        tree[:name] == schema
    end
end

build_tree(schema, tree, 1)
puts 'final result :'
puts tree

执行:

$ ruby -w t.rb 

level=1 schema[:type]="object", schema class is Hash
level=1 tree={"name"=>"200", :children=>[]}
in when object for 1 properties :
1. books
object level=1, property_name=books

level=2 schema[:type]="array", schema class is Hash
level=2 tree={:children=>[]}
in when array
in when Array for 2 items
item 1 has 2 keys :
type
properties
level=2 about to recurs for item 1

level=3 schema[:type]="object", schema class is Hash
level=3 tree={:children=>[]}
in when object for 2 properties :
1. urn
2. title
object level=3, property_name=urn

level=4 schema[:type]="string", schema class is Hash
level=4 tree={:children=>[]}
in when string, schema={"type"=>"string"}
object level=3 after recursion, type= (string) sub_tree=[]
object level=3, property_name=title

level=4 schema[:type]="string", schema class is Hash
level=4 tree={:children=>[]}
in when string, schema={"type"=>"string"}
object level=3 after recursion, type= (string) sub_tree=[]
object level=3 about to return tree={:children=>[{:name=>"urn (string)"}, {:name=>"title (string)"}]}
level=2 after recurs, answer={:children=>[{:name=>"urn (string)"}, {:name=>"title (string)"}]}
item 2 has 2 keys :
type
properties
level=2 about to recurs for item 2

level=3 schema[:type]="object", schema class is Hash
level=3 tree={:children=>[]}
in when object for 2 properties :
1. urn2
2. title2
object level=3, property_name=urn2

level=4 schema[:type]="string", schema class is Hash
level=4 tree={:children=>[]}
in when string, schema={"type"=>"string"}
object level=3 after recursion, type= (string) sub_tree=[]
object level=3, property_name=title2

level=4 schema[:type]="string", schema class is Hash
level=4 tree={:children=>[]}
in when string, schema={"type"=>"string"}
object level=3 after recursion, type= (string) sub_tree=[]
object level=3 about to return tree={:children=>[{:name=>"urn2 (string)"}, {:name=>"title2 (string)"}]}
level=2 after recurs, answer={:children=>[{:name=>"urn2 (string)"}, {:name=>"title2 (string)"}]}
object level=1 after recursion, type= (array) sub_tree=[{"item 1"=>{:children=>[{:name=>"urn (string)"}, {:name=>"title (string)"}]}}, {"item 2"=>{:children=>[{:name=>"urn2 (string)"}, {:name=>"title2 (string)"}]}}]
object level=1 about to return tree={"name"=>"200", :children=>[{:name=>"books (array)", :children=>[{"item 1"=>{:children=>[{:name=>"urn (string)"}, {:name=>"title (string)"}]}}, {"item 2"=>{:children=>[{:name=>"urn2 (string)"}, {:name=>"title2 (string)"}]}}]}]}
final result :
{"name"=>"200", :children=>[{:name=>"books (array)", :children=>[{"item 1"=>{:children=>[{:name=>"urn (string)"}, {:name=>"title (string)"}]}}, {"item 2"=>{:children=>[{:name=>"urn2 (string)"}, {:name=>"title2 (string)"}]}}]}]}

编辑结果:

{"name"=>"200", 
 :children=>[
   {
     :name=>"books (array)",
     :children=>[
       {"item 1"=>{
         :children=>[
           {:name=>"urn (string)"}, 
           {:name=>"title (string)"}
         ]
        }
       },
       {"item 2"=>{
         :children=>[
           {:name=>"urn2 (string)"}, 
           {:name=>"title2 (string)"}
         ]
        }
       }
     ]
   }
 ]
}

答案 1 :(得分:1)

@BernardK建议的解决方案的长度令人印象深刻,但我无法让它发挥作用。这是我更谦虚的解决方案。我将它包装在一个类中,以便我可以正确地测试它。

您的代码存在的一个问题是,在某些地方,您将返回tree[:name] == schema,其评估结果为false。我认为您打算分配tree[:name] = schema,然后返回tree

和@BernardK一样,我假设一个类型&#39;数组的模式&#39;作为它的价值,它将具有一系列的东西。如果这不是它的工作方式,那么请您提供一个示例,其中包括&#39; array&#39;不仅仅是对象&#39;周围的附加层?

希望在这个答案和另一个答案之间,你可以从中找到适合你的东西。

# swagger.rb

class Swagger

  def self.build_tree(schema, tree)
    if schema.class == ActiveSupport::HashWithIndifferentAccess
      case schema['type']
      when 'object'
        tree['children'] = schema['properties'].map do |property_name, property_schema|
          build_tree(property_schema, {'name' => property_name})
        end
        tree
      when 'array'
        schema['items'].map do |item|
          build_tree(item, {'name' => "#{tree['name']} (array)"})
        end
      when 'string'
        {'name' => "#{tree['name']} (string)"}
      end
    else
      raise ArgumentError, "Expected a HashWithIndifferentAccess but got #{schema.class}: #{schema}"
    end
  end
end

这是spec文件:

# /spec/swagger_spec.rb

require_relative '../swagger'

describe Swagger do
  describe '.build_tree' do
    context 'when given a Hash whose type is string' do
      let(:tree) { {"name" => "urn"} }
      let(:schema) { {"type" => "string"}.with_indifferent_access }
      let(:expected) { {"name" => "urn (string)"} }

      it 'returns a Hash with "name" as the key and the tree value and its type as the value' do
        expect(Swagger.build_tree(schema, tree)).to eq(expected)
      end
    end

    context 'when given a simple schema' do
      let(:tree) { {"name" => "200"} }
      let(:schema) { {"type" => "object",
                      "properties" => {
                          "urn" => {"type" => "string"},
                          "title" => {"type" => "string"}
                      }}.with_indifferent_access }

      let(:expected) { {"name" => "200",
                        "children" => [{"name" => "urn (string)"},
                                       {"name" => "title (string)"}
                        ]} }

      it 'transforms the tree into swagger (openAPI) format' do
        expect(Swagger.build_tree(schema, tree)).to eq(expected)
      end
    end

    context 'when given a complicated schema' do
      let(:tree) { {"name" => "200"} }
      let(:schema) { {"type" => "object",
                      "properties" =>
                          {"books" =>
                               {"type" => "array",
                                "items" =>
                                    [{"type" => "object",
                                      "properties" =>
                                          {"urn" => {"type" => "string"}, "title" => {"type" => "string"}}
                                     }] # <-- added brackets
                               }
                          }
      }.with_indifferent_access }

      let(:expected) { {"name" => "200",
                        "children" =>
                            [[{"name" => "books (array)",
                              "children" => [{"name" => "urn (string)"}, {"name" => "title (string)"}]
                              }]]
      } }

      it 'transforms the tree into swagger (openAPI) format' do
        expect(Swagger.build_tree(schema, tree)).to eq(expected)
      end
    end

    context 'when given a schema that is not a HashWithIndifferentAccess' do
      let(:tree) { {"name" => "200"} }
      let(:schema) { ['random array'] }

      it 'raises an error' do
        expect { Swagger.build_tree(schema, tree) }.to raise_error ArgumentError
      end
    end
  end
end

答案 2 :(得分:1)

因此,项目数组不是项目数组,而是子模式的属性数组。这是考虑到这一事实的新解决方案:

schema = 
    { "type"=>"object",
      "properties"=>{
        "books"=>{
          "type"=>"array",
          "items"=> {
              "type"=>"object",
              "properties" => {
                "urn"   => { "type"=>"string" },
                "title" => { "type"=>"string" }
                              }
          } # end items
        } # end books
      } # end properties
    } # end schema

tree = {"name"=>"200"}

def build_tree(schema, tree, level)
    puts
    puts "level=#{level} schema[:type]=#{schema['type'].inspect}, schema class is #{schema.class}"
    puts "level=#{level} tree=#{tree}"
    case schema['type']
    when 'object'
        puts "in when object for #{schema['properties'].size} properties :"
        i = 0
        schema['properties'].each_key{ | name | puts "#{i+=1}. #{name}" }
        tree[:children] = []
        schema['properties'].each do | property_name, property_schema |
            puts "object level=#{level}, property_name=#{property_name}"
            type, sub_tree = build_tree(property_schema, {}, level + 1)
            puts "object level=#{level} after recursion, type=#{type} sub_tree=#{sub_tree}"
            child = { name: property_name + type }
            sub_tree.each { | k, v | child[k] = v }
            tree[:children] << child
        end
        puts "object level=#{level} about to return tree=#{tree}"
        tree
    when 'array'
        puts "in when array"
        case schema['items']
        when Hash
            puts "in when Hash"
            puts "the schema has #{schema['items'].keys.size} keys :"
            schema['items'].keys.each{ | key | puts key }
            # here you could raise an error if the two keys are NOT "type"=>"object" and "properties"=>{ ... }
            puts "Hash level=#{level} about to recurs"
            return ' (array)', build_tree(schema['items'], {}, level + 1)
        else
            puts "oops ! Hash expected"
            "oops ! Hash expected"
        end
    when 'string'
        puts "in when string, schema=#{schema}"
        return ' (string)', {}
    else
        puts "in else"
        tree[:name] == schema # ???? comparison ?
    end
end

build_tree(schema, tree, 1)
puts 'final result :'
puts tree

编辑结果(使用ruby 2.3.3p222测试):

{ "name"=>"200", 
  :children=> [
    {
      :name=>"books (array)", 
      :children=> [
        {:name=>"urn (string)"}, 
        {:name=>"title (string)"}
      ]
    }
  ]
}

不要把它当作精彩的代码。我在每次12级地震中编写Ruby代码。目的是解释在代码中不起作用的内容,并引起注意在递归调用中使用新变量(现在是一个空哈希)。有很多案例需要测试并引发错误。

正确的方式是BDD和@moveson一样:首先为所有情况编写RSpec测试,特别是边缘情况,然后编写代码。我知道它给人的感觉太慢,但从长远来看,它会支付并取代调试和打印痕迹。

有关测试的更多信息

此代码很脆弱:例如,如果类型键未与属性键关联,则它将在schema['properties'].eachundefined method 'each' for nil:NilClass处失败。一个规格就好 context 'when a type object has no properties' do let(:schema) { {"type" => "object", "xyz" => ...

有助于添加代码来检查前置条件。我也懒惰地使用RSpec来制作小脚本,但是为了认真开发,我努力,因为我已经认识到了它的好处。在调试中花费的时间将永远丢失,投入到规范中的时间可以在发生更改时提供安全性,并提供关于代码执行或不执行的清晰易读的报告。我推荐全新的Rspec 3 book

关于访问哈希的另一个词:如果你有混合的字符串和符号,那就是问题的根源。

some_key = some_data # sometimes string, sometimes symbol
schema[some_key]...
如果内部键与外部数据的类型不同,

将找不到该元素。在创建哈希时选择一种类型,例如符号,并系统地将访问变量转换为符号:

some_key = some_data # sometimes string, sometimes symbol
schema[some_key.to_sym]...

或全部为字符串:

some_key = some_data # sometimes string, sometimes symbol
schema[some_key.to_s]...