Home > OS >  How to produce a concatenated string from a hash recursively?
How to produce a concatenated string from a hash recursively?

Time:09-29

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:

  1. 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.
  2. en-us is not a valid LANG value. You can use it a a key, but you probably want something more technically accurate like en_US or en_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:

  • Related