Ruby Lambda For Sorting by Multiple Parameters - ruby
I need to create lambdas for sort criteria.
To simplify the lambda creation process, I would like to order the comparisons consistently, for example:
[a.v1, b.v2] <=> [a.v1, b.v2]
[a.v1, b.v2] <=> [-a.v1, b.v2]
[a.v1, b.v2] <=> [a.v1, -b.v2]
[a.v1, b.v2] <=> [-a.v1, -b.v2]
To ensure that the lambdas work the way I expect, I wrote the following rspec:
class Obj
attr_reader :v1, :v2, :v3
def initialize(param1, param2, param3)
#v1 = param1
#v2 = param2
#v3 = param3
end
end
RSpec.describe(Array) do
let(:o1) { Obj.new(1, 1, 1) }
let(:o2) { Obj.new(2, 1, 1) }
let(:o3) { Obj.new(2, 2, 1) }
let(:o4) { Obj.new(3, 2, 1) }
let(:objs) { [o1, o2, o3, o4] }
# See https://ruby-doc.org/3.2.0/Comparable.html
it "uses comparators properly" do
expect([o1.v1] <=> [o2.v1]).to eq(-1)
expect([o1.v1, o1.v2] <=> [o2.v1, o2.v2]).to eq(-1)
expect([o1.v2, o1.v1] <=> [o3.v1, o3.v2]).to eq(-1)
expect([o1.v2, o1.v1] <=> [o3.v1, -o3.v2]).to eq(-1)
expect([o2.v2, o1.v1] <=> [-o2.v2, o2.v1]).to eq(1)
expect([o2.v2, o1.v1] <=> [-o2.v2, -o2.v1]).to eq(1)
end
# See https://ruby-doc.org/3.2.0/Enumerable.html#method-i-sort
it "sorts by 2 keys, both ascending" do
sort_lambda = ->(a, b) { [a.v1, a.v2] <=> [b.v1, b.v2] }
result = objs.sort(&sort_lambda)
expect(result).to eq([o1, o2, o3, o4])
end
it "sorts by 2 keys, both descending" do
sort_lambda = ->(a, b) { [a.v1, a.v2] <=> [-b.v1, -b.v2] }
result = objs.sort(&sort_lambda)
expect(result).to eq([o4, o3, o2, o1])
end
it "sorts by 2 keys, first descending and second ascending" do
sort_lambda = ->(a, b) { [a.v1, b.v2] <=> [-a.v1, b.v2] }
result = objs.sort(&sort_lambda)
expect(result).to eq([o4, o3, o2, o1])
end
# This one fails ... why?
it "sorts by 2 keys, first ascending and second descending" do
sort_lambda = ->(a, b) { [a.v1, b.v2] <=> [a.v1, -b.v2] }
result = objs.sort(&sort_lambda)
expect(result).to eq([o1, o3, o2, o4])
end
end
The tests all pass, except the last one, which fails with:
Failure/Error: expect(result).to eq([o1, o3, o2, o4])
expected: [#<Obj:0x00007fc2ac112940 #v1=1, #v2=1, #v3=1>, #<Obj:0x00007fc2ac112828 #v1=2, #v2=2, #v3=1>, #<Obj:0x00007fc2ac1128c8 #v1=2, #v2=1, #v3=1>, #<Obj:0x00007fc2ac112788 #v1=3, #v2=2, #v3=1>]
got: [#<Obj:0x00007fc2ac112788 #v1=3, #v2=2, #v3=1>, #<Obj:0x00007fc2ac112828 #v1=2, #v2=2, #v3=1>, #<Obj:0x00007fc2ac1128c8 #v1=2, #v2=1, #v3=1>, #<Obj:0x00007fc2ac112940 #v1=1, #v2=1, #v3=1>]
Why?
I think you might misunderstand how Array#<=> (spaceship) works. (or maybe it is just a typo in your tests)
What this does is sort by a[0] <=> b[0] and if that is 0 (equal) then it moves on to a[1] <=> b[1] and so on until (x <=> y) != 0; however it will only compare a set once meaning 4 elements 4 iterations. This causes an issue for you because it means that it will never have a set of comparisons where a == o2 && b == o3 and then also a == o3 && b == o2
In your failing test:
sort_lambda = ->(a, b) { [a.v1, b.v2] <=> [a.v1, -b.v2] }
a.v1 will always equal a.v1 e.g. return 0 so it moves on to comparing b.v2 to -b.v2 and you end up with b.v2 in descending order.
It appears what you actually wanted is
sort_lambda = ->(a, b) { [a.v1, -a.v2] <=> [b.v1, -b.v2] }
Example:
a = [[1,1,1],[2,1,1],[2,2,1],[3,2,1]]
sort_lambda = ->(a, b) { [a.v1, -a.v2] <=> [b.v1, -b.v2] }
a.sort(&sort_lambda)
#=> [[1, 1, 1], [2, 2, 1], [2, 1, 1], [3, 2, 1]]
Notes:
All your tests suffer from a very specific issue which is that the operator needs to be applied to both sides. For example it "sorts by 2 keys, both descending" appears to pass but this is only due to the order in which you have presented the arguments. e.g.
sort_lambda = ->(a, b) { [a[0], a[1]] <=> [-b[0], -b[1]] }
a.sort(&sort_lambda)
#=> [[3, 2, 1], [2, 2, 1], [2, 1, 1], [1, 1, 1]] #looks like it works
b = [a[0],a[2],a[1],a[3]]
b.sort(&sort_lambda)
#=> [[3, 2, 1], [2, 1, 1], [2, 2, 1], [1, 1, 1]] # Oh No
b.sort(&sort_lambda) == a.sort(&sort_lambda) #=> false
# apply the same unary on both sides
sort_lambda = ->(a, b) { [-a[0], -a[1]] <=> [-b[0], -b[1]] }
b.sort(&sort_lambda)
#=> [[3, 2, 1], [2, 2, 1], [2, 1, 1], [1, 1, 1]] # that's better
b.sort(&sort_lambda) == a.sort(&sort_lambda) #=> true
The it "sorts by 2 keys, first descending and second ascending" test is wrong but because your expectation ([o4, o3, o2, o1]) is incorrect the test passes. Based on the test description the expected output should be [o4, o2, o3, o1] and the lambda body should change to [-a.v1, a.v2] <=> [-b.v1, b.v2]
These tests are easier expressed using sort_by e.g. sort_by {|a| [-a.v1,a.v2]}
For posterity, here is the answer with #engineersmnky's corrections. Both a sort and an equivalent sort_by approach is provided for each scenario.
class Obj
attr_reader :v1, :v2, :v3
def initialize(param1, param2, param3)
#v1 = param1
#v2 = param2
#v3 = param3
end
end
RSpec.describe(Array) do # rubocop:disable Metrics/BlockLength
let(:o1) { Obj.new(1, 1, 1) }
let(:o2) { Obj.new(2, 1, 1) }
let(:o3) { Obj.new(2, 2, 1) }
let(:o4) { Obj.new(3, 2, 1) }
let(:objs) { [o1, o2, o3, o4] }
# See https://ruby-doc.org/3.2.0/Comparable.html
it "uses comparators properly" do
expect([o1.v1] <=> [o2.v1]).to eq(-1)
expect([o1.v1, o1.v2] <=> [o2.v1, o2.v2]).to eq(-1)
expect([o1.v2, o1.v1] <=> [o3.v1, o3.v2]).to eq(-1)
expect([o1.v2, o1.v1] <=> [o3.v1, -o3.v2]).to eq(-1)
expect([o2.v2, o1.v1] <=> [-o2.v2, o2.v1]).to eq(1)
expect([o2.v2, o1.v1] <=> [-o2.v2, -o2.v1]).to eq(1)
end
# See https://ruby-doc.org/3.2.0/Enumerable.html#method-i-sort
it "sort with 2 keys, both ascending" do
sort_lambda = ->(a, b) { [a.v1, a.v2] <=> [b.v1, b.v2] }
result = objs.sort(&sort_lambda)
expect(result).to eq([o1, o2, o3, o4])
end
it "sort_by with 2 keys, both ascending" do
sort_lambda = ->(a) { [a.v1, a.v2] }
result = objs.sort_by(&sort_lambda)
expect(result).to eq([o1, o2, o3, o4])
end
it "sort with 2 keys, both descending" do
sort_lambda = ->(a, b) { [-a.v1, -a.v2] <=> [-b.v1, -b.v2] }
result = objs.sort(&sort_lambda)
expect(result).to eq([o4, o3, o2, o1])
end
it "sort_by with 2 keys, both descending" do
sort_lambda = ->(a) { [-a.v1, -a.v2] }
result = objs.sort_by(&sort_lambda)
expect(result).to eq([o4, o3, o2, o1])
end
it "sort with 2 keys, first descending and second ascending" do
sort_lambda = ->(a, b) { [-a.v1, a.v2] <=> [-b.v1, b.v2] }
result = objs.sort(&sort_lambda)
expect(result).to eq([o4, o2, o3, o1])
end
it "sort_by with 2 keys, first descending and second ascending" do
sort_lambda = ->(a) { [-a.v1, a.v2] }
result = objs.sort_by(&sort_lambda)
expect(result).to eq([o4, o2, o3, o1])
end
it "sort with 2 keys, first ascending and second descending" do
sort_lambda = ->(a, b) { [a.v1, -a.v2] <=> [b.v1, -b.v2] }
result = objs.sort(&sort_lambda)
expect(result).to eq([o1, o3, o2, o4])
end
it "sort_by with 2 keys, first ascending and second descending" do
sort_lambda = ->(a) { [a.v1, -a.v2] }
result = objs.sort_by(&sort_lambda)
expect(result).to eq([o1, o3, o2, o4])
end
end
This solution throws an exception if a property of type Date is used as a descending sort key. The error message of the form undefined method -#' for #<Date: 2011-01-01 ((2455563j,0s,0n),+0s,2299161j)>`.
To see the error:
Modify initialize to be:
def initialize(param1, param2)
#v1 = Date.parse(param1)
#v2 = Date.parse(param2)
#v3 = param3
end
Modify the definitions of o1 .. o4 to be of the form:
let(:o1) { Obj.new('2000-01-01', '2001-01-01', 1) }
let(:o2) { Obj.new('2010-01-01', '2001-01-01', 1) }
let(:o3) { Obj.new('2010-01-01', '2011-01-01', 1) }
let(:o4) { Obj.new('2020-01-01', '2011-01-01', 1) }
let(:objs) { [o1, o2, o3, o4] }
The previous answers fail when applying a descending sort according to Date field(s). This solution uses the suggestion from #ngineersmnky to reverse the comparison order for fields that need to be sorted in descending order.
Note that although sort_by is more convenient to use than sort, only sort provides the ability to reverse the order of comparison, so it must be used for this solution, and not sort_by.
class Obj
# `date_modified` is primary sort key
# `date` (when specified) is secondary sort key
attr_reader :date, :date_modified
def initialize(param1, param2)
#date_modified = Date.parse(param1)
#date = Date.parse(param2)
end
end
RSpec.describe(Obj) do
let(:o1) { Obj.new('2000-01-01', '2001-01-01') }
let(:o2) { Obj.new('2010-01-01', '2001-01-01') }
let(:o3) { Obj.new('2010-01-01', '2011-01-01') }
let(:o4) { Obj.new('2020-01-01', '2011-01-01') }
let(:objs) { [o1, o2, o3, o4] }
# See https://ruby-doc.org/3.2.0/Comparable.html
it "compares one key with ascending dates" do
expect([o1.date_modified] <=> [o2.date_modified]).to eq(-1)
expect([o2.date_modified] <=> [o3.date_modified]).to eq(0)
expect([o3.date_modified] <=> [o4.date_modified]).to eq(-1)
end
it "compares two keys with ascending dates" do
expect([o1.date_modified, o1.date] <=> [o2.date_modified, o2.date]).to eq(-1)
expect([o2.date_modified, o2.date] <=> [o3.date_modified, o3.date]).to eq(-1)
expect([o3.date_modified, o3.date] <=> [o4.date_modified, o4.date]).to eq(-1)
end
it "compares one key with descending dates" do
expect([o1.date_modified] <=> [o2.date_modified]).to eq(-1)
expect([o2.date_modified] <=> [o3.date_modified]).to eq(0)
end
it "compares two keys with descending dates" do
expect([o2.date_modified, o2.date] <=> [o1.date_modified, o1.date]).to eq(1)
expect([o3.date_modified, o3.date] <=> [o2.date_modified, o2.date]).to eq(1)
expect([o4.date_modified, o4.date] <=> [o3.date_modified, o3.date]).to eq(1)
end
# See https://ruby-doc.org/3.2.0/Enumerable.html#method-i-sort
it "sort with both keys ascending" do
sort_lambda = ->(a, b) { [a.date_modified, a.date] <=> [b.date_modified, b.date] }
result = objs.sort(&sort_lambda)
expect(result).to eq([o1, o2, o3, o4])
end
it "sort with both keys descending" do
sort_lambda = ->(a, b) { [b.date_modified, b.date] <=> [a.date_modified, a.date] }
result = objs.sort(&sort_lambda)
expect(result).to eq([o4, o3, o2, o1])
end
it "sort with date_modified descending and date ascending" do
sort_lambda = ->(a, b) { [b.date_modified, a.date] <=> [a.date_modified, b.date] }
result = objs.sort(&sort_lambda)
expect(result).to eq([o4, o2, o3, o1])
end
it "sort with date_modified ascending and date descending" do
sort_lambda = ->(a, b) { [a.date_modified, b.date] <=> [b.date_modified, a.date] }
result = objs.sort(&sort_lambda)
expect(result).to eq([o1, o3, o2, o4])
end
end
Related
Ruby. Shuffle the array so that there are no adjacent elements with the same value
An array of hashes is given (10 elements at least): arr = [{letter: "a", number: "1"}, {letter: "a", number: "3"}, {letter: "b", number: "4"}, {letter: "b", number: "1"}, ..., {letter: "e", number: "2"} ] The task is to shuffle the array so that there are no adjacent elements with the same 'letter' value. So, the result should be like the following: [{letter: "c", number: "4"}, {letter: "a", number: "1"}, {letter: "e", number: "2"}, {letter: "b", number: "1"}, ..., {letter: "a", number: "3"} ] What is the simplest way to do that? === UPDATE === The number of repeated letters in the array is precisely known - it's 20% of the array length. So, the array looks like the following: [ {letter: "a", number: "1"}, {letter: "a", number: "3"}, {letter: "b", number: "4"}, {letter: "b", number: "1"}, {letter: "c", number: "7"}, {letter: "c", number: "3"}, {letter: "d", number: "6"}, {letter: "d", number: "4"}, {letter: "e", number: "5"}, {letter: "e", number: "2"} ] Or, its simplified version: ["a", "a", "b", "b", "c", "c", "d", "d", "e", "e"] Or, for example, there is a simplified array containing 15 elements: ["a", "a", "a", "b", "b", "b", "c", "c", "c", "d", "d", "d", "e", "e", "e"]
The simplest way (without any random): # Calculate letter frequency freq = arr.group_by { |h| h[:letter] }.map { |k, v| [k, v.size] }.to_h # Then check that the most frequent element occurs less that arr.size / 2 center = (arr.size + 1) / 2 if freq.values.max > center # Impossible end # Sort array by frequency to have most frequent first. sarr = arr.sort_by { |h| freq[h[:letter]] }.reverse sarr[0..center-1].zip(sarr[center..-1]).flatten.compact Your problem is a special case of this question. See my answer for the detailed explanation how this works. We even don't need to sort by letter frequency. It's for corner cases like "abbcccc". We can solve them in another way: # Works with correct data: most frequent letter occurs <= center times def f(arr) arr = arr.sort center = (arr.size + 1) / 2 arr = arr[0..center-1].zip(arr[center..-1]).flatten.compact double = (1..arr.size-1).find { |i| arr[i] == arr[i-1] } double ? arr.rotate(double) : arr # fix for the corner cases end puts f(%w[a a a a b b c].shuffle).join # ababaca puts f(%w[a a b b b b c].shuffle).join # bcbabab puts f(%w[a b b c c c c].shuffle).join # cacbcbc The only non-linear part of the algorithm is arr.sort. But as you can see by the link above, we even don't need the sorting. We need letters counts, which could be found in linear time. Therefore, we can reduce the algorithm to O(n). The number of repeated letters in the array is precisely known - it's 20% of the array length. With this update, the algorithm is simplified to (as there are no corner cases): sarr = arr.sort_by { |h| h[:letter] } center = (arr.size + 1) / 2 sarr[0..center-1].zip(sarr[center..-1]).flatten.compact
The simple and maybe the less effective way could be the brute force. So on a simplified version of the array, one can do: ary = %w(a a c c b a s) loop do break if ary.shuffle!.slice_when { |a, b| a == b }.to_a.size == 1 end Some check should be added to assure that a solution exists, to avoid infinite loop. Other (better?) way is to shuffle then find the permutation (no infinite loop) which satisfy the condition: ary.shuffle! ary.permutation.find { |a| a.slice_when { |a, b| a == b }.to_a.size == 1 } If a solution does not exist, it returns nil. Run the the benchmark: def looping ary = %w(a a c c b a s) loop do break if ary.shuffle!.slice_when { |a, b| a == b }.to_a.size == 1 end ary end def shuffle_permute ary = %w(a a c c b a s) ary.shuffle! ary.permutation.lazy.find { |a| a.slice_when { |a, b| a == b }.to_a.size == 1 } end require 'benchmark' n = 500 Benchmark.bm do |x| x.report { looping } x.report { shuffle_permute } end
Code def reorder(arr) groups = arr.group_by { |h| h[:letter] } return nil if 2 * groups.map { |_,v| v.size }.max > arr.size + 1 max_key = groups.max_by { |_,a| a.size }.first letters = ([max_key] + (groups.keys - [max_key])).cycle ordered = [] while ordered.size < arr.size k = letters.next ordered << groups[k].pop unless groups[k].empty? end ordered end nilis returned if it is not possible to rearrange the elements in such a way that g[:letter] != h[:letter] for all pairs of consecutive elements g and h. Note that this method has near linear computational complexity, O(arr.size), "near" because hash lookups are not quite constant time. If desired, one could call the method with arr randomized: reorder(arr.shuffle). Example arr = [ { letter: "a" }, { letter: "e" }, { letter: "b" }, { letter: "b" }, { letter: "e" }, { letter: "a" }, { letter: "a" }, { letter: "f" } ] reorder(arr) #=> [{:letter=>"a"}, {:letter=>"e"}, {:letter=>"b"}, {:letter=>"f"}, # {:letter=>"a"}, {:letter=>"e"}, {:letter=>"b"}, {:letter=>"a"}] Proof The assertion is that if the line return nil if 2 * groups.map { |_,v| v.size }.max > arr.size + 1 were removed from the method the array returned by the method would have the property that for all pairs of successive elements, g, h, g[:letter] != h[:letter] if and only if 2 * groups.map { |_,v| v.size }.max <= arr.size + 1 The proof has two parts. The above inequality holds if the method produces a valid array Compute max_key = groups.max_by { |_,a| a.size }.first max_key_freq = groups.map { |_,v| v.size }.max and assume a valid array is returned. There must be at least one element other than max_key between each successive value of max_key in that array. The number of elements of arr other than max_key must therefore be at least max_key_freq - 1, so that max_key_freq + max_key_freq - 1 <= arr.size Hence, 2 * max_key_freq <= arr.size + 1 which is the same as: 2 * groups.map { |_,v| v.size }.max <= arr.size + 1 The above inequality does not hold if the method produces an invalid array Suppose ordered is returned and it contains successive elements g and h for which both g[:letter] and h[:letter] equal the same letter l. Because of the way ordered is constructed: groups[k] must be empty for all keys k in groups for which k != l; f[:letter] must equal l for all elements of ordered following g (if there are any); and l must be the first key enumerated by keys, which is a letter that appears with a frequency that is not less than that of any other letter. l has frequency groups.map { |_,v| v.size }.max. If n = groups.keys.size there must be a non-negative integer k (loosely, the number of rounds of allocations for all keys of groups) such that the number of elements h of arr for which h[:letter] != l equals k*n and the number of elements h of arr for which h[:letter] == l is k*n + 2 + m, where m >= 0. The size of arr is therefore 2*k*n + 2 + m. In that case, 2 * groups.map { |_,v| v.size }.max > arr.size + 1 -> 2 * (k*n + 2 + m) > (k*n + 2 + m + k*n) + 1 -> 2*k*n + 4 + 2*m > 2*k*n + 3 + m -> (4-3) + m > 0 -> true Explanation For the example, groups = arr.group_by { |h| h[:letter] } #=> {"a"=>[{:letter=>"a"}, {:letter=>"a"}, {:letter=>"a"}], # "e"=>[{:letter=>"e"}, {:letter=>"e"}], # "b"=>[{:letter=>"b"}, {:letter=>"b"}], # "f"=>[{:letter=>"f"}]} The following tells us that a solution exists. 2 * groups.map { |_,v| v.size }.max > arr.size + 1 #=> 2 * [3, 2, 2, 1].max > 8 + 1 #=> 2 * 3 > 9 #=> 6 > 9 #=> false Next create an enumerator letters. max_key = groups.max_by { |_,a| a.size }.first #=> "a" letters = ([max_key] + (groups.keys - [max_key])).cycle #=> #<Enumerator: ["a", "e", "b", "f"]:cycle> The elements of letters are generated as follows. letters.next #=> "a" letters.next #=> "e" letters.next #=> "b" letters.next #=> "f" letters.next #=> "a" letters.next #=> "e" ... ad infinititum See Array#cycle. I can best explain the remaining calculations by salting the method with puts statements before running the method. Note that arr.size #=> 8. def reorder(arr) groups = arr.group_by { |h| h[:letter] } puts "groups = #{groups}" return nil if 2 * groups.map { |_,v| v.size }.max > arr.size + 1 max_key = groups.max_by { |_,a| a.size }.first letters = ([max_key] + (groups.keys - [max_key])).cycle ordered = [] while ordered.size < arr.size puts "\nordered.size = #{ordered.size} < #{arr.size} = #{ordered.size < arr.size}" k = letters.next puts "k = #{k}" puts "groups[#{k}].empty? = #{groups[k].empty?}" ordered << groups[k].pop unless groups[k].empty? puts "ordered = #{ordered}" puts "groups = #{groups}" end ordered end reorder(arr) #=> [{:letter=>"a"}, {:letter=>"e"}, {:letter=>"b"}, {:letter=>"f"}, # {:letter=>"a"}, {:letter=>"e"}, {:letter=>"b"}, {:letter=>"a"}] The following is displayed. groups = {"a"=>[{:letter=>"a"}, {:letter=>"a"}, {:letter=>"a"}], "e"=>[{:letter=>"e"}, {:letter=>"e"}], "b"=>[{:letter=>"b"}, {:letter=>"b"}], "f"=>[{:letter=>"f"}]} ordered.size = 0 < 8 = true k = a groups[a].empty? = false ordered = [{:letter=>"a"}] groups = {"a"=>[{:letter=>"a"}, {:letter=>"a"}], "e"=>[{:letter=>"e"}, {:letter=>"e"}], "b"=>[{:letter=>"b"}, {:letter=>"b"}], "f"=>[{:letter=>"f"}]} ordered.size = 1 < 8 = true k = e groups[e].empty? = false ordered = [{:letter=>"a"}, {:letter=>"e"}] groups = {"a"=>[{:letter=>"a"}, {:letter=>"a"}], "e"=>[{:letter=>"e"}], "b"=>[{:letter=>"b"}, {:letter=>"b"}], "f"=>[{:letter=>"f"}]} ordered.size = 2 < 8 = true k = b groups[b].empty? = false ordered = [{:letter=>"a"}, {:letter=>"e"}, {:letter=>"b"}] groups = {"a"=>[{:letter=>"a"}, {:letter=>"a"}], "e"=>[{:letter=>"e"}], "b"=>[{:letter=>"b"}], "f"=>[{:letter=>"f"}]} ordered.size = 3 < 8 = true k = f groups[f].empty? = false ordered = [{:letter=>"a"}, {:letter=>"e"}, {:letter=>"b"}, {:letter=>"f"}] groups = {"a"=>[{:letter=>"a"}, {:letter=>"a"}], "e"=>[{:letter=>"e"}], "b"=>[{:letter=>"b"}], "f"=>[]} ordered.size = 4 < 8 = true k = a groups[a].empty? = false ordered = [{:letter=>"a"}, {:letter=>"e"}, {:letter=>"b"}, {:letter=>"f"}, {:letter=>"a"}] groups = {"a"=>[{:letter=>"a"}], "e"=>[{:letter=>"e"}], "b"=>[{:letter=>"b"}], "f"=>[]} ordered.size = 5 < 8 = true k = e groups[e].empty? = false ordered = [{:letter=>"a"}, {:letter=>"e"}, {:letter=>"b"}, {:letter=>"f"}, {:letter=>"a"}, {:letter=>"e"}] groups = {"a"=>[{:letter=>"a"}], "e"=>[], "b"=>[{:letter=>"b"}], "f"=>[]} ordered.size = 6 < 8 = true k = b groups[b].empty? = false ordered = [{:letter=>"a"}, {:letter=>"e"}, {:letter=>"b"}, {:letter=>"f"}, {:letter=>"a"}, {:letter=>"e"}, {:letter=>"b"}] groups = {"a"=>[{:letter=>"a"}], "e"=>[], "b"=>[], "f"=>[]} ordered.size = 7 < 8 = true k = f groups[f].empty? = true ordered = [{:letter=>"a"}, {:letter=>"e"}, {:letter=>"b"}, {:letter=>"f"}, {:letter=>"a"}, {:letter=>"e"}, {:letter=>"b"}] groups = {"a"=>[{:letter=>"a"}], "e"=>[], "b"=>[], "f"=>[]} ordered.size = 7 < 8 = true k = a groups[a].empty? = false ordered = [{:letter=>"a"}, {:letter=>"e"}, {:letter=>"b"}, {:letter=>"f"}, {:letter=>"a"}, {:letter=>"e"}, {:letter=>"b"}, {:letter=>"a"}] groups = {"a"=>[], "e"=>[], "b"=>[], "f"=>[]} Refering to the revised question, if arr = ["a", "a", "b", "b", "c", "c", "d", "d", "e", "e"] one could simply write: arr.each_slice(arr.index { |s| s != arr.first }.to_a.transpose.flatten #=> ["a", "b", "c", "d", "e", "a", "b", "c", "d", "e"] or arr.each_slice(arr.count(arr.first)).to_a.transpose.flatten
This sounds a lot like backtracking. I would build the "shuffled" array from left to right. Assume that there are N elements in the array. Say that at some point during the algorithm, you have already the first k elements arranged to fulfil the condition. Now you pick from the remaining (N-k) elements the first one, which you can append to your result array, without breaking the condition. If you can find one, you repeat the process recursively, now having an result array of (k+1) elements. If you can not find one, you return a failure indicator and let the caller (i.e. the previous recursion) try another choice.
Keys of a hash whose values sum to a particular value
I have a hash: a = {"Q1"=>1, "Q2"=>2, "Q5"=>3, "Q8"=>3} I want to retrieve a set of keys from it such that the sum of their values equals a certain number, for example 5. In such case, the output should be: Q2 Q5 Please help me on how to get this.
def find_combo(h, tot) arr = h.to_a (1..arr.size).find do |n| enum = arr.combination(n).find { |e| e.map(&:last).sum == tot } return enum.map(&:first) unless enum.nil? end end h = {"Q1"=>1, "Q2"=>2, "Q5"=>3, "Q8"=>3} find_combo(h, 5) #=> ["Q2", "Q5"] find_combo(h, 2) #=> ["Q2"] find_combo(h, 6) #=> ["Q5", "Q8"] find_combo(h, 4) #=> ["Q1", "Q5"] find_combo(h, 8) #=> ["Q2", "Q5", "Q8"] find_combo(h, 9) #=> ["Q1", "Q2", "Q5", "Q8"] find_combo(h, 10) #=> nil
Just out of curiosity: hash = {"Q1"=>1, "Q2"=>2, "Q5"=>3, "Q8"=>3} arr = hash.to_a 1.upto(hash.size). lazy. find do |i| res = arr.combination(i).find do |h| h.map(&:last).sum == 5 end break res if res end.tap { |result| break result.to_h if result } #⇒ {"Q2" => 2, "Q5" => 3}
Ruby - converting a string into hash with each character as key and index as value?
I am trying to transform a given string into a hash with each its character = key and index = value. For example, if I have str = "hello", I would like it to transform into {"h"=>0, "e"=>1, "l"=>2, "l"=>3, "o"=>4}. I created a method as such: def map_indices(arr) arr.map.with_index {|el, index| [el, index]}.to_h end #=> map_indices('hello'.split('')) #=> {"h"=>0, "e"=>1, "l"=>3, "o"=>4} The problem is it skips the first l. If I reverse the order of el and index: arr.map.with_index {|el, index| [index, el]}.to_h, I get all the letters spelled out: {0=>"h", 1=>"e", 2=>"l", 3=>"l", 4=>"o"} But when I invert it, I get the same hash that skips one of the l's. map_indices('hello'.split('')).invert #=> {"h"=>0, "e"=>1, "l"=>3, "o"=>4} Why is this behaving like such? How can I get it to print {"h"=>0, "e"=>1, "l"=>2, "l"=>3, "o"=>4}?
It can be done, but will confuse other Ruby programmers.A normal hash treats a key "a" as identical to another "a". Unless a little known feature .compare_by_identity is used: h = {}.compare_by_identity "hello".chars.each_with_index{|c,i| h[c] = i} p h # => {"h"=>0, "e"=>1, "l"=>2, "l"=>3, "o"=>4}
Any of the following could be used. For str = "hello" all return {"h"=>[0], "e"=>[1], "l"=>[2, 3], "o"=>[4]} str.each_char .with_index .with_object({}) { |(c,i),h| (h[c] ||= []) << i } See String#each_char, Enumerator#with_index and Enumerator#with_object. The block variables have been written to exploit array decomposition. str.each_char .with_index .with_object(Hash.new { |h,k| h[k] = [] }) { |(c,i),h| h[c] << i } See the form of Hash::new that takes a block and no argument. If a hash has been defined h = Hash.new { |h,k| h[k] = [] } and later h[c] << i is executed, h[c] is first set equal to an empty array if h does not have a key c. str.size .times .with_object(Hash.new { |h,k| h[k] = [] }) { |i,h| h[str[i]] << i } str.each_char .with_index .group_by(&:first) .transform_values { |a| a.flat_map(&:last) } See Enumerable#group_by, Hash#transform_values (introduced in Ruby v2.5) and Enumerable#flat_map. Note that str.each_char .with_index .group_by(&:first) #=> {"h"=>[["h", 0]], "e"=>[["e", 1]], "l"=>[["l", 2], ["l", 3]], # "o"=>[["o", 4]]}
Another option you can use is zipping two enumerations together. s = "hello" s.chars.zip(0..s.size) This yields: [["h", 0], ["e", 1], ["l", 2], ["l", 3], ["o", 4]]
I am new to Ruby and I am sure this can be refactored, but another alternative might be: arr1 = "Hello".split(%r{\s*}) arr2 = [] for i in 0..arr1.size - 1 arr2 << i end o = arr1.zip(arr2) a_h = [] o.each do |i| a_h << Hash[*i] end p a_h.each_with_object({}) { |k, v| k.each { |kk,vv| (v[kk] ||= []) << vv } } => {"H"=>[0], "e"=>[1], "l"=>[2, 3], "o"=>[4]}
Sort by value, key in case of equality
I want to sort a Hash in Ruby by value, then by key in case of equality. I tried this but it returns the same array: hash.sort { |x, y| if x[1] == y[1] comp = x[0] <=> y[0] else comp = x[1] <=> y[1] end comp }
hash.sort {|h1,h2| h1.reverse <=> h2.reverse } or simply: hash.sort_by(&:reverse) If you want to have a Hash as response: Hash[ hash.sort_by(&:reverse) ]
Return variable rather than the value
I am curious about a feature of the .each method. a = 1 b = 2 [a,b].each do |x| puts x end Is there a way for ruby to return the variable "a" rather than the value 1?
It doesn't return 1, it returns [1, 2], the each method returns what it iterated over. > a = 1 => 1 > b = 2 => 2 > r = [a, b].each { |x| puts x } 1 2 => [1, 2] > p r.inspect "[1, 2]" If you're asking if you can "go backwards" from the array value, or the variable inside the iteration block, I don't see how. If you were iterating over a map with key/value pairs, yes. > m = { a: 1, b: 2} => {:a=>1, :b=>2} > m.each { |k, v| p "#{k} = #{v}" } "a = 1" "b = 2"