Push all zero values to the end - Ruby on Rails (Postgresql) - ruby-on-rails

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

Related

Sort membership tiers by names on index page

So I have a drivers (user table) which has a relationship with the subscriptions table. There are 3 different tiers available: Gold, Silver and a Free tier. What i want to do is group and order by tiers, so I'd have the golds together, silvers together etc in descending order.
What i have now in my controller:
class DriversController < ApplicationController
def index
order_subs = Driver.order_by_subs.all
def gold_drivers
Driver.select { |driver| driver.subscriptions.stripe_plan == 'price_xxx' || driver.subscriptions.stripe_plan == 'price_xxx'}
end
def silver_drivers
Driver.select { |driver| driver.subscriptions.stripe_plan == 'price_xxx' || driver.subscriptions.stripe_plan == 'price_xxx'}
end
def free_drivers
Driver.select { |driver| driver.subscriptions == 'null' || driver.subscriptions == ''}
end
#pagy, #drivers = pagy(
Driver.joins(:profile).select(
'drivers.*',
'(profiles.no_races + profiles.no_poles + profiles.no_podiums + profiles.no_wins) AS score'
).reorder(gold_drivers, silver_drivers, free_drivers, score),
page: params[:page],
items: 16
)
end
end
So my thoughts were I could select the records under a variable i.e gold_drivers and then add them as I would in the reorder section in the #pagy pagination section .reorder(gold_drivers, silver_drivers, free_drivers, score) At the moment when i run the page I get the error undefined method stripe_plan' for nil:NilClass` so i'm guessing it can't find the column. If it's a free user, they won't have a record in the subscription table. Thanks
EDIT: driver model
scope :is_gold, -> { where("drivers.subscriptions.stripe_plan == 'price_xxx'") || where("drivers.subscriptions.stripe_plan == 'price_xxx'") }
scope :is_silver, -> { where("drivers.subscriptions.stripe_plan == 'price_xxx'") || where("drivers.subscriptions.stripe_plan == 'price_xxx'") }
scope :is_null, -> { where("drivers.subscriptions.stripe_plan == ''") || where("drivers.subscriptions.stripe_plan" == null) }
scope :order_by_subs, -> { reorder(:is_gold, :is_silver, :is_null) }
I have to be honest your code is incredibly confusing. It looks like you can order by subs, but I guess you can't, I'm not sure what is working and what isn't looking at your code. I see what you are doing from our previous conversation and this is off. Like I said in my comment, I would focus on understanding the basics here before you dive into some of this stuff. I'm going to fix your method and explain along the way so it hopefully makes some more sense and either works, or shows you a path to getting it to work.
class DriversController < ApplicationController
def index
# because there is no # on this it is a local variable, it will not be available in the view. Wondering also why you didn't just use this for the select below. Also, if you can already order by subs why would you even need to select them, wasn't that the reason for selecting them like this in the first place?
order_subs = Driver.order_by_subs.all
# it is my understanding you want these in the view so we add the # symbol which makes it an instance variable and you access those variables in the view now
#gold_drivers = order_subs.select { |driver| driver.subscriptions.stripe_plan == 'price_xxx' || driver.subscriptions.stripe_plan == 'price_xxx'}
#silver_drivers = order_subs.select { |driver| driver.subscriptions.stripe_plan == 'price_xxx' || driver.subscriptions.stripe_plan == 'price_xxx'}
#free_drivers = order_subs.select { |driver| driver.subscriptions == 'null' || driver.subscriptions == ''}
# Not sure where your local variable 'score' is coming from, do you need to set that first?
#pagy, #drivers = pagy(
Driver.joins(:profile).select(
'drivers.*',
'(profiles.no_races + profiles.no_poles + profiles.no_podiums + profiles.no_wins) AS score'
).reorder(#gold_drivers, #silver_drivers, #free_drivers, score),
page: params[:page],
items: 16
)
end
end
O.k so now you are setting the drivers values as instance variables which can be accessed in your view.

How to combine the sorting of two arrays into one?

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.

Clean up messy code that query's based on multiple options

