Refactor this if else between block of Ruby/Rails code - ruby-on-rails

Here's the code. Basically, the user selects a billing day (the 1st of each month, or the 15th of each month). The start_date is when the "contract" begins, the expire_date is when it expires.
So, if today is the 3rd, and they want to be billed on the 15th, then simply go to the 15th day of the current month. However, if today is the 3rd and they want to be billed on the first, then get the 1st day of next month... etc.
if params[:billing_day] == 1 && start_date.day > 1
expire_date = start_date.at_beginning_of_month.next_month
elsif params[:billing_day] == 15 && start_date.day < 15
expire_date = start_date.change(:day => 15)
elsif params[:billing_day] == 15 && start_date.day > 15
expire_date = start_date.at_beginning_of_month.next_month.change(:day => 15)
else
expire_date = start_date.change(:day => params[:billing_day])
end
It just seems crazy, surely it can be simplified in Rails. Thanks!

I would write something along the lines of
expire_date = start_date.change(:day => params[:billing_day])
if expire_date <= start_date
expire_date += 1.month
end
You'd need to validate that a valid billing day was picked before this

Came up with this. But not sure if it's good idea
expire_date = if params[:billing_day] == 1 && start_date.day > 1
start_date.at_beginning_of_month.next_month
elsif params[:billing_day] == 15 && start_date.day < 15
start_date.change(:day => 15)
elsif params[:billing_day] == 15 && start_date.day > 15
start_date.at_beginning_of_month.next_month.change(:day => 15)
else
start_date.change(:day => params[:billing_day])
end
And what if params[:billing_day] == 15 && start_date.day == 15 ? What should happen?
Came up with this also, but it behaves a little different in the case of start_date.day == 15
expire_date = if params[:billing_day] == 1 && start_date.day > 1
start_date.at_beginning_of_month.next_month
elsif params[:billing_day] == 15
if start_date.day < 15
start_date.change(:day => 15)
else
start_date.at_beginning_of_month.next_month.change(:day => 15)
end
else
start_date.change(:day => params[:billing_day])
end
Also notice that in Rails it's good practice to put logic in models, and since you are using params[] you are doing it in the controller.

class ExpireDate
def initialize(billing_day, start_date)
#billing_day = billing_day
#start_date = start_date
end
def expires_on
if billing_day == 1
return billing_on_1st
elsif billing_day == 15
return billing_on_15th
else
raise "Unknown billing_day"
end
end
def billing_on_1st
if #start_date.day > 1
return #start_date.at_beginning_of_month.next_month
else
return #start_date.change(:day => #billing_day)
end
end
def billing_on_15th
if #start_date.day < 15
return #start_date.change(:day => 15)
else
return #start_date.at_beginning_of_month.next_month.change(:day => 15)
end
end
end
expire_date = ExpireDate.new(params[:billing_day], start_date).expires_on

Related

How to get start and end date from quarters?

I am building a rails 5 app.
I need to be able to get the dates that is from a current quarter. With that I mean the user will provide me with a selected quarter (1 to 4) and I will convert that number to a start and end date for that selected quarter. How can I do that?
This is how I tried it but it is good?
def quarter_date(quarter, year)
if quarter == 1
where(date_at: Time.parse("01-01-#{year}")..Time.parse("01-03-#{year}"))
elsif quarter == 2
where(date_at: Time.parse("01-04-#{year}")..Time.parse("01-06-#{year}"))
elsif quarter == 3
where(date_at: Time.parse("01-07-#{year}")..Time.parse("01-09-#{year}"))
elsif quarter == 4
where(date_at: Time.parse("01-10-#{year}")..Time.parse("01-12-#{year}"))
end
end
You mean something like this?
require 'date'
today = Date.today
=> #<Date: 2018-07-02 ((2458302j,0s,0n),+0s,2299161j)>
year = today.year
=> 2018
input = 3
start_date = Date.new(2018, input * 3 - 2, 1)
=> #<Date: 2018-07-01 ((2458301j,0s,0n),+0s,2299161j)>
end_date = Date.new(2018, input * 3 + 1, 1) - 1
=> #<Date: 2018-09-30 ((2458392j,0s,0n),+0s,2299161j)>
It returns the start and end dates for the given quarter of current year.
Update
Updated with method from your attempt:
def quarter_date_range(quarter, year)
start_date = Time.parse("#{year}-#{quarter * 3 - 2}-1")
end_date = (start_date + 2.months).end_of_month
where(date_at: start_date..end_date)
end

Any option to post-process returned value in long conditional, other than setting variables for each statement?

def some_method(x)
if x == 1
date = Date.today
elsif x == 5
date = Date.today + 2
else
date = Date.today - 2
end
date + 20
end
For visual clarity, is it possible somehow to omit date = for each statement and catch whatever the returned value is from the conditional and add 20 to it?
(The code is for example purpose, my own code has 10 if-statements.)
def some_method(x)
date = if x == 1
Date.today
elsif x == 5
Date.today + 2
else
Date.today - 2
end
date + 20
end
If you have 10 if statements it is probably better to refactor code using case-when like this:
def some_method(x)
date = case x
when 1; Date.today
when 5; Date.today + 2
else; Date.today - 2
end
date + 20
end

