Feature #12648
open`Enumerable#sort_by` with descending option
Added by sawa (Tsuyoshi Sawada) over 8 years ago. Updated over 2 years ago.
Description
I would like to pass an optional argument to Enumerable#sort_by
or Enumerable#sort_by!
to allow descending sort. When the sort key is singular, this could be done by passing a single optinal boolean variable that represents ascending when false
(default) and descending when true
:
[3, 1, 2].sort_by(&:itself) # => [1, 2, 3]
[3, 1, 2].sort_by(false, &:itself) # => [1, 2, 3]
[3, 1, 2].sort_by(true, &:itself) # => [3, 2, 1]
When there are multiple sort keys, corresponding numbers of arguments should be passed:
[3, 1, 2, 0].sort_by{|e| [e % 2, e]} # => [0, 2, 1, 3]
[3, 1, 2, 0].sort_by(false, false){|e| [e % 2, e]} # => [0, 2, 1, 3]
[3, 1, 2, 0].sort_by(false, true){|e| [e % 2, e]} # => [2, 0, 3, 1]
[3, 1, 2, 0].sort_by(true, false){|e| [e % 2, e]} # => [1, 3, 0, 2]
[3, 1, 2, 0].sort_by(true, true){|e| [e % 2, e]} # => [3, 1, 2, 0]
Updated by sawa (Tsuyoshi Sawada) over 8 years ago
When the number of arguments passed is less than the sort keys, sort should be ascended or descended at a higher array level.
[3, 1, 2, 0].sort_by{|e| [e % 2, e]} # => [0, 2, 1, 3]
[3, 1, 2, 0].sort_by(false){|e| [e % 2, e]} # => [0, 2, 1, 3]
[3, 1, 2, 0].sort_by(true){|e| [e % 2, e]} # => [3, 1, 2, 0]
In the last two examples above, the single argument false
or true
should describe ascending or descending sort of the array [e % 2, e]
as a whole.
Updated by duerst (Martin Dürst) over 8 years ago
On 2016/08/02 18:57, sawadatsuyoshi@gmail.com wrote:
Issue #12648 has been reported by Tsuyoshi Sawada.
Feature #12648:
Enumerable#sort_by
with descending option
https://bugs.ruby-lang.org/issues/12648
I have felt the need for such an additional argument (or something
similar) quite recently. But the examples with numbers aren't very
convincing, just changing
array.sort_by { |e| e }
to
array.sort_by { |e| -e }
will do the job. But there are many cases where that's not possible,
starting with strings.
Updated by nobu (Nobuyoshi Nakada) over 8 years ago
I prefer more descriptive option, e.g., enum.sort_by(:descend) {|e| e}
.
https://github.com/ruby/ruby/compare/trunk...nobu:feature/12648-sort_by-order
Updated by sawa (Tsuyoshi Sawada) over 8 years ago
Nobuyoshi Nakada wrote:
I prefer more descriptive option, e.g.,
enum.sort_by(:descend) {|e| e}
.https://github.com/ruby/ruby/compare/trunk...nobu:feature/12648-sort_by-order
That's good too.
Updated by knu (Akinori MUSHA) over 8 years ago
Maybe the shorter forms :asc
/ :desc
like in SQL would sound more familiar.
Updated by sawa (Tsuyoshi Sawada) over 8 years ago
Akinori MUSHA wrote:
Maybe the shorter forms
:asc
/:desc
like in SQL would sound more familiar.
Actually, I also had that in mind as one way to go.
Updated by MSP-Greg (Greg L) over 8 years ago
In concept, I agree, but, although it's common to return an array from the block, any object can be returned that supports <=>.
Hence, a better solution might be adding a method like sort_keys
or sort_key
, where an array is returned by the block, and an array is used as the single parameter for ascending/descending info. I might suggest, rather than true / false, or :asc / :desc, use 1 for ascending and -1 for descending.
If the parameter array is shorter, remaining keys could default to ascending, if it's longer, it's truncated.
Or (and maybe better), it could just raise an error if the arrays are different length.
Updated by nobu (Nobuyoshi Nakada) over 8 years ago
Greg L wrote:
Hence, a better solution might be adding a method like
sort_keys
orsort_key
, where an array is returned by the block, and an array is used as the single parameter for ascending/descending info.
Could you make clear what object these sort_key
/sort_keys
methods belong to?
The array to be sorted?
Or the returned object (it may not be an array) from the block?
I might suggest, rather than true / false, or :asc / :desc, use 1 for ascending and -1 for descending.
If the parameter array is shorter, remaining keys could default to ascending, if it's longer, it's truncated.
Or (and maybe better), it could just raise an error if the arrays are different length.
Do you mean enum.sort_by(-1) {...}
for descending?
Updated by MSP-Greg (Greg L) over 8 years ago
Nobuyoshi Nakada wrote:
Greg L wrote:
Hence, a better solution might be adding a method like
sort_keys
orsort_key
, where an array is returned by the block, and an array is used as the single parameter for ascending/descending info.Could you make clear what object these
sort_key
/sort_keys
methods belong to?
The array to be sorted?
Or the returned object (it may not be an array) from the block?
I mentioned the fact that an array is often not returned by the block, hence, my suggestion for adding a new method.
Sorry, I should have shown a signature, below would be a possibility. The example shows three sort keys, 1st and 3rd are ascending, 2nd is descending.
t = enum.sort_key([1, -1, 1]) { |x| [f(x), g(x), h(x)] }
Do you mean
enum.sort_by(-1) {...}
for descending?
Yes, but in an array, as above. IOW, both the block return and the single parameter must be arrays.
Updated by MSP-Greg (Greg L) over 8 years ago
Taking a step back, we are using arrays simply because that is a object that allows sorting via multiple criteria (if criteria a is equal, test with criteria b, etc).
It is also my understanding that sort_by creates arrays of [enum_item, sort_value], and we are also using an array for sort_value.
I'm not much for writing (or reading) c, but it would seem that a new method could use whatever structures were most efficient, regardless of the fact that arrays are used for the parameter and the block return.
Updated by matz (Yukihiro Matsumoto) over 8 years ago
I think we are talking about two things at once.
First, adding reverse (or descending) option to sort_by
.
I think it may be useful for some cases, but it's only slightly better than sort_by().reverse
.
Second, adding secondary key sort order to sort_by
.
It may be useful too for some cases, but it should be a separate method.
Do you have any name suggestion?
Matz.
Updated by MSP-Greg (Greg L) over 8 years ago
Yukihiro Matsumoto wrote:
I think we are talking about two things at once.
First, adding reverse (or descending) option to
sort_by
.
I think it may be useful for some cases, but it's only slightly better thansort_by().reverse
.
I would concur.
Second, adding secondary key sort order to
sort_by
.
Just to clarify, I'm not sure what you mean by 'adding secondary key sort order', key word being 'secondary'. Original poster (Tsuyoshi Sawada) stated 'When there are multiple sort keys', so (hopefully) sort key limit of more than two...
It may be useful too for some cases, but it should be a separate method.
Do you have any name suggestion?
Maybe something like
sort_keys([sort order values]) { |x| [f(x), g(x), ...] }
where [sort order values]
is an array of composed of 1
or -1
values. Most people would associate 1
with ascending and -1
with descending. I think true
and false
don't convey an order as well. As to 'rules' for whether the two arrays must be identical in length, or if not, what occurs, that's a lot of options.
At a minimum, I think [sort order values]
should be optional, in which case all keys would sort ascending. As to length, it might be easiest to raise an error unless both are the same length.
Sorry for the delay.
Updated by knu (Akinori MUSHA) over 7 years ago
How does sort_r_by
sound when we already have grep_v
?
Updated by stomar (Marcus Stollsteimer) over 7 years ago
Personally, I really do not like grep_v, or generally using options from command line tools in method names.
Updated by knu (Akinori MUSHA) about 7 years ago
Something like this can be a solution for sorting with multiple keys with separate ordering directions.
module Comparable
class SortableTuple < Array
include Comparable
attr_reader :orderings
def initialize(orderings)
# Adding keyword options like `allow_nil` (`:first`/`:last`) would be great.
replace orderings.map { |key, dir|
desc =
case dir
when :desc
true
when :asc
false
else
raise ArgumentError, "direction must be either :asc or :desc: #{dir.inspect}"
end
[key, desc]
}
end
def <=>(other)
if other.instance_of?(self.class)
other.each_with_index { |(b, desc), i|
a, = self[i]
case cmp = a <=> b
when Integer
return desc ? -cmp : cmp unless cmp.zero?
else
return cmp
end
}
end
end
end
def self.[](*args)
SortableTuple.new(*args)
end
end
require 'pp'
require 'time'
pp [
["banana", Date.parse("2017-10-03")],
["apple", Date.parse("2017-10-03")],
["grape", Date.parse("2017-10-02")],
["melon", Date.parse("2017-10-02")],
["orange", Date.parse("2017-10-01")],
["cherry", Date.parse("2017-10-01")],
].sort_by { |name, date| Comparable[date => :asc, name => :desc] }
# [["apple", #<Date: 2017-10-03 ((2458030j,0s,0n),+0s,2299161j)>],
# ["banana", #<Date: 2017-10-03 ((2458030j,0s,0n),+0s,2299161j)>],
# ["grape", #<Date: 2017-10-02 ((2458029j,0s,0n),+0s,2299161j)>],
# ["melon", #<Date: 2017-10-02 ((2458029j,0s,0n),+0s,2299161j)>],
# ["cherry", #<Date: 2017-10-01 ((2458028j,0s,0n),+0s,2299161j)>],
# ["orange", #<Date: 2017-10-01 ((2458028j,0s,0n),+0s,2299161j)>]]
Updated by knu (Akinori MUSHA) about 7 years ago
Another path could be to introduce the sense of "reversed object".
I don't yet have a good name for the method that wouldn't cause name clash, but here it goes.
module Comparable
class ReversedObject
include Comparable
def initialize(object)
@object = object
end
attr_reader :object
def <=>(other)
other.object <=> object if other.instance_of?(self.class)
end
end
def reversed
ReversedObject.new(self)
end
end
p ["aaa", "bbb", "ccc"].sort_by(&:reversed)
# ["ccc", "bbb", "aaa"]
require 'pp'
require 'time'
pp [
["banana", Date.parse("2017-10-03")],
["apple", Date.parse("2017-10-03")],
["grape", Date.parse("2017-10-02")],
["melon", Date.parse("2017-10-02")],
["orange", Date.parse("2017-10-01")],
["cherry", Date.parse("2017-10-01")],
].sort_by { |name, date| [date, name.reversed] }
# [["apple", #<Date: 2017-10-03 ((2458030j,0s,0n),+0s,2299161j)>],
# ["banana", #<Date: 2017-10-03 ((2458030j,0s,0n),+0s,2299161j)>],
# ["grape", #<Date: 2017-10-02 ((2458029j,0s,0n),+0s,2299161j)>],
# ["melon", #<Date: 2017-10-02 ((2458029j,0s,0n),+0s,2299161j)>],
# ["cherry", #<Date: 2017-10-01 ((2458028j,0s,0n),+0s,2299161j)>],
# ["orange", #<Date: 2017-10-01 ((2458028j,0s,0n),+0s,2299161j)>]]
Updated by mame (Yusuke Endoh) over 5 years ago
- Related to Feature #15725: Proposal: Add Array#reverse_sort, #revert_sort!, #reverse_sort_by, and #reverse_sort_by! added
Updated by ttanimichi (Tsukuru Tanimichi) over 2 years ago
It's a lot of work to write sort_by { -1 * _1.created_at.to_f }
when the target objects are Time
instances.
['foo', 'bar', 'baz'].map { search(_1) }.flatten.sort_by { -1 * _1.created_at.to_f }
def search(query)
client = Twitter::REST::Client.new do |config|
config.consumer_key = "YOUR_CONSUMER_KEY"
config.consumer_secret = "YOUR_CONSUMER_SECRET"
config.access_token = "YOUR_ACCESS_TOKEN"
config.access_token_secret = "YOUR_ACCESS_SECRET"
end
client.search(query, result_type: "recent").take(10)
end
$ ruby -e "-1 * Time.now"
-e:1:in `*': Time can't be coerced into Integer (TypeError)
from -e:1:in `<main>'