I wrote a method which sorts an array of hashes by given hash keys. The method should put nil
values at the end.
def sort(records, *attrs)
records.sort do |a,b|
result = 0
attrs.each do |attr|
unless a[attr] == b[attr]
result = if a[attr].nil?
1
elsif b[attr].nil?
-1
else
a[attr] <=> b[attr]
end
break
end
end
result
end
end
p sort([{:a => 1},{:a => nil},{:a => 2}], :a)
#=> [{:a=>1}, {:a=>2}, {:a=>nil}]
p sort([{:a => nil},{:a => 'x'},{:a => 'a'}], :a)
#=> [{:a=>"a"}, {:a=>"x"}, {:a=>nil}]
My solution looks quite complex. Is there a better way to achieve the ordering in Ruby?
3 Answers 3
Set Unions
Looking at your code, when you loop through the attrs, you are breaking after you find the first key that is in a
and in b
and in the passed attrs
. This is also known as the union
of the arrays. As such you can simplify the inner loop with:
(attrs & a.keys & b.keys).first
Shorthand if/elseif syntax
You can use the keyword then
in conjunction with if
and elsif
to cut down on the whitespace of your if-eslif chain. The syntax would look like:
if attr.nil? then 0
elsif a[attr].nil? then 1
elsif b[attr].nil? then -1
else a[attr] <=> b[attr] end
Putting it all together
def sort(records, *attrs)
records.sort do |a, b|
attr = (attrs & a.keys & b.keys).first
if attr.nil? then 0
elsif a[attr].nil? then 1
elsif b[attr].nil? then -1
else a[attr] <=> b[attr] end
end
end
-
\$\begingroup\$ Thanks. I replaced the union line with
attr = attrs.find { |e| a[e] != b[e] }
, which also works. \$\endgroup\$sschmeck– sschmeck2016年05月11日 14:50:03 +00:00Commented May 11, 2016 at 14:50
You can create temporary sorting columns:
def sort records, *attrs
records.sort_by do |h|
h.values_at(*attrs).map do |v|
v.nil? ? [2] : [1, v]
end
end
end
Here I added columns with values 1 or 2 to the left for higher priority -- could add to the right or even in between for more complex sorting.
-
2\$\begingroup\$ The -1,0,1 values are the the result of the manual
a<=>b
comparison happening in the sort block. They are the expected return values. \$\endgroup\$Zack– Zack2016年05月11日 19:42:19 +00:00Commented May 11, 2016 at 19:42 -
\$\begingroup\$ @Zack, oh, understood. Rarely using
#sort
I forgot about that. Thank you for note. \$\endgroup\$Nakilon– Nakilon2016年05月12日 01:27:04 +00:00Commented May 12, 2016 at 1:27
Thanks for the suggestions. After all I implemented it the following way. It combines the solutions of Zack and Nakilon.
def sort(records, *attrs)
records.sort do |a,b|
k = attrs.find { |e| a[e] != b[e] } # 1.
k ? [a[k] ? 0 : 1, a[k]] <=> [b[k] ? 0 : 1, b[k]] : 0 # 2.
end
end
- Select the attribute that differs (Similar to the union idea of Zack)
- Introduce a pseudo value for comparison with
nil
values (Taken from Nakilons post)
-
\$\begingroup\$ What's wrong why Nakilon's answer? Using
sort
is kind of clunky whensort_by
(a higher-level abstraction) can do the job just fine. \$\endgroup\$tokland– tokland2016年05月12日 07:26:59 +00:00Commented May 12, 2016 at 7:26 -
\$\begingroup\$ @tokland Nothing is wrong. It does the
nil
extra value for every attribute. When selecting the attribute that differs, you have to it just one time. It's only style not correctness. On the other hand, my solution has some redunancy (x[k] ? 0 : 1
) that Nakilon approach avoids. :-) \$\endgroup\$sschmeck– sschmeck2016年05月12日 08:21:11 +00:00Commented May 12, 2016 at 8:21 -
\$\begingroup\$ While shorter, I feel like this version is much harder to read and understand. \$\endgroup\$Zack– Zack2016年05月12日 12:17:15 +00:00Commented May 12, 2016 at 12:17