Complex subsorts on an array of arrays - ruby

I wrote a quick method that confirms that data coming from a webpage is sorted correctly:
def subsort_columns(*columns)
columns.transpose.sort
end
Which worked for basic tests. Now, complex subsorts have been introduced, and I'm pretty certain I'll need to still use an array, since hashes can't be guaranteed to return in a specific order. The order of the input in this case represents subsort priority.
# `columns_sort_preferences` is an Array in the form of:
# [[sort_ascending_bool, column_data]]
# i.e.
# subsort_columns([true, column_name], [false, column_urgency], [true, column_date])
# Will sort on name ascending, then urgency descending, and finally date ascending.
def subsort_columns(*columns_sort_preferences)
end
This is where I'm stuck. I want to do this cleanly, but can't come up with anything but rolling out a loop for each subsort that occurs on any parent sort...but it sounds wrong.
Feel free to offer better suggestions, as I'm not tied to this implementation.
Here's some test data:
a = [1,1,1,2,2,3,3,3,3]
b = %w(a b c c b b a b c)
c = %w(x z z y x z z y z)
subsort_columns([true, a], [false, b], [false, c])
=> [[1, 'c', 'z'],
[1, 'b', 'z'],
[1, 'a', 'x'],
[2, 'c', 'y'],
[2, 'b', 'x'],
[3, 'c', 'z'],
[3, 'b', 'z'],
[3, 'b', 'y'],
[3, 'a', 'z']]
Update:
Marking for reopen because I've linked to this question in a comment above the function in our codebase that I provided as my own answer. Not to mention the help I got from an answer here that clearly displays the solution to my problem, whom I'd like to give a bounty to for giving me a tip in the right direction. Please don't delete this question, it is very helpful to me. If you disagree, at least leave a comment specifying what is unclear to you.

Use sort {|a, b| block} → new_ary:
a = [1,1,1,2,2,3,3,3,3]
b = %w(a b c c b b a b c)
c = %w(x z z y x z z y z)
sorted = [a, b, c].transpose.sort do |el1, el2|
[el1[0], el2[1], el2[2]] <=> [el2[0], el1[1], el1[2]]
end
Result:
[[1, "c", "z"],
[1, "b", "z"],
[1, "a", "x"]
[2, "c", "y"],
[2, "b", "x"],
[3, "c", "z"],
[3, "b", "z"],
[3, "b", "y"],
[3, "a", "z"]]
For a descending column reverse the left and right elements of the spaceship operator.

One way to do this is to do a series of 'stable sorts' in reverse order. Start with the inner sort and work out to the outer. The stability property means that the inner sort order remains intact.
Unfortunately, Ruby's sort is not stable. But see this question for a workaround.

# Sort on each entry in `ticket_columns`, starting with the first column, then second, etc.
# Complex sorts are supported. If the first element in each `ticket_columns` is a true/false
# boolean (specifying if an ascending sort should be used), then it is sorted that way.
# If omitted, it will sort all ascending.
def _subsort_columns(*ticket_columns)
# Is the first element of every `ticket_column` a boolean?
complex_sort = ticket_columns.all? { |e| [TrueClass, FalseClass].include? e[0].class }
if complex_sort
data = ticket_columns.transpose
sort_directions = data.first
column_data = data[1..-1].flatten 1
sorted = column_data.transpose.sort do |cmp_first, cmp_last|
cmp_which = sort_directions.map { |b| b ? cmp_first : cmp_last }
cmp_these = sort_directions.map { |b| b ? cmp_last : cmp_first }
cmp_left, cmp_right = [], []
cmp_which.each_with_index { |e, i| cmp_left << e[i] }
cmp_these.each_with_index { |e, i| cmp_right << e[i] }
cmp_left <=> cmp_right
end
sorted
else
ticket_columns.transpose.sort
end
end

Related

Method to sort strings in descending order (in complex keys)

