【问题标题】:How to optimize extracting data from nested hashes in ruby?如何优化从 ruby​​ 中的嵌套哈希中提取数据?
【发布时间】:2020-04-22 12:10:19
【问题描述】:

背景

我有一组嵌套哈希,它们提供一组参数来定义应用程序行为:

custom_demo_options: {
    verticals: {
        fashion: true,
        automotive: false,
        fsi: false
    },
    channels: {
        b2b: true,
        b2c: true
    }
}

website_data: {
    verticals: {
        fashion: {
            b2b: {
                code: 'luma_b2b',
                url: 'b2b.luma.com'
            },
            b2c: {
                code: 'base',
                url: 'luma.com'
            }
        } 
    }
}

custom_demo_options 散列中所做的选择与存储在website_data 散列中的数据相关,并用于从中返回值:

data = []
collection = {}
custom_demo_options[:verticlas].each do |vertical_name, vertical_choice|
    # Get each vertical selection
    if vertical_choice == true
        # Loop through the channels for each selected vertical
        custom_demo_options[:channels].each do |channel_name, channel_choice|
            # Get each channel selection for each vertical selection
            if channel_choice == true
                # Loop through the website data for each vertical/channel selection
                website_data[:verticals].each do |site_vertical, vertical_data|
                    # Look at the keys of the [:website_data][:verticals] hash
                    # If we have a vertical selection that matches a website_data vertical...
                    if site_vertical == vertical_name
                        # For each website_data vertical collection...
                        vertical_data.each do |vertical_channel, channel_value|
                            # If we have a matching channel in the collection...
                            if vertical_channel == channel_name 
                                # Add the channel's url and code to the collection hash
                                collection[:url] = channel_value[:url]
                                collection[:code] = channel_value[:code]
                                # Push the collection hash(es) onto the data array
                                data.push(collection)
                            }
                        }
                    }
                }
            }
        }
    }
}

推送到数据数组的数据最终用于创建如下的nginx映射定义:

map $http_host $MAGE_RUN_CODE {
    luma.com base;
    b2b.luma.com luma_b2b;
}

作为哈希之间关系的示例,如果用户设置custom_demo_options[:channels][:b2b] tofalse, the b2b code/url pair stored in thewebsite_data`哈希将从nginx块中删除:

map $http_host $MAGE_RUN_CODE {
    luma.com base;
}

问题

上面的代码有效,但我知道它的效率非常低。我对 ruby​​ 比较陌生,但我认为这很可能是一个逻辑挑战,而不是特定于语言的挑战。

我的问题是,连接这些哈希而不是像我一样使用循环的正确方法是什么?我在hash.select 上做了一些阅读,似乎这可能是最好的路线,但我想知道:我应该考虑其他方法来优化此操作吗?

更新

我已经能够实施第一个建议(再次感谢发帖者);但是,我认为第二种解决方案将是更好的方法。一切都按描述工作;但是,我的数据结构略有变化,虽然我了解解决方案的作用,但我无法相应地进行调整。这是新的结构:

custom_demo_options = {
    verticals: {
        fashion: true,
        automotive: false,
        fsi: false
    },
    channels: {
        b2b: true,
        b2c: true
    },
    geos: [
        'us_en'
    ]
}
website_data = {
    verticals: {
        fashion: {
            us_en: {
                b2b: {
                    code: 'luma_b2b',
                    url: 'b2b.luma.com'
                },
                b2c: {
                    code: 'base',
                    url: 'luma.com'
                }
            }
        } 
    }
}

所以,我在哈希中添加了另一个级别,:geo

我已经尝试适应第二种解决方案如下:

class CustomOptionsMap
    attr_accessor :custom_options, :website_data

    def initialize(custom_options, website_data)
        @custom_options = custom_options
        @website_data = website_data[:verticals]
    end

    def data
        verticals = selected_verticals
        channels = selected_channels
        geos = selected_geos

        # I know this is the piece I'm not understanding.  How to map channels and geos accordingly.
        verticals.map{ |vertical| @website_data.fetch(vertical).slice(*channels) }
    end

    private
    def selected_geos
        @custom_options[:geos].select{|_,v| v } # I think this is correct, as it extracts the geo from the array and we don't have additional keys
    end
    def selected_verticals
        @custom_options[:verticals].select{|_,v| v }.keys
    end
    def selected_channels
        @custom_options[:channels].select{|_,v| v }.keys
    end
end

demo_configuration = CustomOptionsMap.new(custom_demo_options, website_data)
print demo_configuration.data

非常感谢任何关于我在地图声明方面缺少的指导。

【问题讨论】:

  • 虽然这两个答案都有助于解决我提出的问题,但我觉得 OOP 方法更适合我的特定需求。从长远来看,它也更容易阅读和理解。

标签: ruby loops hash


【解决方案1】:

面向对象方法。

在这种情况下使用 OOP 可能更具可读性和一致性,因为 Ruby 是面向对象的语言。 引入简单的 Ruby 类并使用 activesupport 模块,该模块通过一些有用的方法扩展了 Hash,可以通过以下方式获得相同的结果:

class WebsiteConifg
  attr_accessor :custom_options, :website_data

  def initialize(custom_options, website_data)
    @custom_options = custom_options
    @website_data   = website_data[:verticals]
  end

  def data
    verticals = selected_verticals
    channels = selected_channels

    verticals.map{ |vertical| @website_data.fetch(vertical).slice(*channels) }
  end

  private
  def selected_verticals
    @custom_options[:verticals].select{|_,v| v }.keys
  end

  def selected_channels
    @custom_options[:channels].select{|_,v| v }.keys
  end

基于传递的custom_demo_options,我们可以只选择那些键的垂直和通道,其值设置为true

为你的配置会返回

selected_verticals #  [:fashion]
selected_channels  #  [:b2b, :b2c]

+data() 简单的公共接口根据传递的选项遍历所有选定的verticals,并使用slice(keys)返回给定通道的哈希数组。

fetch(key) 给定键的返回值相当于 h[:key]

h = {a: 2, b: 3}
h.fetch(:a)  # 2
h.fetch(:b)  # 3

slice(key1, key2) 确实需要activesupport

返回包含作为参数传递的哈希,键。方法接受多个参数,因为在我们的示例中,我们得到了这些键的数组,我们可以使用* splat 运算符来遵守此接口。

h = {a: 2, b: 3}
h.slice(:a)         # {:a=>2}
h.slice(:a, :b)     # {:a=>2, :b=>3}
h.slice(*[:a, :b])  # {:a=>2, :b=>3}

用法

  website_config = WebsiteConifg.new(custom_demo_options, website_data) 
  website_config.data   

  # returns
  # [{:b2b=>{:code=>"luma_b2b", :url=>"b2b.luma.com"}, :b2c=>{:code=>"base", :url=>"luma.com"}}]

更新

更改了相关部分:

def data
    verticals = selected_verticals
    channels = selected_channels
    geos = selected_geos

    verticals.map do |vertical|
      verticals_data = @website_data.fetch(vertical)

      # in case of multiple geolocations
      # collecting relevant entries of all of them
      geos_data = geos.map{|geo| verticals_data.fetch(geo) }

      # for each geo-location getting selected channels 
      geos_data.map {|geo_data| geo_data.slice(*channels)  }
    end.flatten
  end

  private 
  # as `website_data' hash is using symbols, we need to covert string->sym 
  def selected_geos
    @custom_options[:geos].map(&:to_sym)
  end

  def selected_verticals
    selected_for(:verticals).keys
  end

  def selected_channels
    selected_for(:channels).keys
  end

  def selected_for(key)
    @custom_options[key].select{|_,v| v }
  end


