如何优化从 ruby 中的嵌套哈希中提取数据?



Background

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

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] to, 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 是面向对象的语言。 引入简单的 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)迭代器中的每个步骤上具有哪种输出(数据(的最简单方法是放置调试器 比如:撬,再见虫。

假设你有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哈希多次推送到数组中

在这一点上,你可以用一些链式的方法把它重构成一种更函数式的编程风格,但我会把这个练习留给你。

最新更新