【问题标题】:How to deep transform values on Ruby hash如何在 Ruby 哈希上深度转换值
【发布时间】:2019-08-01 07:13:54
【问题描述】:

我有一个如下所示的哈希:

hash = {
  'key1' => ['value'],
  'key2' => {
    'sub1' => ['string'],
    'sub2' => ['string'],
  },
  'shippingInfo' => {
                   'shippingType' => ['Calculated'],
                'shipToLocations' => ['Worldwide'],
              'expeditedShipping' => ['false'],
        'oneDayShippingAvailable' => ['false'],
                   'handlingTime' => ['3'],
    }
  }

我需要将每个值转换为数组中的单个字符串,以使其最终如下所示:

hash = {
  'key1' =>  'value' ,
  'key2' => {
    'sub1' => 'string' ,
    'sub2' => 'string' ,
  },
  'shippingInfo' => {
                   'shippingType' => 'Calculated' ,
                'shipToLocations' => 'Worldwide' ,
              'expeditedShipping' => 'false' ,
        'oneDayShippingAvailable' => 'false' ,
                   'handlingTime' => '3' ,
    }
  }

我找到了这个,但无法让它工作 https://gist.github.com/chris/b4138603a8fe17e073c6bc073eb17785

【问题讨论】:

    标签: ruby hash transform


    【解决方案1】:

    我注意到很多答案都带有不必要的递归。当前版本的带有 ActiveSupport 的 Ruby 2.7.x(我用 6.1.4.4 测试过)将允许您这样做:

    输入数据:

    hash = {
      'key1' => ['value'],
      'key2' => {
        'sub1' => ['string'],
        'sub2' => ['string']},
      'shippingInfo' => {
        'shippingType' => ['Calculated'],
        'shipToLocations' => ['Worldwide', 'Web'],
        'expeditedShipping' => ['false'],
        'oneDayShippingAvailable' => ['false'],
        'handlingTime' => ['3']}}
    

    解决方案:

    hash.deep_transform_values do |value|
      # whatever you need to do to any nested value, like:
      if value == value.to_i.to_s
        value.to_i
      else
        value
      end
    end
    

    上面的例子会返回一个类型转换的 String 到 Integer。

    【讨论】:

    • Hash#deep_transform_values 需要 active_support 并且您需要使用正确的版本。包含这些详细信息以使此答案有效。
    • @lacostenycoder 我提到了 ActiveSupport 和 ruby​​ 版本,为了完整起见,也会添加 gem 的版本。
    • 在实例化一个新的哈希对象之前需要这两行代码。 require 'active_support' require 'active_support/core_ext'
    【解决方案2】:
    hash = {
      'key1' => ['value'],
      'key2' => {
        'sub1' => ['string'],
        'sub2' => ['string'],
      },
      'shippingInfo' => {
                       'shippingType' => ['Calculated'],
                    'shipToLocations' => ['Worldwide', 'Web'],
                  'expeditedShipping' => ['false'],
            'oneDayShippingAvailable' => ['false'],
                       'handlingTime' => ['3'],
        }
      }
    

    def recurse(hash)
      hash.transform_values do |v|
        case v
        when Array
          v.size == 1 ? v.first : v
        when Hash
          recurse v
        else
          # raise exception
        end
      end
    end
    

    recurse hash
      #=> {"key1"=>"value",
      #    "key2"=>{
      #      "sub1"=>"string",
      #      "sub2"=>"string"
      #    },
      #    "shippingInfo"=>{
      #      "shippingType"=>"Calculated",
      #      "shipToLocations"=>["Worldwide", "Web"],
      #      "expeditedShipping"=>"false",
      #      "oneDayShippingAvailable"=>"false",
      #      "handlingTime"=>"3"
      #    }
      #  } 
    

    【讨论】:

    • 我看到我的答案与@Sebastian 之前的答案相似。
    • 是的,除了使用开关样式外非常相似,但仍然有效。这是否可以很好地转换为 Hash 类的猴子补丁?
    【解决方案3】:

    作为替代方案,考虑使用一个对象并允许初始化器为您解构一些键。

    很多像我这样的人开始使用 Ruby 来支持 Perl 的原因之一是因为可以更好地表达对象,而不是像数组和散列这样的原语。充分利用它!

    class ShippingStuff # You've kept the data vague
    
      def initialize key1:, key2:, shippingInfo:
        @blk = -> val {
          val.respond_to?(:push) && val.size == 1 ?
              val.first :
              cleankeys(val)
        }
        @key1 = cleankeys key1
        @key2 = cleankeys key2
        @shippingInfo = shippingInfo
      end
    
      attr_reader :key1, :key2, :shippingInfo
    
      # basically a cut down version of what
      # Sebastian Palma answered with
      def cleankeys data
        if data.respond_to? :transform_values
          data.transform_values &@blk
        else
          @blk.call(data)
        end
      end
    
    end
    
    
    hash = {
      'key1' => ['value'],
      'key2' => {
        'sub1' => ['string'],
        'sub2' => ['string'],
      },
      'shippingInfo' => {
                       'shippingType' => ['Calculated'],
                    'shipToLocations' => ['Worldwide'],
                  'expeditedShipping' => ['false'],
            'oneDayShippingAvailable' => ['false'],
                       'handlingTime' => ['3'],
      }
    }
    
    shipper = ShippingStuff.new hash.transform_keys!(&:to_sym)
    shipper.key1
    # "value"
    shipper.key2
    # {"sub1"=>"string", "sub2"=>"string"}
    shipper.shippingInfo
    # {"shippingType"=>["Calculated"], "shipToLocations"=>["Worldwide"], "expeditedShipping"=>["false"], "oneDayShippingAvailable"=>["false"], "handlingTime"=>["3"]}
    

    同样,我什至会为 shippingInfo 数据创建一个 Info 类。

    如果key1key2 是动态的,您可能会遇到不同的问题,但也有解决办法(double splat 就是其中之一)。

    【讨论】:

    • 这可能是一个更好的模式,虽然有点死板。这里的数据结构是一个非特定的例子,
    • @lacostenycoder 当然,只是想提供一个替代方案,我不是在批评。 “只要我的 2 美分”可能会让它成为批评,可能是因为人们在批评别人之前使用了它,对吧?我应该找到一个替代短语...(o_º)
    【解决方案4】:

    类似的东西呢:

    def deep_transform_values(hash)
      return hash unless hash.is_a?(Hash)
    
      hash.transform_values do |val|
        if val.is_a?(Array) && val.length == 1
          val.first
        else
          deep_transform_values(val)
        end
      end
    end
    

    用类似的东西测试过:

    hash = {
      'key1' => ['value'],
      'key2' => {
        'sub1' => ['string'],
        'sub2' => ['string'],
      },
      'shippingInfo' => {
                       'shippingType' => ['Calculated'],
                    'shipToLocations' => ['Worldwide'],
                  'expeditedShipping' => ['false'],
            'oneDayShippingAvailable' => ['false'],
                       'handlingTime' => ['3'],
                       'an_integer' => 1,
                       'an_empty_array' => [],
                       'an_array_with_more_than_one_elements' => [1,2],
                       'a_symbol' => :symbol,
                       'a_string' => 'string'
        }
      }
    

    给予:

    {
      "key1"=>"value",
      "key2"=>{
        "sub1"=>"string",
        "sub2"=>"string"
      },
      "shippingInfo"=> {
        "shippingType"=>"Calculated",
        "shipToLocations"=>"Worldwide",
        "expeditedShipping"=>"false",
        "oneDayShippingAvailable"=>"false",
        "handlingTime"=>"3",
        "an_integer"=>1,
        "an_empty_array"=>[],
        "an_array_with_more_than_one_elements"=>[1, 2],
        "a_symbol"=>:symbol,
        "a_string"=>"string"
      }
    }
    

    根据您在 cmets 中的问题,我想逻辑会有所改变:

    class Hash
      def deep_transform_values
        self.transform_values do |val|
          next(val.first) if val.is_a?(Array) && val.length == 1
          next(val) unless val.respond_to?(:deep_transform_values)
    
          val.deep_transform_values
        end
      end
    end
    

    【讨论】:

    • 这适用于我的用例。但是,如果我想对 Hash 类进行猴子补丁,我该如何修改它以使用递归,以便我可以在 Hash 的实例上调用它?
    • 我已经根据您的情况更新了答案@lacostenycoder。
    • 这很好用。我之前没有使用 next 和 argss。你能指出我的文档吗?
    • This 是 Ruby 文档目前所拥有的。
    • 不需要递归调用deep_transform_values,因为无论如何都会访问所有值。该实现是通过 Active-support 处理的,因此您需要 Rails(或参考 gem)。
    猜你喜欢
    • 2016-10-31
    • 1970-01-01
    • 2017-12-22
    • 2020-12-13
    • 2011-04-19
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多