3

I have a hash which looks like this:

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

I need to convert each value which is a single string inside an array so that it ends up like this:

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

I found this but couldn't get it work https://gist.github.com/chris/b4138603a8fe17e073c6bc073eb17785

lacostenycoder
  • 10,623
  • 4
  • 31
  • 48

4 Answers4

6

What about something like:

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

Tested with something like:

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'
    }
  }

Gives:

{
  "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"
  }
}

Following your question in the comments, I guess the logic would change a bit:

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
Sebastián Palma
  • 32,692
  • 6
  • 40
  • 59
  • This works well for my use case. However, if I wanted to monkey-patch Hash class, how can I modify this to work with recursion so I can call it on an instance of Hash? – lacostenycoder Mar 11 '19 at 01:27
  • 1
    I've updated the answer with a variation for your case @lacostenycoder. – Sebastián Palma Mar 11 '19 at 01:34
  • this works great. I haven't used `next` with argss before. Can you point me to documentation for it? – lacostenycoder Mar 11 '19 at 01:41
  • [This](https://ruby-doc.org/core-2.6.1/Enumerator.html#method-i-next) is what the Ruby doc has so far. – Sebastián Palma Mar 11 '19 at 01:46
  • Calling `deep_transform_values` recursively is not needed, as all the values will be visited anyway. The implementation is handled via Active-support so you'll need Rails (or reference the gem). – Dennis Dec 24 '21 at 10:48
3
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"
  #    }
  #  } 
Cary Swoveland
  • 106,649
  • 6
  • 63
  • 100
1

As an alternative, consider using an object and allowing the initializer to deconstruct some of the keys for you.

One of the reasons a lot of people like myself started using Ruby in favour of Perl was because of the better expression of objects in place of primitives like arrays and hashes. Use it to your advantage!

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"]}

In the same vein, I'd even make an Info class for the shippingInfo data.

You may run into a different problem if key1 and key2 are dynamic, but there's ways around that too (double splat for one).

ian
  • 12,003
  • 9
  • 51
  • 107
  • This is probably a better pattern although a bit more rigid. The data structure here was a non-specific example, – lacostenycoder Mar 11 '19 at 03:32
  • @lacostenycoder Sure, just wanted to give an alternative, I'm not criticising. The "Just my 2 cents" possibly makes it come off as criticism, probably because people use that before they criticise people, right? I should find a replacement phrase… (o_º) – ian Mar 11 '19 at 03:39
1

I noticed a lot of answers with unnecessary recursion. Current version of Ruby 2.7.x with ActiveSupport (I tested with 6.1.4.4) will allow you to do this:

Input data:

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

Solution:

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

The example above will return a typecast String to Integer.

Dennis
  • 780
  • 6
  • 17
  • `Hash#deep_transform_values` requires `active_support` and you need to be on the correct version. Include those details to make this answer valid. – lacostenycoder Dec 29 '21 at 22:04
  • 1
    @lacostenycoder I mentioned ActiveSupport, and ruby version, will for completeness add the version of the gem as well. – Dennis Jan 17 '22 at 16:28
  • you need these two lines before you instantiate a new hash object. `require 'active_support' require 'active_support/core_ext'` – lacostenycoder Jan 19 '22 at 17:54