了解each(map) 迭代器中每个步骤的输出(数据)类型的最简单方法是将调试器放置在那里 像:撬,再见。

【讨论】:

  • 我还没有机会深入研究这个(因为它包含许多我(迄今为止)不熟悉的概念),但我只是想感谢您抽出时间来回答和这个建议。根据选择获取值的行为是我需要在多个地方做的事情,我开始认识到 OOP 方法的必要性和好处。当我有机会探索时,我会做出回应。再次感谢。
  • 再次感谢您抽出宝贵时间回复。在稍微修改了我的原始数据结构后,我对 fetch 和 select 做了一些阅读。您愿意根据我的尝试扩展您的答案吗?我认为这是一个很好的解决方案,而且几乎很清楚,我认为......
  • 当然,@SteveK,你可以看看更新版本。代码可以进一步简化为单行代码geos_data = geos.map{|lang| verticals_data.fetch(lang).slice(*channels) }。我留下了一个带有 cmets 的扩展版本,因为它应该更容易理解和调试。
  • 啊,我明白了!现在使用多个 map 语句挖掘数据树是有意义的!我的逻辑很好,只是我对这些操作还不够了解,转换为符号也很有意义。天哪,这有什么帮助。非常感谢。
【解决方案2】:

假设您有key = :foohash = { foo: 1, bar: 2 } - 您想知道该键的哈希值。

您在这里使用的方法本质上是

result = nil
hsh.each { |k,v| result = v if k == :foo }

但既然你可以简单地说,为什么要这样做

result = hsh[:foo]

您似乎了解哈希如何成为可迭代结构,并且您可以像数组一样遍历它们。但是你做得过火了,忘记了哈希是索引结构。就您的代码而言,我会像这样重构它:

# fixed typo here: verticlas => verticals
custom_demo_options[:verticals].each do |vertical_name, vertical_choice|
  # == true is almost always unnecessary, just use a truthiness check
  next unless vertical_choice
  custom_demo_options[:channels].each do |channel_name, channel_choice|
    next unless channel_choice
    vertical_data = website_data[:verticals][site_vertical]
    channel_value = vertical_data[channel_name]
    # This must be initialized here:
    collection = {}
    collection[:url] = channel_value[:url]
    collection[:code] = channel_value[:code]
    data.push(collection)
  end
end

您可以看到很多嵌套和复杂性都被移除了。请注意,我正在初始化collection,当时它添加了属性。这有点过多,但我强烈建议阅读 Ruby 中的可变性。您当前的代码可能不会按照您的预期执行,因为您将相同的collection 哈希多次推送到数组中

此时,您可以使用一些链式方法将其重构为更具功能性的编程风格,但我会将这个练习留给您

【讨论】:

  • 非常感谢您的解释和重构。我看到的唯一小问题是 website_data[:verticals][site_vertical] - 由于您删除了 each 语句之一,因此未设置 site_vertical。它用 vertical_name 代替。我一定会带您阅读有关 ruby​​ 中的可变性的内容。我的代码实际上来自厨师食谱和模板。我遇到了我认为是厨师特有的怪癖,这迫使我首先将数据数组声明为其余代码之外的空数组。无论如何,下一个除非方法对我来说是新的并且非常受欢迎!
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2021-12-22
  • 1970-01-01
  • 2018-04-24
  • 2014-01-20
  • 2017-04-17
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多