0

In Ruby or Rails What's the cleanest way to turn this string:

"[{one:1, two:2, three:3, four:4},{five:5, six:6}]"

into an array of hashes like this:

[{one:1, two:2, three:3, four:4},{five:5, six:6}]

Alekx
  • 891
  • 1
  • 11
  • 19
  • 1
    I don't know what cleanest means, but `eval` may be the shortest. – ymonad Jul 02 '14 at 00:35
  • True, this is the shortest and if this data wasn't being passed in by the user I would consider `eval`, but we can't trust the source of this data. – Alekx Jul 02 '14 at 00:40
  • 1
    If you cannot trust the data, and you don't want to evaluate them because of that, then you cannot turn them into objects. It is impossible. – sawa Jul 02 '14 at 00:47
  • I don't want to `eval` the code, but I'd like to still have an array of hashes. – Alekx Jul 02 '14 at 00:52
  • I posted an answer that was incorrect, as @sawa pointed out. It was a relatively easy fix, but I've given up for the time being. All I have access to at the moment is a Windows computer. I've not used windows for 20 years. It's just too frustrating for me to continue at the moment... – Cary Swoveland Jul 02 '14 at 03:13
  • Also, once you get used to HHKB, you cannot do without it. – sawa Jul 02 '14 at 03:36

5 Answers5

4

Here is a one-liner on two lines:

s.split(/}\s*,\s*{/).
  map{|s| Hash[s.scan(/(\w+):(\d+)/).map{|t| proc{|k,v| [k.to_sym, v.to_i]}.call(*t)}]}

NB I was using split(":") to separate keys from values, but @Cary Swoveland's use of parens in the regex is cooler. He forgot the key and value conversions, however.

Or a bit shorter, but uses array indexing instead of the proc, which some may find unappealing:

s.split(/}\s*,\s*{/).
  map{|s| Hash[s.scan(/(\w+):(\d+)/).map{|t| [t[0].to_sym, t[1].to_i]}]}

Result:

=> [{:one=>1, :two=>2, :three=>3, :four=>4}, {:five=>5, :six=>6}]

Explanation: Start from the end. The last map processes a list of strings of the form "key: value" and returns a list of [:key, value] pairs. The scan processes one string of comma-separated key-value pairs into a list of "key: value" strings. Finally, the initial split separates the brace-enclosed comma-separated strings.

Gene
  • 46,253
  • 4
  • 58
  • 96
  • This will give a quite different thing from what the string represented unless the hash keys are symbols and the values are integers (which happens to be the case with the OP's examples, but clearly not always the case, as can be told from the OP's mentioning that the data cannot be trusted). – sawa Jul 02 '14 at 02:32
  • 1
    @sawa He's trying to avoid using `eval` because he can't afford to execute arbitrary code. This will either return a hash or generate an exception, which meets the spec he implied. If OP want's rigorous syntax checking, he should be using a real parser, not regexes. – Gene Jul 02 '14 at 02:55
  • Damn! I was going to the one liner and I could not make it. Very nice solution! – Afonso Tsukamoto Jul 02 '14 at 03:14
  • @AfonsoTsukamoto Ruby libraries and Rubik's cube have in common that tricks make solutions shorter. But in the end it probably doesn't mean much. Yours will be simpler to understand for most people. – Gene Jul 02 '14 at 14:31
  • @Gene That is quite true, but still, if you can understand mine, you know regex, among other thing, and if you know regex, then you get to see why yours is so much better! – Afonso Tsukamoto Jul 02 '14 at 18:32
  • @CarySwoveland On my machine the code is far from the right margin. There is no scrollbar. – Gene Jul 09 '14 at 09:35
1

Try this:

"[{one:1, two:2, three:3, four:4},{five:5, six:6}]".
  split(/\}[ ]*,[ ]*\{/).
  map do |h_str| 
    Hash[
      h_str.split(",").map do |kv| 
        kv.strip.
          gsub(/[\[\]\{\}]/, '').
          split(":")
      end.map do |k, v|
        [k.to_sym, v.to_i]
      end
    ]
  end
Harish Shetty
  • 64,083
  • 21
  • 152
  • 198
0

Not pretty, not optimized, but solves it. (It was fun to do, though :) )

a = "[{one:1, two:2, three:3, four:4},{five:5, six:6}]"
array = []
a.gsub(/\[|\]/, '').split(/{|}/).map{ |h| h if h.length > 0 && h != ','}.compact.each do |v|
  hsh = {}
  v.split(',').each do |kv|
    arr = kv.split(':')
    hsh.merge!({arr.first.split.join.to_sym => arr.last.to_i})
  end
  array << hsh
end

If you want me to explain it, just ask.

