Reputation: 901
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}]
Upvotes: 0
Views: 1794
Reputation: 110665
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
.
Upvotes: 0
Reputation: 27845
Another approach: Your string looks like a YAML or JSON -definition:
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:
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}]
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}]
Upvotes: 0
Reputation: 64363
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
Upvotes: 1
Reputation: 46960
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.
Upvotes: 4
Reputation: 1214
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.
Upvotes: 0