I have the following complicated hash structure (among many) that looks like the following:
hash = {"en-us"=>
{:learn_more=>"Learn more",
:non_apple_desktop=>"To redeem, open this link.",
:value_prop_safety=>"",
:storage_size=>
{:apple_tv_1_month_tms=>
{:cta=>"Offer",
:details=>"Get a 1-month subscription!.",
:disclaimer=>"This offer expires on December 10, 2021.",
:header=>"Watch The Morning Show ",
:normal_price=>"$2.99"}
}
}
}
What I'd like to do is to have a function that will produce the following string output based off the hash structure:
en-us.storage_size.apple_tv_1_month_tms.cta: Offer
en-us.storage_size.apple_tv_1_month_tms.details: Get a 1-month subscription!.
en-us.storage_size.apple_tv_1_month_tms.disclaimer: This offer expires on December 10, 2021.
en-us.storage_size.apple_tv_1_month_tms.header: Watch The Morning Show
en-us.storage_size.apple_tv_1_month_tms.normal_price: $2.99
en-us.learn_more: Learn more
en-us.non_apple_desktop: To redeem, open this link.
en-us.value_prop_safety:
I've used this recursive function from another stackoverflow question that somewhat accomplishes this:
def show(hash, current_path = '')
string = ''
hash.each do |k,v|
if v.respond_to?(:each)
current_path = "#{k}."
show v, current_path
else
string = "#{current_path}#{k}: #{v}" "\n"
end
end
string
end
If I place a puts
statement in the body of the method I can see the desired result but its line by line. What I need is to obtain the entirety of the output because I will be writing it to a csv. I can't seem to get it to work in its current incarnation.
If I were to place a puts show(hash)
into my irb, then I won't get any output. So in summary, I am trying to do the following:
show(hash) ----->
en-us.storage_size.apple_tv_1_month_tms.cta: Offer
en-us.storage_size.apple_tv_1_month_tms.details: Get a 1-month subscription!.
en-us.storage_size.apple_tv_1_month_tms.disclaimer: This offer expires on December 10, 2021.
en-us.storage_size.apple_tv_1_month_tms.header: Watch The Morning Show
en-us.storage_size.apple_tv_1_month_tms.normal_price: $2.99
en-us.learn_more: Learn more
en-us.non_apple_desktop: To redeem, open this link.
en-us.value_prop_safety:
This should be an easy recursive task but I can't pinpoint what exactly I've got wrong. Help would be greatly appreciated. Thank you.
CodePudding user response:
In my opinion it is much more convenient to use i18n
gem
It has I18n::Backend::Flatten#flatten_translations
method. It receives a hash of translations (where the key is a locale and the value is another hash) and return a hash with all translations flattened, just as you need
Just convert the resulting hash to a string and you're done
require "i18n/backend/flatten"
include I18n::Backend::Flatten
locale_hash = {"en-us"=>
{:learn_more=>"Learn more",
:non_apple_desktop=>"To redeem, open this link.",
:value_prop_safety=>"",
:storage_size=>
{:apple_tv_1_month_tms=>
{:cta=>"Offer",
:details=>"Get a 1-month subscription!.",
:disclaimer=>"This offer expires on December 10, 2021.",
:header=>"Watch The Morning Show ",
:normal_price=>"$2.99"}
}
}
}
puts flatten_translations(nil, locale_hash, nil, nil).
map { |k, v| "#{k}: #{v}" }.
join("\n")
# will print
# en-us.learn_more: Learn more
# en-us.non_apple_desktop: To redeem, open this link.
# en-us.value_prop_safety:
# en-us.storage_size.apple_tv_1_month_tms.cta: Offer
# en-us.storage_size.apple_tv_1_month_tms.details: Get a 1-month subscription!.
# en-us.storage_size.apple_tv_1_month_tms.disclaimer: This offer expires on December 10, 2021.
# en-us.storage_size.apple_tv_1_month_tms.header: Watch The Morning Show
# en-us.storage_size.apple_tv_1_month_tms.normal_price: $2.99
Of course it's better to include not in main
object, but in some service object
require "i18n/backend/flatten"
class StringifyLocaleHash
include I18n::Backend::Flatten
attr_reader :locale_hash
def self.call(locale_hash)
new(locale_hash).call
end
def initialize(locale_hash)
@locale_hash = locale_hash
end
def call
flatten_translations(nil, locale_hash, nil, nil).
map { |k, v| "#{k}: #{v}" }.
join("\n")
end
end
# To get string call such way
StringifyLocaleHash.(locale_hash)
CodePudding user response:
Considerations on the Approach
First, some issues:
- Ruby is not JavaScript. Hashes don't have properties, so chained dots aren't going to work unless you do a lot of work to split and join the paths to your data after the fact. The juice is probably not worth the squeeze.
en-us
is not a valid LANG value. You can use it a a key, but you probably want something more technically accurate likeen_US
oren_US.UTF-8
as a key if the locale matters.
Secondly, if you already know the structure of your JSON, then you should just make each JSON key a column value and populate it. This is likely a lot easier.
Thirdly, while Ruby can do recursion, most of its built-in methods are designed for iteration. Iterating is faster and easier for a lot of things, so unless you're doing this for other reasons, just iterate over the information you need.
Building CSV from Structured Data with a Predefined Format
Finally, if you just want the data to populate a CSV or create strings, then maybe you just want to extract the data and then munge it with information you already know anyway. For example:
require "hashie"
# assume your hash is already defined
offer_values =
hash.extend(Hashie::Extensions::DeepFind)
.deep_find(:apple_tv_1_month_tms).values
#=>
# ["Offer",
# "Get a 1-month subscription!.",
# "This offer expires on December 10, 2021.",
# "Watch The Morning Show ",
# "$2.99"]
To get the values of the top-level keys, you can can do it like this:
linking = hash["en-us"].keys[0..-2].map { hash.dig "en-us", _1 }
#=> ["Learn more", "To redeem, open this link.", ""]
With more work, you could do similar things with ActiveSupport or by using Ruby 3.1 pattern matching with a find pattern, but this will work on older Ruby versions too. The only downside is that it relies on the Hashie gem for Hashie::DeepFind, but this is one of those cases where Ruby doesn't have a simple built-in method for finding nested structures other than pattern matching and I think it's worth it.
You'll still have to do some work to convert the extracted data into the exact format you want, but this gets you all the values you're after into a pair of variables, offer_values and linking. You can then do whatever you want with them.
See Also
In addition to Hashie::DeepFind and Ruby 3 pattern matching, you also have a number of query languages that might be useful in extracting data from JSON, some of which have Ruby gems or can be easily integrated in your Ruby application:
- JSONPath
- XPath 3.1
- XQuery 3.1