My apologies if this has been answered before or is obvious...did some searching here and on the Goog and couldn't find an answer.
I'm looking to sort an array of Providers by price and whether they are a preferred_provider? (true or false)
For instance in array p of Providers...
p1.price == 1, p1.preferred_provider? == false
p2.price == 2, p2.preferred_provider? == true
p2.price == 3, p3.preferred_provider? == true
I would like to p.sort_by and get:
[p2 p3 p1]
IAW
p.sort_by {|x| x.preferred_provider?, x.price }
does not work and gets...
undefined method `<=>' for false:FalseClass
Any suggestions on better ways to approach this problem?
Most languages provide sort functions that accept comparators for this sort of thing. In Ruby, this is just array.sort:
p.sort {|a, b| if (a.preferred_provider? == b.preferred_provider?
then a.price <=> b.price
elsif a.preferred_provider?
1
else -1
}
You could define a <=> on the Provider class to do what you want, and then sort using the Array.sort method (rather than Enumerable.sort_by). Here's a definition of <=> that I whipped up:
class Provider
def <=>(other)
if preferred_provider?
if other.preferred_provider?
#price <=> other.price
else
1
end
else
if other.preferred_provider?
-1
else
#price <=> other.price
end
end
end
end
Then, if you have your array p, you could just do p_sorted = p.sort.
(Note that I haven't tested this code, so there may be a few errors, but I think it serves to demonstrate the idea.)
Related
I have a table vehicles that has_one vehicle_size. The VehicleSize model has a column in the table size, a String. Here are examples of a size value: 12ft, 19ft, EV. The goal is to sort vehicles based on the size from the vehicle_sizes table.
Here is my current solution:
def order_by_size(resources)
return resources unless context.params[:by_size] == 'asc' || context.params[:by_size] == 'desc'
if context.params[:by_size] == 'desc'
resources.joins(:vehicle_size).group('vehicle_sizes.size').order('vehicle_sizes.size DESC')
else
resources.joins(:vehicle_size).group('vehicle_sizes.size').order('vehicle_sizes.size ASC')
end
end
The solution above performs sorting. First, however, I need to push all zero values to the end regardless if the order is desc or asc (* zero means EV or any other string without numbers).
I tried to sort records with .sort { ... }, but it returns an array instead of active relation, with is necessary for me.
Solution where I get an array with sort:
def order_by_size(resources)
return resources unless context.params[:by_size] == 'asc' || context.params[:by_size] == 'desc'
if context.params[:by_size] == 'desc'
resources.joins(:vehicle_size).group('vehicle_sizes.size').sort do |x, y|
if x.vehicle_size.size.to_i.zero?
1
elsif y.vehicle_size.size.to_i.zero?
-1
else
y.vehicle_size.size.to_i <=> x.vehicle_size.size.to_i
end
end
else
resources.joins(:vehicle_size).group('vehicle_sizes.size').sort do |x, y|
if x.vehicle_size.size.to_i.zero?
1
elsif y.vehicle_size.size.to_i.zero?
-1
else
x.vehicle_size.size.to_i <=> y.vehicle_size.size.to_i
end
end
end
end
How can I modify my first or second solution to return an active relation where all String (zeros) will be pushed to the end regardless of sorting? Am I missing something here?
Many thanks for considering my request.
VehicleSize.order(Arel.sql("size = 'EV', size"))
or
VehicleSize.order(Arel.sql("size = 'EV', size desc"))
This way records with size = EV will be last, but others will be sorted as you need
Result will be relation
If you need specify table name, you can use vehicle_sizes.size instead of size
If you have few values without number (EV and ED here) you can do something like this to avoid hardcode
zero_array = %w[EV ED]
VehicleSize.order(
VehicleSize.sanitize_sql_for_order([Arel.sql("size IN (?), size DESC"), zero_array])
)
You can add a new field to the order
vehicle_sizes.size = 0, vehicle_sizes.size DESC
Or
vehicle_sizes.size <> 0, vehicle_sizes.size DESC
How to combine this sort code for two models into one?
I have two model, Author and Book.
I get Author and Book data, sorted it, and in the end I concat this two data.
sorted_author = author.sort do |a, b|
if a.name.nil? || a.amount.nil?
-1
elsif b.name.nil? || b.amount.nil?
1
else
[a.name, a.amount] <=> [b.name, b.amount]
end
end
sorted_book = book.sort do |a, b|
if a.data.nil? || a.price.nil?
-1
elsif b.data.nil? || b.price.nil?
1
else
[a.data, a.price] <=> [b.data, b.price]
end
end
sorted_author.concat(sorted_book)
The problem is that the date will be sorted separately, I need two arrays to be sorted as one.
I can do something like this.
author.concat(book).sort_by do |a|
if a.instance_of? Author
[a.name, a.amount]
else
[a.data, a.price]
end
end
But if here, for example, the name, date, price are nil, then the sorting will end with an error.
To replicate the functionality you have (all authors, sorted, on top of all books, sorted,) you might use that nil is falsey in ruby.
author.concat(book).sort_by do |a|
if a.instance_of? Author
[1, a.name || -1, a.amount || -1]
else
[0, a.data || -1, a.price || -1]
end
end
Note that I put leading 1 for authors and 0 for books to preserve all the authors come before all the books.
I want to convert all the values in a nested hash to a utf8 compatible string. I initially thought this would be easy and something like deep_apply should be available for me to use, but I am unable to find anything this simple on a quick google and SO search.
I do not want to write (maintain) a method similar to the lines of Change values in a nested hash . Is there a native API implementation or a shorthand available for this or do I have to write my own method?
I ended up implementing my own approach, that is in no way perfect but works well for my use case and should be easy to maintain. Posting it here for reference to anyone who wants to try it out
def deep_apply object, klasses, &blk
if object.is_a? Array
object.map { |obj_ele| deep_apply(obj_ele, klasses, &blk) }
elsif object.is_a? Hash
object.update(object) {|_, value| deep_apply(value, klasses, &blk) }
elsif klasses.any? { |klass| object.is_a? klass }
blk.call(object)
else
object
end
end
usage:
=> pry(main)> deep_apply({a: [1, 2, "sadsad"]}, [String, Integer]) { |v| v.to_s + "asd" }
=> {:a=>["1asd", "2asd", "sadsadasd"]}
Interesting to learn of the deep_merge approach taken in the answer by "The F". Here is another approach which requires adding a few helper methods.
First, the helper methods:
From the top answer here (converting-a-nested-hash-into-a-flat-hash):
def flat_hash(h,f=[],g={})
return g.update({ f=>h }) unless h.is_a? Hash
h.each { |k,r| flat_hash(r,f+[k],g) }
g
end
From a Github repo called ruby-bury (this functionality was proposed to Ruby core, but rejected)
class Hash
def bury *args
if args.count < 2
raise ArgumentError.new("2 or more arguments required")
elsif args.count == 2
self[args[0]] = args[1]
else
arg = args.shift
self[arg] = {} unless self[arg]
self[arg].bury(*args) unless args.empty?
end
self
end
end
And then a method tying it together:
def change_all_values(hash, &blk)
# the next line makes the method "pure functional"
# but can be removed otherwise.
hash = Marshal.load(Marshal.dump(hash))
flat_hash(hash).each { |k,v| hash.bury(*(k + [blk.call(v)])) }
hash
end
A usage example:
irb(main):063:0> a = {a: 1, b: { c: 1 } }
=> {:a=>1, :b=>{:c=>1}}
irb(main):064:0> b = change_all_values(a) { |val| val + 1 }
=> {:a=>2, :b=>{:c=>2}}
irb(main):066:0> a
=> {:a=>1, :b=>{:c=>1}}
There is deep_merge
yourhash.deep_merge(yourhash) {|_,_,v| v.to_s}
Merge the hash with itself, inspect the value and call to_s on it.
This method requires require 'active_support/core_ext/hash' at the top of file if you are not using ruby on rails.
Obviously, you may handle the conversion of v inside the deep_merge as you like to meet your requirements.
In rails console:
2.3.0 :001 > h1 = { a: true, b: { c: [1, 2, 3] } }
=> {:a=>true, :b=>{:c=>[1, 2, 3]}}
2.3.0 :002 > h1.deep_merge(h1) { |_,_,v| v.to_s}
=> {:a=>"true", :b=>{:c=>"[1, 2, 3]"}}
Well, it's quite simple to write it - so why don't write your own and be absolutely sure how does it behave in all situations ;)
def to_utf8(h)
if h.is_a? String
return h.force_encoding('utf-8')
elsif h.is_a? Symbol
return h.to_s.force_encoding('utf-8').to_sym
elsif h.is_a? Numeric
return h
elsif h.is_a? Array
return h.map { |e| to_utf8(e) }.to_s
else
return h.to_s.force_encoding('utf-8')
end
return hash.to_a.map { |e| result.push(to_utf8(e[0], e[1])) }.to_h
end
You may want to check if all behavior and conversions are correct - and change it if necessary.
This question already has answers here:
What is the Ruby <=> (spaceship) operator?
(6 answers)
Closed 9 years ago.
What is the meaning of '<==>' in Ruby?
Example: The code comes from the following class that compares numbers in the format x.x.x,
def <==>(other)
# Some code here
end
The following code comes from this class that orders numbers like x.x.x,
class Version
attr_reader :fst, :snd, :trd
def initialize(version="")
v = version.split(".")
#fst = v[0].to_i
#snd = v[1].to_i
#trd = v[2].to_i
end
def <=>(other)
return #fst <=> other.fst if ((#fst <=> other.fst) != 0)
return #snd <=> other.snd if ((#snd <=> other.snd) != 0)
return #trd <=> other.trd if ((#trd <=> other.trd) != 0)
end
def self.sort
self.sort!{|a,b| a <=> b}
end
def to_s
#sorted = #fst.to_s + "." + #snd.to_s + "." + #trd.to_s
#Puts out "#{#sorted}".
end
end
That is the spaceship operator. However, it is actually <=> (not <==>).
Although that is not its official name, I'm sure, it's the most commonly used name for that operator. It is a comparison operator where
If other is less than self, return 1,
If other is equal to self, return 0
If other is greater than self, return -1
It is a powerful operator in that by just implementing this you can do sorting of your own type and participate in a lot of other niceties, like the Enumerable mixin.
Why don't you just try it out? By just typing in the code you posted, it is trivial to see for yourself that it doesn't mean anything, since <==> is not a valid method name in Ruby. The code you posted will just raise a SyntaxError.
I want to make a loop on a variable that can be altered inside of the loop.
first_var.sort.each do |first_id, first_value|
second_var.sort.each do |second_id, second_value_value|
difference = first_value - second_value
if difference >= 0
second_var.delete(second_id)
else
second_var[second_id] += first_value
if second_var[second_id] == 0
second_var.delete(second_id)
end
first_var.delete(first_id)
end
end
end
The idea behind this code is that I want to use it for calculating how much money a certain user is going to give some other user. Both of the variables contain hashes. The first_var is containing the users that will get money, and the second_var is containing the users that are going to pay. The loop is supposed to "fill up" a user that should get money, and when a user gets full, or a user is out of money, to just take it out of the loop, and continue filling up the rest of the users.
How do I do this, because this doesn't work?
Okay. What it looks like you have is two hashes, hence the "id, value" split.
If you are looping through arrays and you want to use the index of the array, you would want to use Array.each_index.
If you are looping through an Array of objects, and 'id' and 'value' are attributes, you only need to call some arbitrary block variable, not two.
Lets assume these are two hashes, H1 and H2, of equal length, with common keys. You want to do the following: if H1[key]value is > than H2[key]:value, remove key from H2, else, sum H1:value to H2:value and put the result in H2[key].
H1.each_key do |k|
if H1[k] > H2[k] then
H2.delete(k)
else
H2[k] = H2[k]+H1[k]
end
end
Assume you are looping through two arrays, and you want to sort them by value, and then if the value in A1[x] is greater than the value in A2[x], remove A2[x]. Else, sum A1[x] with A2[x].
b = a2.sort
a1.sort.each_index do |k|
if a1[k] > b[k]
b[k] = nil
else
b[k] = a1[k] + b[k]
end
end
a2 = b.compact
Based on the new info: you have a hash for payees and a hash for payers. Lets call them ees and ers just for convenience. The difficult part of this is that as you modify the ers hash, you might confuse the loop. One way to do this--poorly--is as follows.
e_keys = ees.keys
r_keys = ers.keys
e = 0
r = 0
until e == e_keys.length or r == r_keys.length
ees[e_keys[e]] = ees[e_keys[e]] + ers[r_keys[r]]
x = max_value - ees[e_keys[e]]
ers[r_keys[r]] = x >= 0 ? 0 : x.abs
ees[e_keys[e]] = [ees[e_keys[e]], max_value].min
if ers[r_keys[r]] == 0 then r+= 1 end
if ees[e_keys[e]] == max_value then e+=1 end
end
The reason I say that this is not a great solution is that I think there is a more "ruby" way to do this, but I'm not sure what it is. This does avoid any problems that modifying the hash you are iterating through might cause, however.
Do you mean?
some_value = 5
arrarr = [[],[1,2,5],[5,3],[2,5,7],[5,6,2,5]]
arrarr.each do |a|
a.delete(some_value)
end
arrarr now has the value [[], [1, 2], [3], [2, 7], [6, 2]]
I think you can sort of alter a variable inside such a loop but I would highly recommend against it. I'm guessing it's undefined behaviour.
here is what happened when I tried it
a.each do |x|
p x
a = []
end
prints
1
2
3
4
5
and a is [] at the end
while
a.each do |x|
p x
a = []
end
prints nothing
and a is [] at the end
If you can I'd try using
each/map/filter/select.ect. otherwise make a new array and looping through list a normally.
Or loop over numbers from x to y
1.upto(5).each do |n|
do_stuff_with(arr[n])
end
Assuming:
some_var = [1,2,3,4]
delete_if sounds like a viable candidate for this:
some_var.delete_if { |a| a == 1 }
p some_var
=> [2,3,4]