Afonso Tsukamoto
  • 1,184
  • 1
  • 12
  • 21
  • This will give a quite different thing from what the string represented unless the hash keys are symbols and the values are integers (which happens to be the case with the OP's examples, but clearly not always the case, as can be told from the OP's mentioning that the data cannot be trusted). – sawa Jul 02 '14 at 02:33
  • I know, you are correct, but this can be easily changed for a more general purpose if the array always contains hashes. And if not, that will probably be a bad input case that can be checked by this code too. – Afonso Tsukamoto Jul 02 '14 at 03:11
0

You could do as below.

Edit: I originally prepared this answer in haste, while on the road, on a borrowed computer with an unfamiliar operating system (Windows). After @sawa pointed out mistakes, I set about fixing it, but became so frustrated with the mechanics of doing so that I gave up and deleted my answer. Now that I'm home again, I have made what I believe are the necessary corrections.

Code

def extract_hashes(str)
  str.scan(/\[?{(.+?)\}\]?/)
     .map { |arr| Hash[arr.first
                          .scan(/\s*([a-z]+)\s*:\d*(\d+)/)
                          .map { |f,s| [f.to_sym, s.to_i] }
                      ]
          }
end

Example

str = "[{one:1, two:2, three:3, four:4},{five:5, six:6}]"
extract_hashes(str)
  #=> [{:one=>1, :two=>2, :three=>3, :four=>4}, {:five=>5, :six=>6}]

Explanation

For str in the example above,

a = str.scan(/\[?{(.+?)\}\]?/)
  #=> [["one:1, two:2, three:3, four:4"], ["five:5, six:6"]]

Enumerable#map first passes the first element of a into the block and assigns it to the block variable:

arr #=> ["one:1, two:2, three:3, four:4"]

Then

b = arr.first
  #=> "one:1, two:2, three:3, four:4"
c = b.scan(/\s*([a-z]+)\s*:\d*(\d+)/)
  #=> [["one", "1"], ["two", "2"], ["three", "3"], ["four", "4"]]
d = c.map { |f,s| [f.to_sym, s.to_i] }
  #=> [[:one, 1], [:two, 2], [:three, 3], [:four, 4]]
e = Hash[d]
  #=> {:one=>1, :two=>2, :three=>3, :four=>4}

In Ruby 2.0, Hash[d] can be replaced with d.to_h.

Thus, the first element of a is mapped to e.

Next, the outer map passes the second and last element of a into the block

arr #=> ["five:5, six:6"]

and we obtain:

Hash[arr.first
        .scan(/\s*([a-z]+)\s*:\d*(\d+)/)
        .map { |f,s| [f.to_sym, s.to_i] }
    ]
  #=> {:five=>5, :six=>6}

which replaces a.last.

Cary Swoveland
  • 106,649
  • 6
  • 63
  • 100
0

Another approach: Your string looks like a YAML or JSON -definition:

YAML

A slightly modified string works:

require 'yaml'
p YAML.load("[ { one: 1, two: 2, three: 3, four: 4}, { five: 5, six: 6 } ]")
#-> [{"one"=>1, "two"=>2, "three"=>3, "four"=>4}, {"five"=>5, "six"=>6}]

There are two problems:

  1. The keys are strings, no symbols
  2. You need some more spaces (one:1 is not recognized, you need a one: 1).

For problem 1 you need a gsub(/:/, ': ') (I hope there are no other : in your string)

For problem 2 was already a question: Hash[a.map{|(k,v)| [k.to_sym,v]}]

Full example:

require 'yaml'
input = "[{one:1, two:2, three:3, four:4},{five:5, six:6}]"
input.gsub!(/:/, ': ')  #force correct YAML-syntax
p YAML.load(input).map{|a| Hash[a.map{|(k,v)| [k.to_sym,v]}]}
#-> [{:one=>1, :two=>2, :three=>3, :four=>4}, {:five=>5, :six=>6}]

JSON

With json you need additonal ", but the symbolization is easier:

require 'json'
input = '[ { "one":1, "two": 2, "three": 3, "four": 4},{ "five": 5, "six": 6} ]'   

p JSON.parse(input)
#-> [{"one"=>1, "two"=>2, "three"=>3, "four"=>4}, {"five"=>5, "six"=>6}]
p JSON.parse(input, :symbolize_names => true)
#-> [{:one=>1, :two=>2, :three=>3, :four=>4}, {:five=>5, :six=>6}]

Example with original string:

require 'json'

input = "[{one: 1, two: 2, three:3, four:4},{five:5, six:6}]"
input.gsub!(/([[:alpha:]]+):/, '"\1":')    
p JSON.parse(input)
#-> [{"one"=>1, "two"=>2, "three"=>3, "four"=>4}, {"five"=>5, "six"=>6}]
p JSON.parse(input, :symbolize_names => true)
#-> [{:one=>1, :two=>2, :three=>3, :four=>4}, {:five=>5, :six=>6}]
Community
  • 1
  • 1
knut
  • 27,320
  • 6
  • 84
  • 112