how to convert special string data to hash in ruby? - ruby

Hi i have some data that system send to me alternative like this:
"Screw:1,Bound:5,Hing:3"
"Bound:5,Screw:3,Hing:1"
"Bound:2,Screw:2"
how can i make this Hash?
{"Screw"=>6 ,"Bound"=>12, "Hing"=>4}
its probably add other key and value later , i hop solve this for me.

arr = [
"Screw:1,Bound:5,Hing:3",
"Bound:5,Screw:3,Hing:1",
"Bound:2,Screw:2"
]
arr.flat_map { |s| s.split(',') }
.each_with_object(Hash.new(0)) do |s,h|
k, v = s.split(':')
h[k] += v.to_i
end
#=> {"Screw"=>6, "Bound"=>12, "Hing"=>4}
Step 1
arr.flat_map { |s| s.split(',') }
#=>["Screw:1", "Bound:5", "Hing:3", "Bound:5", "Screw:3", "Hing:1",
# "Bound:2", "Screw:2"]
See the form of Hash::new that takes an argument and no block. The argument is called the default value, which is here zero. If h has been defined h = Hash.new(0), and h does not have a key k, h[k] returns the default value (and does not modify the hash). h[k] += v.to_i expands to
h[k] = h[k] + v.to_i
so if h does not have a key k this becomes
h[k] = 0 + v.to_i
Alternatively, one could write the following.
arr.flat_map { |s| s.split(/:|,/) }
.each_slice(2)
.with_object(Hash.new(0)) { |(k,v),h| h[k] += v.to_i }
#=> {"Screw"=>6, "Bound"=>12, "Hing"=>4}
Steps 1 and 2
a = arr.flat_map { |s| s.split(/:|,/) }
#=> ["Screw", "1", "Bound", "5", "Hing", "3", "Bound", "5",
# "Screw", "3", "Hing", "1", "Bound", "2", "Screw", "2"]
e = a.each_slice(2)
#=> #<Enumerator: ["Screw", "1", "Bound", "5", "Hing", "3",
# "Bound", "5", "Screw", "3", "Hing", "1",
# "Bound", "2", "Screw", "2"]:each_slice(2)>
The elements generated by the enumerator e can be seen as follows:
e.entries
#=> [["Screw", "1"], ["Bound", "5"], ["Hing", "3"], ["Bound", "5"],
# ["Screw", "3"], ["Hing", "1"], ["Bound", "2"], ["Screw", "2"]]

A good way would be too loop through all of the entries and update the hash depending on the entries that get found.
The following will do it for you.
str = "Screw:1,Bound:5,Hing:3"
output = Hash.new(0)
str.split(",").each do |entry|
key = entry.split(":")
output[key[0]] += key[1].to_i
end
Just modify it so that it handles multiple strings correctly, depending on how they are fed to you in the system.

Looks like the data is CSV so I'd opt to use a CSV parser to avoid possible encoding issues
require 'csv'
def parse input
Hash[CSV.parse_line(input).map { |pair| pair.split(":") }]
end

Related

Ruby hash with multiple comma separated values to array of hashes with same keys

