Ruby: how to split a collection (Enumerable) in multiple (>2) partitions by some predicate? - ruby

Is there a function, to partition a collection (Enumerable) by some non-boolean predicate into more than 2 partitions? (my math knowledge got rusty - but I think, this was called "factorize" - or at least in German "Faktorisierung")
This follows my own yesterday question (Ruby: how to split a collection by predicate into two collections in ruby?)
and I wonder, is there another standard ruby method I just missed.
Here´s an example, showing the principle:
def factorize(eee)
h={}; h.default_proc= proc{|h,k| h[k]=[] }
eee.each{|e| k=yield e; h[k]<<e }
h.values
end
factorize( 1..10 ){|n| n%3 } #=> [[1, 4, 7, 10], [2, 5, 8], [3, 6, 9]]

You can use group_by: https://apidock.com/ruby/v2_5_5/Enumerable/group_by
Example:
(1..10).group_by { |n| n % 3 }.values
=> [[1, 4, 7, 10], [2, 5, 8], [3, 6, 9]]

While the Enumerable#group_by solution is clearly the answer, it may be instructive to streamline your original attempt using #each_with_object.
def factorize(eee)
eee.each_with_object({}) do |e, hsh|
k = yield e
hsh[k] ||= []
hsh[k] << e
end.values
end
factorize(1..10) { |x| x % 3 }
# => [[1, 4, 7, 10], [2, 5, 8], [3, 6, 9]]

Related

create array with iterative assignment

Would like to exploit the following behaviour in Ruby
ary = Array.new(5) { |i|
[i, j=2*i, k=j+1]
}
p ary #> [[0, 0, 1], [1, 2, 3], [2, 4, 5], [3, 6, 7], [4, 8, 9]]
It works for my purposes, but I couldn't find in the language definition whether this is legal Ruby. Is it? Or is it likely to break in the future?
[Edit] A smaller working example raising the same issue is
i = 1
ary = [i, j=2*i, k=j+1]
p ary #> [1, 2, 3]
But of course this example only has theoretical relevance contrary to the first, which does have practical relevance.

Problems using `elsif` in a `sort` block

I have this array:
ary = [[1, 6, 7], [1, 4, 9], [1, 8, 3]]
I want to sort it by the first odd number, or the last number if they are all even, in each subarray.
Since the first element in each array is the same object 1 for this particular ary, I can solve this like this:
ary2 = ary.sort_by { |a, b, c| b.odd? ? b : c }
But when I try a more general one:
arr2 = ary.sort_by { |a, b, c| a.odd? ? a : b.odd? ? b : c }
ary2 comes back unsorted.
I tried removing the ternary operators like this:
ary2 = ary.sort_by do |a, b, c|
if a.odd?
a
elsif b.odd?
b
else
c
end
end
with the same effect (i.e., none).
Is there some reason that elsif can't be used in blocks passed to the sort_by method?
Edit: Axiac pointed out the problem with my logic. It looks like conditional logic has to deal with all of the possible permutations of odd and even values. This works:
arr2 = arr.sort_by do |a, b, c|
if a.odd?
if b.odd?
if c.odd?
[a, b, c]
else
[a, b]
end
elsif c.odd?
[a, c]
else
a
end
elsif b.odd?
if c.odd?
[b, c]
else
b
end
else
c
end
end
Maybe there's a more succinct and less brittle way to do it, but it's probably a good idea to do it this way instead:
arr2 = arr.sort_by do |sub_arr|
temp = sub_arr.select do |e|
e.odd?
end
temp.empty? ? Array(sub_arr.last) : temp
end
I'll see myself out.
Regarding your original question, just as axiac points out in the comment, the result of the sorting should be exactly the same as the input array because they are all sorted by the first odd element in each subarray, which is 1, and the sort method is stable in MRI.
Regarding your question after the edit, my answer would be:
ary.sort_by{|a| a[0...-1].select(&:odd?) << a.last}
# => [[1, 8, 3], [1, 6, 7], [1, 4, 9]]
I am pretty confident that this is what you wrote after the edit that you wanted, but I am not sure if this is what you wanted since the sorting mechanism looks strange to me.
I find the statement of the question ambiguous. I will give an answer that is consist with one interpretation. If that is not what you want, please clarify hte question.
def my_sort(arr)
arr.sort_by {|a| a.any?(&:odd?) ? a.map {|e| e.odd? ? e : Float::INFINITY} : [a.last]}
end
my_sort [[1, 6, 7], [1, 4, 9], [1, 2, 3]]
#=> [[1, ∞, 7], [1, ∞, 9], [1, ∞, 3]] (sort_by)
#=> [[1, 2, 3], [1, 6, 7], [1, 4, 9]]
my_sort [[3, 6, 7], [4, 1, 9], [5, 8, 1]]
#=> [[3, ∞, 7], [∞, 1, 9], [5, ∞, 1]] (sort_by)
#=> [[3, 6, 7], [5, 8, 1], [4, 1, 9]]
my_sort [[2, 6, 8], [4, 1, 4], [8, 6, 2]]
#=> [[8], [∞, 1, ∞], [2]] (sort_by)
#=> [[8, 6, 2], [2, 6, 8], [4, 1, 4]]
my_sort [[8, 6, 2], [5, 1, 1], [6, 8, 4]]
#=> [[2], [5, 1, 1], [4] (sort_by)
#=> [[8, 6, 2], [6, 8, 4], [5, 1, 1]]
For each example I've shown the arrays used by sort_by to produce the sort shown on the following line.