I'm using Rails, but the underlying question here applies more broadly. I have a report page on my web app that allows the user to specify what they're filtering on, and query the database based on those filters (MongoDB).
The data is based around hotels, the user must first select the regions of the hotels (state_one, state_two, state_three), then the statuses of the hotels (planning, under_construction, operational), then an optional criteria, price range (200, 300, 400). Users can select multiple of each of these options.
My way of doing this currently is to create an empty array, iterate through each region, and push the region into the array if the user selected that region. Then, I'm iterating through THAT array, and assessing the status of the hotels in those regions, if any hotel has the status the user has selected, then I'm adding that hotel to a new empty array. Then I do the same thing for price range.
This works, but the code is offensively messy, here's an example of the code:
def find_hotel
hotels = find_all_hotels
first_array = []
hotels.each do |hotel|
if params[:options][:region].include? 'state_one' and hotel.state == :one
first_array.push(hotel)
elsif params[:options][:region].include? 'state_two' and hotel.state == :two
first_array.push(hotel)
elsif params[:options][:region].include? 'state_three' and hotel.state == :three
first_array.push(hotel)
end
end
second_array = []
first_array.each do |hotel|
if params[:options][:region].include? 'planning' and hotel.status == :planning
first_array.push(hotel)
elsif params[:options][:region].include? 'under_construction' and hotel.status == :under_construction
first_array.push(hotel)
elsif params[:options][:region].include? 'operational' and hotel.status == :operational
first_array.push(hotel)
end
end
third_array = []
second_array.each do |hotel|
# More of the same here, this could go on forever
end
end
What are some better ways of achieving this?
How about this:
STATES = [:one, :two, :three]
STATUSES = [:planning, :under_construction, :operational]
PRICES = [200, 300, 400]
def find_hotel
region = params[:options][:region]
first_array = set_array(region, find_all_hotels, STATES, :state)
second_array = set_array(region, first_array, STATUSES, :status)
third_array = set_array(region, second_array, PRICES, :price_range)
end
def set_array(region, array, options, attribute)
array.each_with_object([]) do |element, result|
options.each do |option|
result << element if region.include?(option) && element[attribute] == option
end
end
end
UPDATE
Added attribute parameter to set_array in order to make the code work with your updated example.
Since second_array is empty, whatever you get by iterating over it (perhaps third_array) would also be empty.
def find_hotel
hotels = find_all_hotels
first_array = hotels
.select{|hotel| params[:options][:region].include?("state_#{hotel.state}")}
first_array += first_array
.select{|hotel| params[:options][:region].include?(hotel.status.to_s)}
second_array = third_array = []
...
end

Ruby on Rails - Change Order Behavior

I'm having a minor problem with how RoR behaves when I tell it to display a result in a certain order.
I have a table called categories that contains a code column. Values in this code column include 1, 6, 12A, and 12B. When I tell the system to display the result (a dropdown) by order according to a foreign key id number and then a code value, it lists the codes in the order of 1, 12A, 12B, and 6 (which ideally should be 1, 6, 12A, and 12B).
Here is the dropdown:
collection_select(:category, :category_id, Category.order(:award_id, :code), :id, :award_code_category)
I know part of the problem is the A and B part of those two codes, I can't treat them as strict integers (code is a string type in the table).
I would love any thoughts about this.
steakchaser's answer called on:
['1', '12B', '12A', '6']
would return
['1', '6', '12B', '12A']
You lose the ordering of the letters.
You could create a helper to do the sorting:
def self.sort_by_category_codes(categories)
sorted_categories = categories.sort do |cat1, cat2|
if cat1.award_id == cat2.award_id
# award id matches so compare by code
if cat1.code.to_i == cat2.code.to_i
# the leading numbers are the same (or no leading numbers)
# so compare alphabetically
cat1.code <=> cat2.code
else
# there was a leading number so sort numerically
cat1.code.to_i <=> cat2.code.to_i
end
else
# award ids were different so compare by them
cat1.award_id <=> cat2.award_id
end
end
return sorted_categories
end
Both ['1', '12A', '12B', '6'] and ['1', '12B', '12A', '6'] would return ['1', '6', '12A', '12B']
Then call:
collection_select(:category, :category_id, sort_by_category_codes(Category.all), :id, :award_code_category)
The only issue I see with my solution is that codes that start with letters such as just 'A' would be returned ahead of numbers. If you need 'A' to be returned after '1A' you'll need some extra logic in helper method.
You could use a regex (most flexible depending on the pattern you really need to find) as part of the sorting to extract out the numeric portion:
['1', '12A', '12B', '6'].sort{|c1, c2| c1[/\d*(?:\.\d+)?/].to_i <=> c2[/\d*(?:\.\d+)?/].to_i}
Deleting non-integers when sorting is also a little easier to read:
['1', '12A', '12B', '6'].sort{|c1, c2| c1.delete("^0-9.").to_i <=> c2.delete("^0-9.").to_i}
To sort an array of those values you would:
["1", "6", "12A", "12B"].sort do |x, y|
res = x.to_i <=> y.to_i
res = x <=> y if res == 0
res
end
To get the categories sorted in that order could do something like:
categories = Category.all.sort do |x, y|
res = x.code.to_i <=> y.code.to_i
res = x.code <=> y.code if res == 0
res
end
From your code I inferred that you may want to sort on award_id with a second order sort on code. That would look like:
categories = Category.all.sort do |x, y|
res = x.award_id <=> y.award_id
res = x.code.to_i <=> y.code.to_i if res == 0
res = x.code <=> y.code if res == 0
res
end

Sort Objects by Boolean values in Ruby

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.)

Resources