Moving logic from controller to model in rails 3?

I've been building a contest application, and I can easily tell I've been putting wayyyy too much logic in the controller. How can I go about switch this type of logic to the model? (whats important here isn't the logic itself - its far from finished - I'm just trying to understand how to move it out of the controller).
Controller:
def create
#person = Person.new(params[:person])
#yournum = rand(100)
#day = Day.find_by_id(1)
#prereg = Prereg.find_by_email(#person.email)
if #preg != nil
#person.last_name = #prereg.name
end
if #day.number == 1
if #yournum <= 25
#person.prize_id = 2
elsif #yournum > 25 && #yournum <=50
#person.prize_id = 1
elsif #yournum > 51 && #yournum <=75
#person.prize_id = 3
elsif #yournum > 76 && #yournum <=100
#person.prize_id = 4
end
elsif #day.number == 2
if #yournum <= 25
#person.prize_id = 2
elsif #yournum > 25 && #yournum <=50
#person.prize_id = 1
elsif #yournum > 51 && #yournum <=75
#person.prize_id = 3
elsif #yournum > 76 && #yournum <=100
#person.prize_id = 4
end
elsif #day.number == 3
if #yournum <= 50
#person.prize_id = 2
elsif #yournum > 51 && #yournum <=90
#person.prize_id = 1
elsif #yournum > 91 && #yournum <= 95
#person.prize_id = 3
elsif #yournum > 96 && #yournum <=100
#person.prize_id = 4
end
end
#person.save
redirect_to #person
end
Model:
class Person < ActiveRecord::Base
belongs_to :prize
end
Thanks!
Elliot
Indeed, that's a pretty ugly controller. As you say, the solution is easy: move all the logic to the model:
def create
#person = Person.new(params[:person])
#person.set_price
if #person.save
redirect_to #person
else
flash[:error] = ...
render :action => 'new'
end
end
class Person
def set_price
# your logic here
end
end
Note that:
Controller: you need to check if #person was actually saved (maybe some validation failed).
Model: If a person has always to be assigned a price on creation, then use a callback (before_validation, for example). Otherwise, call it from the controller as shown the code above.
class PersonsController < ApplicationController
respond_to :html
def create
#person = Person.new(params[:person])
if #person.save
respond_with #person
else
flash[:error] = 'Render error'
render :action => :new
end
end
end
class Person
before_create :method_name
def method_name
#Put whatever you want to happen before creation here
end
end

all values same sign validation

User should insert all the values either positive or negative.
How may i set same sign validation ?
Right i have written this on before_save ..
unless (self.alt_1 >= 0 && self.alt_2 >=0 && self.alt_3 >= 0 &&
self.alt_4 >= 0 && self.alt_5 >= 0 && self.alt_6 >= 0) ||
(self.alt_1 <= 0 && self.alt_2 <=0 && self.alt_3 <= 0 &&
self.alt_4 <= 0 && self.alt_5 <= 0 && self.alt_6 <= 0)
self.errors.add_to_base(_("All values sign should be same."))
end
first_sign = self.alt_1 <=> 0
(2..6).each do |n|
unless (self.send("alt_#{n}") <=> 0) == first_sign
errors.add_to_base(_("All values' signs should be same."))
break
end
end
With this method we first get the sign of alt_1, and then see if the signs of the rest of the elements (alt_2 through alt_6) match. As soon as we find one that doesn't match we add the validation error and stop. It will run a maximum of 6 iterations and a minimum of 2.
Another more clever, but less efficient method, is to use the handy method Enumerable#all?, which returns true if the block passed to it returns true for all elements:
range = 1..6
errors.add_to_base(_("All values' signs should be same.")) unless
range.all? {|n| self.send("alt_#{n}") >= 0 } ||
range.all? {|n| self.send("alt_#{n}") <= 0 }
Here we first check if all of the elements are greater than 0 and then if all of the elements are less than 0. This method iterates a maximum of 12 times and a minimum of 6.
Here's a slightly different approach for you:
irb(main):020:0> def all_same_sign?(ary)
irb(main):021:1> ary.map { |x| x <=> 0 }.each_cons(2).all? { |x| x[0] == x[1] }
irb(main):022:1> end
=> nil
irb(main):023:0> all_same_sign? [1,2,3]
=> true
irb(main):024:0> all_same_sign? [1,2,0]
=> false
irb(main):025:0> all_same_sign? [-1, -5]
=> true
We use the spaceship operator to obtain the sign of each number, and we make sure that each element has the same sign as the element following it. You could also rewrite it to be more lazy by doing
ary.each_cons(2).all? { |x| (x[0] <=> 0) == (x[1] <=> 0) }
but that's less readable in my opinion.
unless
[:<=, :>=].any? do |check|
# Check either <= or >= for all values
[self.alt1, self.alt2, self.alt3, self.alt4, self.alt5, self.alt6].all? do |v|
v.send(check, 0)
end
end
self.errors.add_to_base(_("All values sign should be same."))
end