Repeating a loop when it reaches the end

I am trying to conceptualize the iteration of two loops
numbers_array = [1,2,3,4,5,6,7,8,9,10]
add_to_array = [1,2,3,4]
While the numbers_array iterates, add_to_array iterates simultaneously adding both elements together at the same time. The caveat is once add_to_array reaches the end, it starts over adding its element to the next index in numbers_array. So at numbers_array[4] we would be adding add_to_array[0] then adding numbers_array[5] to add_to_array[1] and so on. This process would repeat until we reach the end of the numbers_array.
Any input would be greatly appreciated!
You are looking for Enumerable#zip and Enumerable#cycle:
numbers_array = [1,2,3,4,5,6,7,8,9,10]
#⇒ [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
add_to_array = [1,2,3,4]
#⇒ [1, 2, 3, 4]
numbers_array.zip(add_to_array.cycle)
#⇒ [[1, 1], [2, 2], [3, 3], [4, 4], [5, 1],
# [6, 2], [7, 3], [8, 4], [9, 1], [10, 2]]
Now do whatever you want with the array returned. E.g. to reduce the zipped result summing elements, map ro Enumerable#sum:
numbers_array.zip(add_to_array.cycle).map(&:sum)
#⇒ [2, 4, 6, 8, 6, 8, 10, 12, 10, 12]
It works by using the % operator to cycle through the indexes.
numbers_array = [1,2,3,4,5,6,7,8,9,10]
add_to_array = [1,2,3,4]
numbers_array.map.with_index do |n, i|
n + add_to_array[i % add_to_array.length]
end
A cool method that's similar, if you didn't want to start over at the next array, would be .zip
https://apidock.com/ruby/Array/zip
add_to_array.zip(*numbers_array.each_slice(add_to_array.size)).
map { |a| a.sum { |e| e.to_i } }
#=> [16, 20, 13, 16]
e.to_i is needed to convert nil values to zeros. See NilClass#to_i.
Another option:
numbers_array.map { |e| e + add_to_array.rotate!.last }
# => [2, 4, 6, 8, 6, 8, 10, 12, 10, 12]
Drawback: add_to_array is mutated by rotate!

Ruby, perform operation on an array and return the new array, aswell as "changes"

I am looking for a way to perform a certain operation (for instance delete_if) on an array and return both the deleted elements, and the remaining elements.
For example
a = [1,2,3,4,5,6,7,8,9,10]
a.delete_if {|x| x.even? } #=> [[1, 3, 5, 7, 9]]
But what I am looking for is something like
a = [1,2,3,4,5,6,7,8,9,10]
a.some_operation #=> [[1,3,5,7,9],[2,4,6,8,10]]
How would I go about doing this?
Using Enumerable#partition:
a = [1,2,3,4,5,6,7,8,9,10]
a.partition &:even?
# => [[2, 4, 6, 8, 10], [1, 3, 5, 7, 9]]
The first element of the Enumerable#partition return value contains the elements that are evaluated to true in the block. So you need to use odd? to get what you want.
a.partition &:odd?
# => [[1, 3, 5, 7, 9], [2, 4, 6, 8, 10]]
You might be looking for something like this:
a = [1,2,3,4,5,6,7,8,9,10]
a.group_by { |x| x.even? }.values
#=> [[1, 3, 5, 7, 9], [2, 4, 6, 8, 10]]

