Validate price range overlap - ruby-on-rails

I'm working on an application where I'm entering multiple price ranges. I want to validate the price range to keep it from overlapping with another price range. I know how to check whether two arrays overlaps or not, e.g.,
a = [1.0, 2.0]
b = [2.0, 3.0]
a & b #=> true
I have two fields price_start and price_end, so no price range between these two fields should overlap with another
But here its a range, e.g. $1.0 - $10.0 then the next one $10.1 to $20, how we can implement this? please help! thanks

You can write a custom validation like this:
validates :price_range_must_not_overlap
private
def price_ranges_must_overlap
range = (price_start..price_end)
if self.class.
where('id <> ?', self.id)
where('(price_start BETWEEN :price_start AND :price_end) OR (price_end BETWEEN :price_start AND :price_end)',
{ :price_start => price_start,
:price_end => price_end }).any?
errors.add(:base, "Price range overlaps with an existing price range")(b)
end
end
The finder condition might be extracted into a scope.
Read more about this in the Rails guide: http://guides.rubyonrails.org/active_record_validations.html#custom-methods

Related

Is there a way to back a new Rails 5 attribute with two database columns

I'm looking at using the new Rails 5 attributes API for a custom data type, ideally storing the data in two database columns, one for the data value and one for some extra type information.
The Attributes API seems to be designed to work with just one database column and I'm wondering if I'm missing a way to use two columns.
Example
Imagine a Money object, with one decimal or integer column for value and one string column for currency code. I'd pass in my custom money object, store it two columns, and then reading it back would combine the two columns into a Money object.
I've considered serializing the value and currency into a single Postgres JSON column, but I want to be able to do fast SQL SUM queries and sorting on just the value columns, so this doesn't seem ideal.
Thanks in advance for any insight.
I guess you're thinking about creating a ValueObject within your model.
There is ActiveRecord::Aggregations for that. Example:
class Customer < ActiveRecord::Base
composed_of :balance, class_name: "Money", mapping: %w(balance amount)
end
class Money
include Comparable
attr_reader :amount, :currency
EXCHANGE_RATES = { "USD_TO_DKK" => 6 }
def initialize(amount, currency = "USD")
#amount, #currency = amount, currency
end
def exchange_to(other_currency)
exchanged_amount = (amount * EXCHANGE_RATES["#{currency}_TO_#{other_currency}"]).floor
Money.new(exchanged_amount, other_currency)
end
def ==(other_money)
amount == other_money.amount && currency == other_money.currency
end
def <=>(other_money)
if currency == other_money.currency
amount <=> other_money.amount
else
amount <=> other_money.exchange_to(currency).amount
end
end
end
Can't answer your question directly unfortunately, but your example got me thinking. the money-rails gem allows use of a separate currency column. Perhaps it would be worth it to dig thru that gem to see what they are doing behind the scenes.

Validate if new range overlaps existing range

I have a Range model which has a start_range and an end_range column.
My range.rb model:
class Range < ActiveRecord::Base
validate :range_valid
protected
def range_valid
range = Range.all
range.each do |f|
if (f.start_range..f.end_range).overlaps?(self.start_range..self.end_range)
self.errors.add(:base, 'range is already alloted')
break
end
end
end
end
This code takes the start_range and end_range (say 100 and 500) and matches all the database records if any range overlap (or say the two ranges must be completely exclusive ) with the range which the user have entered.
This code is working fine but this code is not feasible if there are millions of records stored in the database
Can anyone please tell me how can I match the overlapping of the range without using loop and fetching all the records by Range.all so that the code should be feasible for real time.
You can easily query Range to check if an existing range overlaps with the given range.
Range.where("end_date >= ?", start_of_a_range).where("start_date <= ?", end_of_a_range).count
To wrap this into a validator I'd first define a scope
range.rb
scope :in_range, ->(range) { where("end_date >= ?", range.first).where("start_date <= ?", range.last) }
And then add the validator:
validates :range_cannot_overlap
def range_cannot_overlap
if Range.in_range(start_range..end_range).count > 0
errors.add(:base, 'range is already alloted')
end
end

The smallest and the largest possible date