Help refactoring this nasty Ruby if/else statement

So I have this big, hairy if/else statement. I pass a tracking number to it, and then it determines what type of tracking number it is.
How can I simplify this thing? Specifically wanting to reduce the number of lines of codes.
if num_length < 8
tracking_service = false
else
if number[1, 1] == 'Z'
tracking_service = 'ups'
elsif number[0, 1] == 'Q'
tracking_service = 'dhl'
elsif number[0, 2] == '96' && num_length == 22
tracking_service = 'fedex'
elsif number[0, 1] == 'H' && num_length == 11
tracking_service = 'ups'
elsif number[0, 1] == 'K' && num_length == 11
tracking_service = 'ups'
elsif num_length == 18 || num_length == 20
check_response(number)
else
case num_length
when 17
tracking_service = 'dhlgm'
when 13,20,22,30
tracking_service = 'usps'
when 12,15,19
tracking_service = 'fedex'
when 10,11
tracking_service = 'dhl'
else
tracking_service = false
end
end
end
Yes, I know. It's nasty.
Try this. I rewrote it using case and regular expressions. I also used :symbols instead of "strings" for the return values, but you can change that back.
tracking_service = case number
when /^.Z/ then :ups
when /^Q/ then :dhl
when /^96.{20}$/ then :fedex
when /^[HK].{10}$/ then :ups
else
check_response(number) if num_length == 18 || num_length == 20
case num_length
when 17 then :dhlgm
when 13, 20, 22, 30 then :usps
when 12, 15, 19 then :fedex
when 10, 11 then :dhl
else false
end
end
Depending on whether or not the tracking code is a ruby object, you could also put helper's in it's class definition:
class TrackingCode < String
# not sure if this makes sense for your use case
def ups?
self[1,1] == 'Z'
end
def dhl?
self[0,1] == 'Q'
end
def fedex?
self.length == 22 && self[0, 2] == '96'
end
# etc...
end
Then your conditional becomes:
if number.ups?
# ...
elsif number.dhl?
# ...
elseif number.fedex?
end
One simplified conditional where you are operating on the implied feature of the tracking code. Likewise, if you were to take a looping approach, your loop would also be cleaner:
%w(ups? dhl? fedex?).each do |is_code|
return if number.send(is_code)
end
or even:
%w(ups? dhl? fedex?).each do |is_code|
yield if number.send(is_code)
end
This method looks like it was written for speed. You can use a minhash as a substitute, but I think the code is fairly clean and doesn't require a refactor. Rubyists tend to be disgusted by needless structure, but oftentimes it's needed to model real-world situations and/or provides a performance boost. The keyword should be needless.
Whilst longer than jtbandes solution, you might like this as it's a bit more declarative:
class Condition
attr_reader :service_name, :predicate
def initialize(service_name, &block)
#service_name = service_name
#predicate = block
end
end
CONDITIONS = [
Condition.new('ups') { |n| n[1] == 'Z' },
Condition.new('dhl') { |n| n[0] == 'Q' },
Condition.new('fedex') { |n| n[0..1] == '96' && n.size == 22 },
Condition.new('ups') { |n| n[0] == 'H' && n.size == 11 },
Condition.new('ups') { |n| n[0] == 'K' && n.size == 11 },
Condition.new('dhlgm') { |n| n.size == 17 },
Condition.new('usps') { |n| [13, 20, 22, 30].include?(n.size) },
Condition.new('fedex') { |n| [12, 15, 19].include?(n.size) },
Condition.new('dhl') { |n| [10, 11].include?(n.size) },
]
def tracking_service(tracking_number)
result = CONDITIONS.find do |condition|
condition.predicate.call(tracking_number)
end
result.service_name if result
end
I haven't dealt with the check_response method call here as I feel you should probably handle that elsewhere (assuming it does something other than return a tracking service name).
I believe this is sufficiently complex to deserve its own method.
BTW, if the length is 20 then the original function returns whatever check_response(n) returns, yet then attempts (and will always fail) to return 'usps'.
#lenMap = Hash.new false
#lenMap[17] = 'dhlgm'
#lenMap[13] = #lenMap[20] = #lenMap[22] = #lenMap[30] = 'usps'
#lenMap[12] = #lenMap[15] = #lenMap[19] = 'fedex'
#lenMap[10] = #lenMap[11] = 'dhl'
def ts n
len = n.length
return false if len < 8
case n
when /^.Z/
return 'ups'
when /^Q/
return 'dhl'
when /^96....................$/
return 'fedex'
when /^[HK]..........$/
return 'ups'
end
return check_response n if len == 18 or len == 20
return #lenMap[len]
end
# test code...
def check_response n
return 'check 18/20 '
end
%w{ 1Zwhatever Qetcetcetc 9634567890123456789012 H2345678901
K2345678901 hownowhownowhownow hownowhownowhownow90
12345678901234567
1234567890123
12345678901234567890
1234567890123456789012
123456789012345678901234567890
123456789012
123456789012345
1234567890123456789
1234567890
12345678901 }.each do |s|
puts "%32s %s" % [s, (ts s).to_s]
end

Resources