Merge N sorted arrays in ruby lazily

How does one merge N sorted arrays (or other list-like data structures) lazily in Ruby? For example, in Python you would use heapq.merge. There must be something like this built into Ruby, right?
Here's a (slightly golfed) solution that should work on arrays of any 'list-like' collections that support #first, #shift, and #empty?. Note that it is destructive - each call to lazymerge removes one item from one collection.
def minheap a,i
r=(l=2*(m=i)+1)+1 #get l,r index
m = l if l< a.size and a[l].first < a[m].first
m = r if r< a.size and a[r].first < a[m].first
(a[i],a[m]=a[m],a[i];minheap(a,m)) if (m!=i)
end
def lazymerge a
(a.size/2).downto(1){|i|minheap(a,i)}
r = a[0].shift
a[0]=a.pop if a[0].empty?
return r
end
p arrs = [ [1,2,3], [2,4,5], [4,5,6],[3,4,5]]
v=true
puts "Extracted #{v=lazymerge (arrs)}. Arr= #{arrs.inspect}" while v
Output:
[[1, 2, 3], [2, 4, 5], [4, 5, 6], [3, 4, 5]]
Extracted 1. Arr= [[2, 3], [2, 4, 5], [4, 5, 6], [3, 4, 5]]
Extracted 2. Arr= [[3], [2, 4, 5], [4, 5, 6], [3, 4, 5]]
Extracted 2. Arr= [[4, 5], [3], [4, 5, 6], [3, 4, 5]]
Extracted 3. Arr= [[4, 5], [3, 4, 5], [4, 5, 6]]
Extracted 3. Arr= [[4, 5], [4, 5], [4, 5, 6]]
Extracted 4. Arr= [[5], [4, 5], [4, 5, 6]]
Extracted 4. Arr= [[5], [5], [4, 5, 6]]
Extracted 4. Arr= [[5, 6], [5], [5]]
Extracted 5. Arr= [[6], [5], [5]]
Extracted 5. Arr= [[5], [6]]
Extracted 5. Arr= [[6]]
Extracted 6. Arr= [[]]
Extracted . Arr= [[]]
Note also that this algorithm is also lazy about maintaining the heap property - it is not maintained between calls. This probably causes it to do more work than needed, since it does a complete heapify on each subsequent call. This could be fixed by doing a complete heapify once up front, then calling minheap(a,0) before the return r line.
I ended up writing it myself using the data structures from the 'algorithm' gem. It wasn't as bad as I expected.
require 'algorithms'
class LazyHeapMerger
def initialize(sorted_arrays)
#heap = Containers::Heap.new { |x, y| (x.first <=> y.first) == -1 }
sorted_arrays.each do |a|
q = Containers::Queue.new(a)
#heap.push([q.pop, q])
end
end
def each
while #heap.length > 0
value, q = #heap.pop
#heap.push([q.pop, q]) if q.size > 0
yield value
end
end
end
m = LazyHeapMerger.new([[1, 2], [3, 5], [4]])
m.each do |o|
puts o
end
Here's an implementation which should work on any Enumerable, even infinite ones. It returns Enumerator.
def lazy_merge *list
list.map!(&:enum_for) # get an enumerator for each collection
Enumerator.new do |yielder|
hash = list.each_with_object({}){ |enum, hash|
begin
hash[enum] = enum.next
rescue StopIteration
# skip empty enumerators
end
}
loop do
raise StopIteration if hash.empty?
enum, value = hash.min_by{|k,v| v}
yielder.yield value
begin
hash[enum] = enum.next
rescue StopIteration
hash.delete(enum) # remove enumerator that we already processed
end
end
end
end
Infinity = 1.0/0 # easy way to get infinite range
p lazy_merge([1, 3, 5, 8], (2..4), (6..Infinity), []).take(12)
#=> [1, 2, 3, 3, 4, 5, 6, 7, 8, 8, 9, 10]
No, there's nothing built in to do that. At least, nothing that springs instantly to mind. However, there was a GSoC project to implement the relevant data types a couple of years ago, which you could use.

Resources