In order to descend-sort an array a of strings, reverse can be used.
a.sort.reverse
But when you want to use a string among multiple sort keys, that cannot be done. Suppose items is an array of items that have attributes attr1 (String), attr2 (String), attr3 (Integer). Sort can be done like:
items.sort_by{|item| [item.attr1, item.attr2, item.attr3]}
Switching from ascending to descending can be done independently for Integer by multiplying it with -1:
items.sort_by{|item| [item.attr1, item.attr2, -item.attr3]}
But such method is not straightforward for String. Can such method be defined? When you want to do descending sort with respect to attr2, it should be written like:
items.sort_by{|item| [item.attr1, item.attr2.some_method, item.attr3]}
I think you can always convert your strings into an array of integers (ord). Like this:
strings = [["Hello", "world"], ["Hello", "kitty"], ["Hello", "darling"]]
strings.sort_by do |s1, s2|
[
s1,
s2.chars.map(&:ord).map{ |n| -n }
]
end
PS:
As #CarySwoveland caught here is a corner case with empty string, which could be solved with this non elegant solution:
strings.sort_by do |s1, s2|
[
s1,
s2.chars.
map(&:ord).
tap{|chars| chars << -Float::INFINITY if chars.empty? }.
map{ |n| -n }
]
end
And #Jordan kindly mentioned that sort_by uses Schwartzian Transform so you don't need preprocessing at all.
The following supports all objects that respond to <=>.
def generalized_array_sort(arr, inc_or_dec)
arr.sort do |a,b|
comp = 0
a.zip(b).each_with_index do |(ae,be),i|
next if (ae<=>be).zero?
comp = (ae<=>be) * (inc_or_dec[i]==:inc ? 1 : -1)
break
end
comp
end
end
Example
arr = [[3, "dog"], [4, "cat"], [3, "cat"], [4, "dog"]]
inc_or_dec = [:inc, :dec]
generalized_array_sort(arr, inc_or_dec)
#=> [[3, "dog"], [3, "cat"], [4, "dog"], [4, "cat"]]
Another example
class A; end
class B<A; end
class C<B; end
[A,B,C].sort #=> [C, B, A]
arr = [[3, A], [4, B], [3, B], [4, A], [3, C], [4,C]]
inc_or_dec = [:inc, :dec]
generalized_array_sort(arr, inc_or_dec)
#=> [[3, A], [3, B], [3, C], [4, A], [4, B], [4, C]]
I'm not sure either of these passes your straightforwardness test, but I think both work correctly. Using #CarySwoveland's test data:
arr = [[3, "dog"], [4, "cat"], [3, "cat"], [4, "dog"]]
arr.sort_by {|a, b| [ a, *b.codepoints.map(&:-#) ] }
# => [[3, "dog"], [3, "cat"], [4, "dog"], [4, "cat"]]
Alternatively, here's a solution that works regardless of the type (i.e. it needn't be a string):
arr.sort do |a, b|
c0 = a[0] <=> b[0]
next c0 unless c0.zero?
-(a[1] <=> b[1])
end
# => [[3, "dog"], [3, "cat"], [4, "dog"], [4, "cat"]]
The latter could be generalized as a method like so:
def arr_cmp(a, b, *dirs)
return 0 if a.empty? && b.empty?
return a <=> b if dirs.empty?
a0, *a = a
b0, *b = b
dir, *dirs = dirs
c0 = a0 <=> b0
return arr_cmp(a, b, *dirs) if c0.zero?
dir * c0
end
This works just like <=> but as its final arguments takes a list of 1 or -1s indicating to the sort directions for each respective array element, e.g.:
a = [3, "dog"]
b = [3, "cat"]
arr_cmp(a, b, 1, 1) # => 1
arr_cmp(a, b, 1, -1) # => -1
Like <=> it's most useful in a sort block:
arr.sort {|a, b| arr_cmp(a, b, 1, -1) }
# => [[3, "dog"], [3, "cat"], [4, "dog"], [4, "cat"]]
I haven't tested it much, though, so there are probably edge cases for which it fails.
While I have no idea about generic academic implementation, in the real life I would go with:
class String
def hash_for_sort precision = 5
(#h_f_p ||= {})[precision] ||= self[0...precision].codepoints.map do |cp|
[cp, 99999].min.to_s.ljust 5, '0'
join.to_i
end
end
Now feel free to sort by -item.attr2.hash_for_sort.
The approach above has some glitches:
no valid sorting for the strings, that differ in > precision letters;
initial call to the function is O(self.length);
codepoints above 99999 would be considered equal (sorting is not accurate).
But taking into account the real circumstanses, I can not imagine when this won’t suffice.
P.S. If I were to solve this task precisely, I would search for an algorithm, converting strings to floats in a one-to-one manner.

Add two arrays into another array [duplicate]

This question already has answers here:
Closed 10 years ago.
Possible Duplicate:
ruby: sum corresponding members of two arrays
So I'm struggling to do this.
I am trying to add two arrays and put the results into a third one. Now I want to be able to do this through a function though.
So I have Array_One, Array_Two, and Array_Three.
I would want to call the "compare function" to one and two and make Three that length and then if they the lengths matched I would want to add One and Two and put the results in three.
Could this be done in one function?
Better way to do this?
That's my thought process but I don't have the knowledge of Ruby to do this.
EDIT:
Sorry for the vagueness.
Array_One = [3,4]
Array_Two = [1,3]
Array_Three= []
I would want to pass One and Two through a function that compares the length and verifies they're the same length.
Then I would, in my mind, send it through a function that actually does the adding.
So in the end I would have Array_Three = [4,7]
Hope that helps.
Your question's description is little confusing, but I think this may be what you want:
def add_array(a,b)
a.zip(b).map{|pair| pair.reduce(&:+) }
end
irb> add_array([1,2,3],[4,5,6])
=> [5, 7, 9]
In addition it generalize to add multiple arrays quite easily:
def add_arrays(first_ary, *other_arys)
first_ary.zip(*other_arys).map{|column| column.reduce(&:+) }
end
irb> add_arrays([1,2,3],[4,5,6])
=> [5, 7, 9]
irb> add_arrays([1,2,3],[4,5,6],[7,8,9])
=> [12, 15, 18]
One possible method (without any checks) assuming I understood your question:
x = [1, 2, 3]
y = [4, 5, 6]
z = []
x.each_with_index do |v, i|
z << v + y[i]
end
Another method (still assuming I understood the question):
[x, y].transpose.map {|v| v.reduce(:+)}
If by add you mean element-wise addition than you can use:
def add(a, b)
a.zip(b).map &:sum
end
assuming your environment has sum defined.
Sum is an alias for reduce(&:+), which you can use instead.
zip is a function that takes two arrays and returns an array of arrays:
[a1, a2, a3, ..., an].zip [b1, b2, b3, ..., bm]
#=> [[a1, b1], [a2, b2], [a3, b3], ..., [an, bn]]
# assuming n <= m
We than take our array of arrays and sum all numbers in that array together and than collect the results with map.
map is a function that takes a block and produces an array:
[c1, c2, c3, ..., cn].map &block
# => [block.call(c1), block.call(c2), block.call(c3), ..., block.call(cn)]
So if we get example input say
a = [1, 2, 3]
b = [4, 2, 5]
a.zip(b) #=> [[1,4], [2,2], [3,5]]
a.zip(b).map(&:sum) #=> [[1,4].sum, [2,2].sum, [3,5].sum] #=> [5, 4, 8]
Now we can check that the same length by using an if condition:
def add(a, b)
a.zip(b).map &:sum if a.size == b.size
end
If you want to add two arrays, you can simply add them:
array1 = [1, 2, 3]
array2 = ['a', 'b', 'c']
if array1.length == array2.length
array3 = array1 + array2
end
# array3 = [1, 2, 3, 'a', 'b', 'c']

Reordering an array in the same order as another array was reordered

I have two arrays a, b of the same length:
a = [a_1, a_2, ..., a_n]
b = [b_1, b_2, ..., b_n]
When I sort a using sort_by!, the elements of a will be arranged in different order:
a.sort_by!{|a_i| some_condition(a_i)}
How can I reorder b in the same order/rearrangement as the reordering of a? For example, if a after sort_by! is
[a_3, a_6, a_1, ..., a_i_n]
then I want
[b_3, b_6, b_1, ..., b_i_n]
Edit
I need to do it in place (i.e., retain the object_id of a, b). The two answers given so far is useful in that, given the sorted arrays:
a_sorted
b_sorted
I can do
a.replace(a_sorted)
b.replace(b_sorted)
but if possible, I want to do it directly. If not, I will accept one of the answers already given.
One approach would be to zip the two arrays together and sort them at the same time. Something like this, perhaps?
a = [1, 2, 3, 4, 5]
b = %w(a b c d e)
a,b = a.zip(b).sort_by { rand }.transpose
p a #=> [3, 5, 2, 4, 1]
p b #=> ["c", "e", "b", "d", "a"]
How about:
ary_a = [ 3, 1, 2] # => [3, 1, 2]
ary_b = [ 'a', 'b', 'c'] # => ["a", "b", "c"]
ary_a.zip(ary_b).sort{ |a,b| a.first <=> b.first }.map{ |a,b| b } # => ["b", "c", "a"]
or
ary_a.zip(ary_b).sort_by(&:first).map{ |a,b| b } # => ["b", "c", "a"]
If the entries are unique, the following may work. I haven't tested it. This is partially copied from https://stackoverflow.com/a/4283318/38765
temporary_copy = a.sort_by{|a_i| some_condition(a_i)}
new_indexes = a.map {|a_i| temporary_copy.index(a_i)}
a.each_with_index.sort_by! do |element, i|
new_indexes[i]
end
b.each_with_index.sort_by! do |element, i|
new_indexes[i]
end

How do I do element-wise comparison of two arrays?

I have two arrays:
a = [1,2,3]
b = [1,4,3]
Is there an element-wise comparison method in Ruby such that I could do something like this:
a == b
returns:
[1,0,1] or something like [TRUE,FALSE,TRUE].
Here's one way that I can think of.
a = [1, 2, 3]
b = [1, 4, 3]
a.zip(b).map { |x, y| x == y } # [true, false, true]
You can also use .collect
a.zip(b).collect {|x,y| x==y }
=> [true, false, true]
a = [1,2,3]
b = [1,4,3]
a.zip(b).map { |pair| pair[0] <=> pair[1] }
=> [0, -1, 0]
The element-wise comparison is achieved with the zip Ruby Array object method.
a = [1,2,3]
b = [1,4,3]
a.zip(b)
=> [[1, 1], [2, 4], [3, 3]]
You can do something like this to get exactly what you want:
[1,2,3].zip([1,4,3]).map { |a,b| a == b }
=> [true, false, true]
This should do the trick:
array1.zip(array2).map { |a, b| a == b }
zip creates one array of pairs consisting of each element from both arrays at that position. Imagine gluing the two arrays side by side.
Try something like this :
#array1 = ['a', 'b', 'c', 'd', 'e']
#array2 = ['d', 'e', 'f', 'g', 'h']
#intersection = #array1 & #array2
#intersection should now be ['d', 'e'].
Intersection—Returns a new array containing elements common to the two arrays, with no duplicates.
You can even try some of the ruby tricks like the following :
array1 = ["x", "y", "z"]
array2 = ["w", "x", "y"]
array1 | array2 # Combine Arrays & Remove Duplicates(Union)
=> ["x", "y", "z", "w"]
array1 & array2 # Get Common Elements between Two Arrays(Intersection)
=> ["x", "y"]
array1 - array2 # Remove Any Elements from Array 1 that are
# contained in Array 2 (Difference)
=> ["z"]

Count sequential occurrences of element in ruby array

Given some array such as the following:
x = ['a', 'b', 'b', 'c', 'a', 'a', 'a']
I want to end up with something that shows how many times each element repeats sequentially. So maybe I end up with the following:
[['a', 1], ['b', 2], ['c', 1], ['a', 3]]
The structure of the results isn't that important... could be some other data types of needed.
1.9 has Enumerable#chunk for just this purpose:
x.chunk{|y| y}.map{|y, ys| [y, ys.length]}
This is not a general solution, but if you only need to match single characters, it can be done like this:
x.join.scan(/(\w)(\1*)/).map{|x| [x[0], x.join.length]}
Here's one line solution. The logic same as Matt suggested, though, works fine with nil's in front of x:
x.each_with_object([]) { |e, r| r[-1] && r[-1][0] == e ? r[-1][-1] +=1 : r << [e, 1] }
Here's my approach:
# Starting array
arr = [nil, nil, "a", "b", "b", "c", "a", "a", "a"]
# Array to hold final values as requested
counts = []
# Array of previous `count` element
previous = nil
arr.each do |letter|
# If this letter matches the last one we checked, increment count
if previous and previous[0] == letter
previous[1] += 1
# Otherwise push a new array for letter/count
else
previous = [letter, 1]
counts.push previous
end
end
I should note that this doesn't suffer from the same problem that Matt Sanders describes, since we're mindful of our first time through the iteration.

Resources