Related
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
how from this:
[
["1", "className", "Connection"],
["2", "className", "Connection"],
["1", "connectorId", "100"],
["2", "connectorId", "100"],
["1", "part_1", "side_1"],
["2", "part_1", "side_1"],
["1", "part_2", "bottomPanel_1"],
["2", "part_2", "bottomPanel_1"],
["1", "transformation", nil],
["2", "transformation", nil]
]
resive something like this:
[
[["1", "className", "Connection"],
["1", "connectorId", "100"],
["1", "part_2", "bottomPanel_1"],
["1", "part_1", "side_1"],
["1", "transformation", nil]],
[["2", "part_1", "side_1"],
["2", "className", "Connection"],
["2", "connectorId", "100"],
["2", "part_2", "bottomPanel_1"],
["2", "transformation", nil]]
]
I need to group nested arrays by first element.
thanks in advance.
Give a your array, try this
a.group_by(&:first).values
=> [
[["1", "className", "Connection"],
["1", "connectorId", "100"],
["1", "part_1", "side_1"],
["1", "part_2", "bottomPanel_1"],
["1", "transformation", nil]],
[["2", "className", "Connection"],
["2", "connectorId", "100"],
["2", "part_1", "side_1"],
["2", "part_2", "bottomPanel_1"],
["2", "transformation", nil]]
]
Ruby's Enumerable module has all kinds of goodies built into it, and group_by does very close to what you want.
arr.group_by { |x| x[0] } # Assuming your input array is called arr
This results in a dictionary where the keys are the groupings (i.e. the first elements of the inner arrays) and the values are the lists of elements in that group. That is,
{
"1" => [
["1", "className", "Connection"],
["1", "connectorId", "100"],
["1", "part_1", "side_1"],
["1", "part_2", "bottomPanel_1"],
["1", "transformation", nil]
],
"2" => [
["2", "className", "Connection"],
["2", "connectorId", "100"],
["2", "part_1", "side_1"],
["2", "part_2", "bottomPanel_1"],
["2", "transformation", nil]
]
}
If all you want are the groupings, not the keys, then we can just do
arr.group_by { |x| x[0] }.values
which gives you exactly the result you want.
While the other answers are both correct, your end result still looks cumbersome to deal with. I would recommend taking this a step further so that the resulting object is easier to utilize
a =[
["1", "className", "Connection"],
["2", "className", "Connection"],
["1", "connectorId", "100"],
["2", "connectorId", "100"],
["1", "part_1", "side_1"],
["2", "part_1", "side_1"],
["1", "part_2", "bottomPanel_1"],
["2", "part_2", "bottomPanel_1"],
["1", "transformation", nil],
["2", "transformation", nil]
]
# shift will mutate the inner Arrays
# if this does not matter you can remove map(&:dup)
b = a.map(&:dup).group_by(&:shift).transform_values(&:to_h)
#=> {"1"=>{
# "className"=>"Connection",
# "connectorId"=>"100",
# "part_1"=>"side_1",
# "part_2"=>"bottomPanel_1",
# "transformation"=>nil},
# "2"=>{
# "className"=>"Connection",
# "connectorId"=>"100",
# "part_1"=>"side_1",
# "part_2"=>"bottomPanel_1",
# "transformation"=>nil}
# }
Now the resulting Object is a Hash which will make accessing the data much easier. e.g. to access 1's connectorId
Your Result: b[0][1][2] or b.dig(0,1,2)
Hash Result: b["1"]["connectorId"]
Turning the results into a Hash will also allow for order independence in the event that the order of a is not as determinate as it is in the post.
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]
I have a set of array like this:
[["1","2"],["1","3"],["2","3"],["2","5"]]
I want to find the union of first values like
["1","2"],["1","3"] matches so i need to create new array like ["1","2,3"]
so the resulting array will be like
[["1","2,3"],["2","3,5"]]
Like most problems in Ruby, the Enumerable module does the job:
input = [["1","2"],["1","3"],["2","3"],["2","5"]]
result = input.group_by do |item|
# Group by first element
item[0]
end.collect do |key, items|
# Compose into new format
[
key,
items.collect do |item|
item[1]
end.join(',')
]
end
puts result.inspect
# => [["1", "2,3"], ["2", "3,5"]]
The group_by method comes in very handy when aggregating things like this, and collect is great for rewriting how the elements appear.
What you are asking for is not a true union for a true union of each 2 it would be:
data = [["1","2"],["1","3"],["2","3"],["2","5"]]
data.each_slice(2).map{|a,b| a | b.to_a }
#=> [["1","2","3"],["2","3","5"]]
Here is a very simple solution that modifies this concept to fit your needs:
data = [["1","2"],["1","3"],["2","3"],["2","5"]]
data.each_slice(2).map do |a,b|
unified = (a | b.to_a)
[unified.shift,unified.join(',')]
end
#=> [["1", "2,3"], ["2", "3,5"]]
Added to_a to piped variable b in the event that there are an uneven number of arrays. eg.
data = [["1","2"],["1","3"],["2","3"],["2","5"],["4","7"]]
data.each_slice(2).map do |a,b|
unified = (a | b.to_a)
[unified.shift,unified.join(',')]
end
#=> [["1", "2,3"], ["2", "3,5"], ["4","7"]]
If you meant that you want this to happen regardless of order then this will work but will destroy the data object
data.group_by(&:shift).map{|k,v| [k,v.flatten.join(',')]}
#=> [["1", "2,3"], ["2", "3,5"], ["4","7"]]
Non destructively you could call
data.map(&:dup).group_by(&:shift).map{|k,v| [k,v.flatten.join(',')]}
#=> [["1", "2,3"], ["2", "3,5"], ["4","7"]]
Here's another way.
Code
def doit(arr)
arr.each_with_object({}) { |(i,*rest),h| (h[i] ||= []).concat(rest) }
.map { |i,rest| [i, rest.join(',')] }
end
Examples
arr1 = [["1","2"],["1","3"],["2","3"],["2","5"]]
doit(arr1)
#=> [["1", "2,3"], ["2", "3,5"]]
arr2 = [["1","2","6"],["2","7"],["1","3"],["2","3","9","4","cat"]]
doit(arr2)
# => [["1", "2,6,3"], ["2", "7,3,9,4,cat"]]
Explanation
For arr1 above, we obtain:
enum = arr1.each_with_object({})
#=> #<Enumerator: [["1", "2"], ["1", "3"], ["2", "3"],
# ["2", "5"]]:each_with_object({})>
We can convert enum to an array see its elements:
enum.to_a
#=> [[["1", "2"], {}], [["1", "3"], {}],
# [["2", "3"], {}], [["2", "5"], {}]]
These elements will be passed into the block, and assigned to the block variables, by Enumerator#each, which will invoke Array#each. The first of these elements ([["1", "2"], {}]) can be obtained by invoking Enumerator#next on enum:
(i,*rest),h = enum.next
#=> [["1", "2"], {}]
i #=> "1"
rest #=> ["2"]
h #=> {}
We then execute:
(h[i] ||= []).concat(rest)
#=> (h["1"] ||= []).concat(["2"])
#=> (nil ||= []).concat(["2"])
#=> [].concat(["2"])
#=> ["2"]
each then passes the next element of enum to the block:
(i,*rest),h = enum.next
#=> [["1", "3"], {"1"=>["2"]}]
i #=> "1"
rest #=> ["3"]
h #=> {"1"=>["2"]}
(h[i] ||= []).concat(rest)
#=> (h["1"] ||= []).concat(["3"])
#=> (["2"] ||= []).concat(["3"])
#=> ["2"].concat(["3"])
#=> ["2", "3"]
After passing the last two elements of enum into the block, we obtain:
h=> {"1"=>["2", "3"], "2"=>["3", "5"]}
map creates an enumerator:
enum_h = h.each
#=> > #<Enumerator: {"1"=>["2", "3"]}:each>
and calls Enumerator#each (which calls Hash#each) to pass each element of enum_h into the block:
i, rest = enum_h.next
#=> ["1", ["2", "3"]]
then computes:
[i, rest.join(',')]
#=> ["1", ["2", "3"].join(',')]
#=> ["1", "2,3"]
The other element of enum_h is processed similarly.
I've this familiar question that looks like permutation/combination of the Math world.
How can I achieve the following via ruby?
badges = "1-2-3"
badge_cascade = []
badges.split("-").each do |b|
badge_cascade << b
end
Gives: => ["1", "2", "3"]
But I want it to be is:
=> ["1", "2", "3",
"1-2", "2-3", "3-1", "2-1", "3-2", "1-3",
"1-2-3", "2-3-1", "3-1-2"]
Functional approach:
bs = "1-2-3".split("-")
strings = 1.upto(bs.size).flat_map do |n|
bs.permutation(n).map { |vs| vs.join("-") }
end
#=> ["1", "2", "3", "1-2", "1-3", "2-1", "2-3", "3-1", "3-2", "1-2-3", "1-3-2", "2-1-3", "2-3-1", "3-1-2", "3-2-1"]
You ned to use Array#permutation method in order to get all permutations:
arr = "1-2-3".split '-' # => ["1", "2", "3"]
res = (1..arr.length).reduce([]) { |res, length|
res += arr.permutation(length).to_a
}.map {|arr| arr.join('-')}
puts res.inspect
# => ["1", "2", "3", "1-2", "1-3", "2-1", "2-3", "3-1", "3-2", "1-2-3", "1-3-2", "2-1-3", "2-3-1", "3-1-2", "3-2-1"]
Let me explain the code:
You split string into array passing separator '-' to String#split method
You need all permutations of length 1, 2, 3. Range 1..arr.length represents all these lengths.
You collect an array of all permutations using Enumerable#reduce.
You will get array of arrays here:
[["1"], ["2"], ["3"], ["1", "2"], ["1", "3"], ["2", "1"], ["2", "3"], ["3", "1"], ["3", "2"], ["1", "2", "3"], ["1", "3", "2"], ["2", "1", "3"], ["2", "3", "1"], ["3", "1", "2"], ["3", "2", "1"]]
You transform all subarrays of this array into strings using Array#join with your '-' separator inside of Enumerable#map
Array#permutation(n) will give you all the permutations of length n as an Array of Arrays so you can call this with each length between 1 and the number of digits in badges. The final step is to map these all back into strings delimited with -.
badges = "1-2-3"
badges_split = badges.split('-')
permutations = []
(1..badges_split.size).each do |n|
permutations += badges_split.permutation(n).to_a
end
result = permutations.map { |permutation| permutation.join('-') }
Update: I think Alex's use of reduce is a more elegant approach but I'll leave this answer here for now in case it is useful.