What is the most efficient and pretty way to map this:
{name:"cheese,test", uid:"1,2"}
to this:
[ {name:"cheese", uid:"1"}, {name:"test", uid:"2"} ]
should work dinamically for example with: { name:"cheese,test,third", uid:"1,2,3" } or {name:"cheese,test,third,fourth", uid:"1,2,3,4", age:"9,8,7,6" }
Finally I made this:
hash = {name:"cheese,test", uid:"1,2"}
results = []
length = hash.values.first.split(',').length
length.times do |i|
results << hash.map {|k,v| [k, v.split(',')[i]]}
end
results.map{|e| e.to_h}
It is working, but i am not pleased with it, has to be a cleaner and more 'rubyst' way to do this
def splithash(h)
# Transform each element in the Hash...
h.map do |k, v|
# ...by splitting the values on commas...
v.split(',').map do |vv|
# ...and turning these into individual { k => v } entries.
{ k => vv }
end
end.inject do |a,b|
# Then combine these by "zip" combining each list A to each list B...
a.zip(b)
# ...which will require a subsequent .flatten to eliminate nesting
# [ [ 1, 2 ], 3 ] -> [ 1, 2, 3 ]
end.map(&:flatten).map do |s|
# Then combine all of these { k => v } hashes into one containing
# all the keys with associated values.
s.inject(&:merge)
end
end
Which can be used like this:
splithash(name:"cheese,test", uid:"1,2", example:"a,b")
# => [{:name=>"cheese", :uid=>"1", :example=>"a"}, {:name=>"test", :uid=>"2", :example=>"b"}]
It looks a lot more convoluted at first glance, but this handles any number of keys.
I would likely use transpose and zip like so:
hash = {name:"cheese,test,third,fourth", uid:"1,2,3,4", age:"9,8,7,6" }
hash.values.map{|x| x.split(",")}.transpose.map{|v| hash.keys.zip(v).to_h}
#=> [{:name=>"cheese", :uid=>"1", :age=>"9"}, {:name=>"test", :uid=>"2", :age=>"8"}, {:name=>"third", :uid=>"3", :age=>"7"}, {:name=>"fourth", :uid=>"4", :age=>"6"}]
To break it down a bit (code slightly modified for operational clarity):
hash.values
#=> ["cheese,test,third,fourth", "1,2,3,4", "9,8,7,6"]
.map{|x| x.split(",")}
#=> [["cheese", "test", "third", "fourth"], ["1", "2", "3", "4"], ["9", "8", "7", "6"]]
.transpose
#=> [["cheese", "1", "9"], ["test", "2", "8"], ["third", "3", "7"], ["fourth", "4", "6"]]
.map do |v|
hash.keys #=> [[:name, :uid, :age], [:name, :uid, :age], [:name, :uid, :age], [:name, :uid, :age]]
.zip(v) #=> [[[:name, "cheese"], [:uid, "1"], [:age, "9"]], [[:name, "test"], [:uid, "2"], [:age, "8"]], [[:name, "third"], [:uid, "3"], [:age, "7"]], [[:name, "fourth"], [:uid, "4"], [:age, "6"]]]
.to_h #=> [{:name=>"cheese", :uid=>"1", :age=>"9"}, {:name=>"test", :uid=>"2", :age=>"8"}, {:name=>"third", :uid=>"3", :age=>"7"}, {:name=>"fourth", :uid=>"4", :age=>"6"}]
end
Input
hash={name:"cheese,test,third,fourth", uid:"1,2,3,4", age:"9,8,7,6" }
Code
p hash
.transform_values { |v| v.split(',') }
.map { |k, v_arr| v_arr.map { |v| [k, v] }
}
.transpose
.map { |array| array.to_h }
Output
[{:name=>"cheese", :uid=>"1", :age=>"9"}, {:name=>"test", :uid=>"2", :age=>"8"}, {:name=>"third", :uid=>"3", :age=>"7"}, {:name=>"fourth", :uid=>"4", :age=>"6"}]
We are given
h = { name: "cheese,test", uid: "1,2" }
Here are two ways to create the desired array. Neither construct arrays that are then converted to hashes.
#1
First compute
g = h.transform_values { |s| s.split(',') }
#=> {:name=>["cheese", "test"], :uid=>["1", "2"]}
then compute
g.first.last.size.times.map { |i| g.transform_values { |v| v[i] } }
#=> [{:name=>"cheese", :uid=>"1"}, {:name=>"test", :uid=>"2"}]
Note
a = g.first
#=> [:name, ["cheese", "test"]]
b = a.last
#=> ["cheese", "test"]
b.size
#=> 2
#2
This approach does not convert the values of the hash to arrays.
(h.first.last.count(',')+1).times.map do |i|
h.transform_values { |s| s[/(?:\w+,){#{i}}\K\w+/] }
end
#=> [{:name=>"cheese", :uid=>"1"}, {:name=>"test", :uid=>"2"}]
We have
a = h.first
#=> [:name, "cheese,test"]
s = a.last
#=> "cheese,test"
s.count(',')+1
#=> 2
We can express the regular expression in free-spacing mode to make it self-documenting.
/
(?: # begin a non-capture group
\w+, # match one or more word characters followed by a comma
) # end the non-capture group
{#{i}} # execute the preceding non-capture group i times
\K # discard all matches so far and reset the start of the match
\w+ # match one or more word characters
/x # invoke free-spacing regex definition mode

Multiple sub-hashes out of one hash

I have a hash:
hash = {"a_1_a" => "1", "a_1_b" => "2", "a_1_c" => "3", "a_2_a" => "3",
"a_2_b" => "4", "a_2_c" => "4"}
What's the best way to get the following sub-hashes:
[{"a_1_a" => "1", "a_1_b" => "2", "a_1_c" => "3"},
{"a_2_a" => "3", "a_2_b" => "4", "a_2_c" => "4"}]
I want them grouped by the key, based on the regexp /^a_(\d+)/. I'll have 50+ key/value pairs in the original hash, so something dynamic would work best, if anyone has any suggestions.
If you're only concerned about the middle component you can use group_by to get you most of the way there:
hash.group_by do |k,v|
k.split('_')[1]
end.values.map do |list|
Hash[list]
end
# => [{"a_1_a"=>"1", "a_1_b"=>"2", "a_1_c"=>"3"}, {"a_2_a"=>"3", "a_2_b"=>"4", "a_2_c"=>"4"}]
The final step is extracting the grouped lists and combining those back into the required hashes.
Code
def partition_hash(hash)
hash.each_with_object({}) do |(k,v), h|
key = k[/(?<=_).+(?=_)/]
h[key] = (h[key] || {}).merge(k=>v)
end.values
end
Example
hash = {"a_1_a"=>"1", "a_1_b"=>"2", "a_1_c"=>"3", "a_2_a"=>"3", "a_2_b"=>"4", "a_2_c"=>"4"}
partition_hash(hash)
#=> [{"a_1_a"=>"1", "a_1_b"=>"2", "a_1_c"=>"3"},
# {"a_2_a"=>"3", "a_2_b"=>"4", "a_2_c"=>"4"}]
Explanation
The steps are as follows.
enum = hash.each_with_object({})
#=> #<Enumerator: {"a_1_a"=>"1", "a_1_b"=>"2", "a_1_c"=>"3", "a_2_a"=>"3",
# "a_2_b"=>"4", "a_2_c"=>"4"}:each_with_object({})>
The first element of this enumerator is generated and passed to the block, and the block variables are computed using parallel assignment.
(k,v), h = enum.next
#=> [["a_1_a", "1"], {}]
k #=> "a_1_a"
v #=> "1"
h #=> {}
and the block calculation is performed.
key = k[/(?<=_).+(?=_)/]
#=> "1"
h[key] = (h[key] || {}).merge(k=>v)
#=> h["1"] = (h["1"] || {}).merge("a_1_a"=>"1")
#=> h["1"] = (nil || {}).merge("a_1_a"=>"1")
#=> h["1"] = {}.merge("a_1_a"=>"1")
#=> h["1"] = {"a_1_a"=>"1"}
so now
h #=> {"1"=>{"a_1_a"=>"1"}}
The next value of enum is now generated and passed to the block, and the following calculations are performed.
(k,v), h = enum.next
#=> [["a_1_b", "2"], {"1"=>{"a_1_a"=>"1"}}]
k #=> "a_1_b"
v #=> "2"
h #=> {"1"=>{"a_1_a"=>"1"}}
key = k[/(?<=_).+(?=_)/]
#=> "1"
h[key] = (h[key] || {}).merge(k=>v)
#=> h["1"] = (h["1"] || {}).merge("a_1_b"=>"2")
#=> h["1"] = ({"a_1_a"=>"1"}} || {}).merge("a_1_b"=>"2")
#=> h["1"] = {"a_1_a"=>"1"}}.merge("a_1_b"=>"2")
#=> h["1"] = {"a_1_a"=>"1", "a_1_b"=>"2"}
After the remaining four elements of enum have been passed to the block the following has is returned.
h #=> {"1"=>{"a_1_a"=>"1", "a_1_b"=>"2", "a_1_c"=>"3"},
# "2"=>{"a_2_a"=>"3", "a_2_b"=>"4", "a_2_c"=>"4"}}
The final step is simply to extract the values.
h.values
#=> [{"a_1_a"=>"1", "a_1_b"=>"2", "a_1_c"=>"3"},
# {"a_2_a"=>"3", "a_2_b"=>"4", "a_2_c"=>"4"}]

Ruby using regex in select block

I've been having a lot of trouble sifting out regex matches. I could use scan, but since it only operates over a string, and I dont want to use a join on the array in question, it is much more tedious. I want to be able to do something like this:
array = ["a1d", "6dh","th3"].select{|x| x =~ /\d/}
# => ["1", "6", "3"}
However this never seems to work. Is there a work around or do I just need to use scan?
Try: Array#map
> array = ["a1d", "6dh","th3"].map {|x| x[/\d+/]}
#=> ["1", "6", "3"]
Note:
select
Returns a new array containing all elements of ary for which the given
block returns a true value.
In your case each element contains digit and it returns true, so you are getting original element via select. while map will perform action on each element and return new array with performed action on each element.
You can use grep with a block:
array = ["a1d", "6dh", "th3"]
array.grep(/(\d)/) { $1 }
#=> ["1", "6", "3"]
It passes each matching element to the block and returns an array containing the block's results.
$1 is a special global variable containing the first capture group.
Unlike map, only matching elements are returned:
array = ["a1d", "foo", "6dh", "bar", "th3"]
array.grep(/(\d)/) { $1 }
#=> ["1", "6", "3"]
array.map { |s| s[/\d/] }
#=> ["1", nil, "6", nil, "3"]
Depending on your requirements, you may wish to construct a hash.
arr = ["a1d", "6dh", "th3", "abc", "3for", "rg6", "def"]
arr.each_with_object(Hash.new { |h,k| h[k] = [] }) { |str,h| h[str[/\d+/]] << str }
#=> {"1"=>["a1d"], "6"=>["6dh", "rg6"], "3"=>["th3", "3for"], nil=>["abc", "def"]}
Hash.new { |h,k| h[k] = [] } creates an empty hash with a default block, represented by the block variable h. That means that if the hash does not have a key k, the block is executed, adding the key value pair k=>[] to the hash, after which h[k] << k is executed.
The above is a condensed (and Ruby-like) way of writing the following.
h = {}
arr.each do |str|
s = str[/\d+/]
h[s] = [] unless h.key?(s)
h[s] << str
end
h
# => {"1"=>["a1d"], "6"=>["6dh", "rg6"], "3"=>["th3", "3for"], nil=>["abc", "def"]}
The expression in the third line could alternatively be written
arr.each_with_object({}) { |str,h| (h[str[/\d+/]] ||= []) << str }
h[str[/\d+/]] ||= [] sets h[str[/\d+/]] to an empty array if the hash h does not have a key str[/\d+/].
See Enumerable#each_with_object and Hash::new.
#Stefan suggests
arr.group_by { |str| str[/\d+/] }
#=> {"1"=>["a1d"], "6"=>["6dh", "rg6"], "3"=>["th3", "3for"], nil=>["abc", "def"]}
What can I say?

Convert range to pattern

I have several ranges of numbers and I wonder if there are any algorithms to convert these ranges to patterns, like this:
Range: 5710000-5716999
Patterns: 5710*, 5711*, 5712*, 5713*, 5714*, 5715*, 5716*
Range: 5003070-5003089
Patterns: 500307*, 500308*
Range: 7238908-7238909
Patterns: 7238908*, 7238909*
I'm using Ruby, if it matters.
UPDATE 1:
More examples:
Range: 1668659-1668671
Patterns: 1668659*, 166866*, 1668670*, 1668671*
Range: 9505334305-9505334472
Patterns: 9505334305*, 9505334306*, 9505334307*, 9505334308*, 9505334309*, 950533431*, 950533432*, ..., 950533446*, 9505334470*, 9505334471*, 9505334472*
def doit(range)
b, e = range.begin.to_s, range.end.to_s
idx = b.chars.zip(e.chars).index { |a,b| a!=b }
return "#{b}*" if idx.nil?
(b[idx]..e[idx]).map { |c| b[0,idx] + c + '*' }
end
doit(5710000..5716999)
#=> ["5710*", "5711*", "5712*", "5713*", "5714*", "5715*", "5716*"]
doit(5003070..5003089)
#=> ["500307*", "500308*"]
doit(7238908..7238909)
#=> ["7238908*", "7238909*"]
doit(123..123)
#=> "123*"
The steps are as follows.
range = 5003070..5003089
b, e = range.begin.to_s, range.end.to_s
#=> ["5003070", "5003089"]
b #=> "5003070"
e #=> "5003089"
ab = b.chars
#=> ["5", "0", "0", "3", "0", "7", "0"]
ae = e.chars
#=> ["5", "0", "0", "3", "0", "8", "9"]
c = ab.zip(ae)
#=> [["5", "5"], ["0", "0"], ["0", "0"], ["3", "3"],
# ["0", "0"], ["7", "8"], ["0", "9"]]
idx = c.index { |a,b| a!=b }
#=> 5
return "#{b}*" if idx.nil?
#=> return "5003070*" if 5.nil?
r = b[idx]..e[idx]
#=> "7".."8"
r.map { |c| b[0,idx] + c + '*' }
#=> ["500307*", "500308*"]
It seems, I figured out how to make such converting using group_by method on the full range. I suppose, this algorithm kinda slow and inefficient, but it's very straightforward and works fine, so I'll stick to it.
def range_to_pattern(range)
values = range.to_a
while values.group_by{ |x| x.to_s[0...-1] }.any? { |_, v| v.size == 10 }
patterns = []
values.group_by{ |x| x.to_s[0...-1] }.each_pair{ |k, v| v.size == 10 ? patterns << k.to_i : patterns += v }
values = patterns
end
values
end
Results:
irb(main):072:0> range_to_pattern(5710000..5716999)
=> [5710, 5711, 5712, 5713, 5714, 5715, 5716]
irb(main):073:0> range_to_pattern(5003070..5003089)
=> [500307, 500308]
irb(main):074:0> range_to_pattern(7238908..7238909)
=> [7238908, 7238909]
irb(main):075:0> range_to_pattern(1668659..1668671)
=> [1668659, 166866, 1668670, 1668671]
irb(main):076:0> range_to_pattern(9505334305..9505334472)
=> [9505334305, 9505334306, 9505334307, 9505334308, 9505334309, 950533431, 950533432, 950533433, 950533434, 950533435, 950533436, 950533437, 950533438, 950533439, 950533440, 950533441, 950533442, 950533443, 950533444, 950533445, 950533446, 9505334470, 9505334471, 9505334472]

Join common keys in hash one-liner

I have this array of pairs:
[{"a"=>"1"}, {"b"=>"2"}, {"a"=>"3"}, {"b"=>"4"}, {"a"=>"5"}]
I would like a method to merge the keys in common with multiple values to:
[{"a"=>["1","3","5"]}, {"b"=>["2","4"]}]
Improved following Marc-Andre's suggestion.
array = [{"a"=>"1"}, {"b"=>"2"}, {"a"=>"3"}, {"b"=>"4"}, {"a"=>"5"}]
array.group_by(&:keys).map{|k, v| {k.first => v.flat_map(&:values)}}
Or
array.group_by{|h| h.keys.first}.each_value{|a| a.map!{|h| h.values.first}}
Haven't tried it yet, but something like that should also works
a.each_with_object( Hash.new{ |h,k| h[k] = [] } ) do |x, hash|
hash[x.keys.first] << x.values.first
end
edit: For a one liner, and the same output :
[a.each_with_object( Hash.new{ |h,k| h[k] = [] } ) { |x, hash| hash[x.keys.first] << x.values.first }]
A solution to your problem:
array.map(&:first).group_by(&:first).map{|k, v| {k => v.map(&:last)}}
I'm curious as to why you start and end with hashes containing only one key-pair. Arrays would be better suited. E.g.:
other = [["a", "1"], ["b", "2"], ["a", "3"], ["b", "4"], ["a", "5"]]
r = other.group_by(&:first).map{|k, v| [k => v.map(&:last)]}
r # => [["a", ["1", "3", "5"]], ["b", ["2", "4"]]]
Hash[r] # => {"a"=>["1", "3", "5"], "b"=>["2", "4"]}
array = [{"a"=>"1"}, {"b"=>"2"}, {"a"=>"3"}, {"b"=>"4"}, {"a"=>"5"}]
{}.tap{ |r| array.each{ |h| h.each{ |k,v| (r[k]||=[]) << v } } }
Not sure if you're giving out awards for brevity, but I like this way. The merge function with a block is ideally suited for this:
new = {}
array.each {|p| new.merge!(p) {|k,l,r| [l,r].flatten }}

Resources