I am creating a filtering partial view, where user can pick a from-date and a to-date using a calendar. These dates are used then within model scope to perform SQL Where clause in database query. If a user does not pick one of dates, the default value should be assigned: minimal available date for from and maximal for to.
unless params[:from].blank? and params[:to].blank?
from = begin Date.parse(params[:from]) rescue ??? end
to = begin Date.parse(params[:to]) rescue ??? end
#model_instances = #model_instances.start_end from, to
end
(...)
scope :start_end, -> (start_date, end_date) { where('(:start_date <= "from" AND "from" <= :end_date ) OR' +
'(:start_date <= "to" AND "to" <= :end_date ) OR' +
'("from" <= :start_date AND :end_date <= "to")',
{start_date: start_date, end_date: end_date}) }
The from and to model Date attributes are also database fields in related table.
In Rails, Date class has a family of methods beginning_of (day, week, month, etc.), but there are no methods such beginning_of_time, end_of_time, Date.min, Date.max.
Are there any methods to obtain the minimal and maximal dates?
You could skip the condition on start and end dates if no params is given:
if params[:from].present? and params[:to].present?
#model_instances.start_end(params[:from], params[:to])
end
And then you will get results not depending on dates since no from and/or end dates have been filled.
You could compare ranges of dates to your model's values and setup default values if params not passed.
#setup default if desired, or you can skip if params are blank
from = params[:from] || default_from
to = params[:to] || default_to
#model_instances.start_end(from, to)
and your scope would look something like this if you used date ranges for activerecord
scope :start_end, ->(start_date, end_date){where(from: start_date..end_date, to:start_date..end_date)}

If same score then same rank in RAILS 3.2

Hi I have a ranking system wherein if they have same score or points then both users should have same rank.
I am getting it thru the index, but can't manage to make their indexes both equal if they have same score
user.rb
def get_rank
x = User.get_rank.index(self)
x ? (x + 1) : x
end
def self.get_rank
Response.joins(:answer).where("answers.correct is TRUE").map(&:user).uniq.sort_by(&:score).reject{|me| me.super_admin or me.questions.count < Question.count}.reverse
end
How can I make the users who have same scores to have just 1 similar rank.
E.g. if both users get 25 points, and 25 is the highest from the postings, then they must have the first rank.
Any workarounds will be appreciated
The question is rather confusing but I think you could make better use of the database functions. Maybe something like this works, since I don't know your full models, especially which object has the score of the user. I'm assuming its on the user object:
def get_rank
scores = User.select(:score).joins(:response, :answers).where(:answers => [:correct => true]).order('score DESC').group(:score).all
# example result: [24, 22, 21, 20 ...]
rank = scores.index(score) + 1
end
The result of that statement gives you a sorted array of all actually applied scores. Since you do know the current user's score, you can get the index of that score, which is also the rank number.

Rails searching with multiple conditions (if values are not empty)

Let's say I have a model Book with a field word_count, amongst potentially many other similar fields.
What is a good way for me to string together conditions in an "advanced search" of the database? In the above example, I'd have a search form with boxes for "word count between ___ and ___". If a user fills in the first box, then I want to return all books with word count greater than that value; likewise, if the user fills in the second box, then I want to return all books with word count less than that value. If both values are filled in, then I want to return word counts within that range.
Obviously if I do
Book.where(:word_count => <first value>..<second value>)
then this will break if only one of the fields was filled in. Is there any way to handle this problem elegantly? Keep in mind that there may be many similar search conditions, so I don't want to build separate queries for every possible combination.
Sorry if this question has been asked before, but searching the site hasn't yielded any useful results yet.
How about something like:
#books = Book
#books = #books.where("word_count >= ?", values[0]) if values[0].present?
#books = #books.where("word_count <= ?", values[1]) if values[1].present?
ActiveRecord will chain the where clauses
The only problem is that if values[0] && values[1] the query would not return anything if values[0] was greater than values[1].
For our advanced searching we create a filter object which encapsulates the activerecord queries into simple methods. It was originally based on this Thoughtbot post
A book filter could look something like this:
class BookFilter
def initialize
#relation = Book.scoped
end
def restrict(r)
minimum_word_count!(r[:first]) if r[:first].present?
maximum_word_count!(r[:second]) if r[:second].present?
recent! if r.try(:[], :recent) == '1'
#relation
end
protected
def recent!
where('created_at > ? ', 1.week.ago)
end
def minimum_word_count!(count)
where('word_count >= ? ', count)
end
def maximum_word_count!(count)
where('word_count <= ?', count)
end
def where(*a)
#relation = #relation.where(*a)
end
end
#to use
books = BookFilter.new.restrict(params)
Take a look at the ransack gem, which is the successor to the meta_search gem, which still seems to have the better documentation.
If you do want to roll your own, there's nothing preventing you from chaining clauses using the same attribute:
scope = Book
scope = scope.where("word_count >= ?", params[:first]) if params[:first]
scope = scope.where("word_count <= ?", params[:last]) if params[:last]
But it's really not necessary to roll your own search, there are plenty of ready solutions available as in the gems above.

Resources