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

6
  • 1
    I don't know what cleanest means, but eval may be the shortest. Commented Jul 2, 2014 at 0: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. Commented Jul 2, 2014 at 0: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. Commented Jul 2, 2014 at 0:47
  • I don't want to eval the code, but I'd like to still have an array of hashes. Commented Jul 2, 2014 at 0: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... Commented Jul 2, 2014 at 3:13

5 Answers 5

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.

Sign up to request clarification or add additional context in comments.

6 Comments

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 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.
Damn! I was going to the one liner and I could not make it. Very nice solution!
@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 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!
|
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

Comments

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.

2 Comments

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).
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.
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}]

Comments

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.

1 Comment

This gives a different result from what the OP wants.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.