Rails replace if/elseif block by guard - ruby-on-rails

I create a table of cash transactions inside of PDF using Prawn gem. To do so I iterate through the Hash with parsed_cash_transactions but before each is started I need to save the last_item of that Hash to check when I should display the summary table below the main table.
def transactions_row
last_item = parsed_cash_transactions.last[:position]
parsed_cash_transactions.each do |cash_transaction|
# some operations with cash_transaction item
table_end_position = cursor
if last_item == cash_transaction[:position] && table_end_position < 204
new_page
draw_gray_line if cash_transaction[:position].to_i.even?
elsif table_end_position < 15
new_page
draw_gray_line if cash_transaction[:position].to_i.even?
end
end
end
To deal with all requirements I've got if block below. I'm wondering is there a better, cleaner way to replace that if block? maybe I could use guard somehow?
if last_item == cash_transaction[:position] && table_end_position < 204
new_page
draw_gray_line if cash_transaction[:position].to_i.even?
elsif table_end_position < 15
new_page
draw_gray_line if cash_transaction[:position].to_i.even?
end

You could indeed use a guard clause, although it is not the most pretty, since it condition is pretty long.
def transactions_row
last_item = parsed_cash_transactions.last[:position]
parsed_cash_transactions.each do |cash_transaction|
# some operations with cash_transaction item
table_end_position = cursor
next unless last_item == cash_transaction[:position] && table_end_position < 204 ||
table_end_position < 15
new_page
draw_gray_line if cash_transaction[:position].to_i.even?
end
end

Related

How to rewrite this code snippet using Sandi Metz rules?

Supposing I have a Survey rails model that has_many: questions and a need to be able to resequence the questions...
Where would I put the renumber_questions method (Survey model, Question model, or other class, and why?) and how would this ugly method below best be written following the rules?
def renumber_questions
last_page = 0
new_page = 0
new_seq = 0
questions.unscope(:order).order(page: :asc, seq: :asc).each do |question|
if last_page != question.page
new_page = new_page + 1
end
last_page = question.page
new_seq = new_seq + 1
question.page = new_page
question.seq = new_seq
question.save
end
end
Put it on another class, because of Single Responsibility (mentioned in the link you gave).
Despite the large number of private methods we wrote, keeping classes short proved easy. It forced us to consider what the single responsibility of our class was, and what should be extracted.
class RenumberQuestions
def initialize(questions)
#questions = questions.unscope(:order).order(page: :asc, seq: :asc)
#last_page = 0
#new_page = 0
#new_seq = 0
end
def call
#questions.each do |question|
counting_var(question)
mutate_(question)
end
end
def counting_var(question)
#new_page += 1 if #last_page != question.page
#last_page = question.page
#new_seq += 1
end
def mutate_(question)
question.page = #new_page
question.seq = #new_seq
question.save
end
end
Sorry for the bad naming. I don't know what you do, so I just naming it as I understand. And you can make this more clean.

custom validation gives blank field after validation which is not right

I have a custom validation in my model like this:
class Appointment < ActiveRecord::Base
#VIRTUAL ATTRIBUTES
attr_accessor :start_date, :start_time, :duration
#RELATIONSHIPS
belongs_to :task
#VALIDATIONS
before_validation :convert_to_datetime
before_validation :dur
validates :duration, presence: true
validate :is_date_nil
validate :time_collision_validation, if: :is_appointments_not_empty
validate :check_time
after_save :save_start_date
def is_appointments_not_empty
Appointment.all.present?
end
def check_time
start_at = Time.parse("#{#start_date} #{#start_time}")
if start_at < Time.now
errors.add(:start_date, "Cannot input past times")
end
end
def convert_to_datetime
unless #start_date.blank? && #start_time.blank?
self.start_at = Time.parse("#{#start_date} #{#start_time}")
end
end
def dur
if #start_date.present? && #start_time.present? && #duration.present?
self.end_at = Time.parse("#{#start_date} #{#start_time}") + (#duration.to_f*60*60)
end
end
def time_collision_validation
appointments = Appointment.all
if #start_date.present? && #start_time.present? && duration == 0.to_s
start_at = Time.parse("#{#start_date} #{#start_time}")
end_at = Time.parse("#{#start_date} #{#start_time}") + (#duration.to_f*60*60)
appointments.each do |a|
if start_at <= a.end_at - (2*60*60) && start_at >= a.start_at - (1*60*60)
errors.add(:start_time)
errors.add(:start_date, "An appointment already
exists at #{a.start_at.strftime("%I:%M%p")} of #{a.start_at.strftime("%d/%m/%Y")}
to #{a.end_at.strftime("%I:%M%p")} of #{a.end_at.strftime("%d/%m/%Y")}.
Please select a different date or time.")
break
end
end
elsif #start_date.present? && #start_time.present? && duration.present?
start_at = Time.parse("#{#start_date} #{#start_time}")
end_at = Time.parse("#{#start_date} #{#start_time}") + (#duration.to_f*60*60)
appointments.each do |a|
if start_at <= a.end_at - (2*60*60) && a.start_at <= end_at
errors.add(:start_time)
errors.add(:start_date, "An appointment already
exists at #{a.start_at.strftime("%I:%M%p")} of #{a.start_at.strftime("%d/%m/%Y")}
to #{a.end_at.strftime("%I:%M%p")} of #{a.end_at.strftime("%d/%m/%Y")}.
Please select a different date or time.")
break
end
end
end
end
def is_date_nil
if #start_date.blank? && #start_time.blank?
errors.add(:start_date, "Start date can't be blank")
errors.add(:start_time, "Start time can't be blank")
end
if #start_date.blank? && #start_time.present?
errors.add(:start_date, "Start date can't be blank")
end
if #start_time.blank? && #start_date.present?
errors.add(:start_time, "Start time can't be blank")
end
end
def start_date=(date)
#start_date = Date.strptime(date, "%d/%m/%Y") if date.present?
end
# def save_start_date
# #start_date = Date.strptime(#start_date, "%d/%m/%Y") if #start_date.present?
# end
# def save_start_date
# #start_date = Date.parse(#start_date).strftime("%d/%m/%Y")if #start_date.present?
# end
def start_time=(time)
#start_time = Time.parse(time).strftime("%H:%M:%S") if time.present?
end
def duration=(duration)
#duration = duration if duration.present?
end
# def start_date
# #start_date.strftime("%d/%m/%Y") if start_at.present? # || start_at.strftime("%d/%m/%Y") if start_at.present?
# end
def start_date
unless #start_date.blank?
#start_date.strftime("%d/%m/%Y")
end
# start_at.strftime("%d/%m/%Y") if start_at.present?
end
def start_time
#start_time || start_at.strftime("%I:%M%p") if start_at.present?
end
# def duration
# #duration || 9
# end
end
After this time_collision_validation executes, the value fields are blank which I don't want because I'm concerned with UX. ie: start_date and start_time fields are blank.
When I checked the value attribute in input in the HTML source code, the value contains a date string. I wonder why it does not show in the field.
Can somebody help me with this and explain what is going on please? >.<
validate :time_collision_validation
def time_collision_validation
appointments = Appointment.all
if self.start_date.present? && self.start_time.present? && duration.present?
start_at = Time.parse("#{self.start_date} #{self.start_time}")
end_at = Time.parse("#{self.start_date} #{self.start_time}") + (self.duration.to_f.hours)
appointments.each do |appointment|
if duration == 0.to_s
duration_ok = start_at >= appointment.start_at - (1.hours)
else
duration_ok = appointment.start_at <= end_at
end
if start_at <= appointment.end_at - (2.hours) && duration_ok
errors.add(:start_time)
errors.add(:start_date, "An appointment already
exists at #{appointment.start_at.strftime("%I:%M%p")} of #{appointment.start_at.strftime("%d/%m/%Y")}
to #{appointment.end_at.strftime("%I:%M%p")} of #{appointment.end_at.strftime("%d/%m/%Y")}.
Please select a different date or time.")
break
end
end
end
end
Notes:
you variously refer to duration and self.duration. For readability i would always use self.duration rather than duration or self.duration as it makes it clear to the reader that you are talking about a method/field of the current object rather than a local variable. I've change all instances of referencing methods/fields of the current object to self.methodname
you had a lot of repetition shared between the two if cases. I've refactored these to avoid repetition.
2*60*60 is like saying 2.hours and the latter is much more readable.
what are start_date and start_time - are they strings or date/time objects?
rather than loading every appointment in your entire database, and cycling through them, it would be much more efficient to just search for a single colliding other appointment. if you find one then you can add the errors. I was tempted to do this here but it's not clear exactly what's going on with your database.

Wrapping 'next' and 'previous' functions

In my Rails 4 app, I defined functions in my model than get the (nth) next or previous row in de database, wrapping around the entire database, so that Item.last.next will refer to Item.first:
def next(n=0)
following = Item.where("id > ?", self.id).order("id asc") + Item.where("id < ?", self.id).order("id asc")
following[n % following.length]
end
def prev(n=0)
n = n % Item.count-1
previous = Item.where("id < ?", self.id).order("id desc") + Item.where("id > ?", self.id).order("id desc")
previous[n % previous.length]
end
This results in three database queries per method call, and I've learned to keep database queries to a minimum, so I wonder if there is a way do get this result with only one query.
What you are looking for seems a bit high level. So let's prepare the basic API at first.
def next(n=1)
self.class.where('id > ?', id).limit(n).order('id ASC')
end
def previous(n=1)
self.class.where('id > ?', id).limit(n).order('id DESC')
end
Then higher level methods
def next_recycle(n=1)
klass = self.class
return klass.first if (n = 1 && self == klass.last)
next(n)
end
def previous_recycle(n=1)
klass = self.class
return klass.last if (n == 1 && self == klass.first)
previous(n)
end
You can pick methods according to needs.

Adding a bitwise virtual column to a model

I'm building this RoR site on an existing database. The user model on database has a column called "secret", which is a bitwise integer that holds information of the columns user has set as secret (first name, last name, etc).
Variables are to the power of two, for example: last name = 1<<1 = 2, first name = 1<<2 = 4, email == 1<<3 = 8, etc. So if user has set first name & email as secret, the column value becomes 4+8 = 12.
Now, I'm trying to find a generalized way to implement these virtual columns into a Rails model. So that, I could do (just a dummy example, the point being, i want to retrieve & store the status):
if user.secret_email?
user.secret_name_last = true
user.secret_name_first = false
end
How to implement these virtual columns neatly to a model (without modifying the existing database)? Current I've got following. It works, but it's not neat. As I've got 20 secret columns, the code looks very ugly.
SECRET_NAME_LAST = (1 << 1) # 2
attr_accessible :secret_name_last
def secret_name_last; secret & SECRET_NAME_LAST > 0 unless secret.nil?; end
def secret_name_last=(value); secret_set_value(SECRET_NAME_LAST, value); end
SECRET_NAME_FIRST = (1 << 2) # 4
attr_accessible :secret_name_first
def secret_name_first; secret & SECRET_NAME_FIRST > 0 unless secret.nil?; end
def secret_name_first=(value); secret_set_value(SECRET_NAME_FIRST, value); end
SECRET_EMAIL = (1 << 3) # 8
attr_accessible :secret_email
def secret_email; secret & SECRET_EMAIL > 0 unless secret.nil?; end
def secret_email=(value); secret_set_value(SECRET_EMAIL, value); end
***snip (17 more)***
private
def secret_set_value(item, value)
if self.secret.nil?
self.secret = 0
end
if value == "1" || value == true || value == 1
# Add item to secret column (if it doesn't exist)
if self.secret & item == 0
self.secret += item
end
else
# Remove item from secret column (if it exists)
if self.secret & item > 0
self.secret -= item
end
end
end
It would be great of I could just do something like:
as_bitwise :secret_name_first, :column=>'secret', :value=>4
as_bitwise :secret_name_last, :column=>'secret', :value=>2
Or even,
as_bitwise :secret, { :secret_name_last=>4, :secret_name_first=>2 }
EDIT
Based on Brandan's excellent answer, this is what I've got currently:
module BitwiseColumn
extend ActiveSupport::Concern
module ClassMethods
def bitwise_column(*args)
mapping = args.extract_options!
column_name = args.shift
real_column_name = args.shift
logger.debug "Initializing bitwisecolumn, column: " + column_name.to_s
mapping.each_pair do |attribute, offset|
logger.debug "\tSetting a pair: offset: " + offset.to_s + ", " + attribute.to_s
mask = 2 ** offset
class_eval %{
attr_accessible :#{column_name}_#{attribute}
def #{column_name}_#{attribute}?
#{real_column_name} & #{mask} > 0 unless #{real_column_name}.nil?
end
def #{column_name}_#{attribute}=(value)
if self.#{real_column_name}.nil?
self.#{real_column_name} = 0
end
if value == "1" || value == true || value == 1
if self.#{real_column_name} & #{mask} == 0
self.#{real_column_name} += #{mask}
end
else
if self.#{real_column_name} & #{mask} > 0
self.#{real_column_name} -= #{mask}
end
end
end
}
end
end
end
end
This allows me to use:
bitwise_column :secret, :realsecretcolumnatdatabase, :name_last=>1, :name_first=>2, :email=>3, :picture=>5, :dob=>6, :place=>12
After that, I can call User.first.secret_name_last? etc.
You can use class_eval to DRY up your code quite a bit. I'd also suggest factoring this behavior into some kind of a module separate from your User class so that you can test it thoroughly and separately from other User-specific behavior.
Like you, I tend to start these kinds of tasks with the desired API and work backwards. I started with this in my model:
class User < ActiveRecord::Base
include BitwiseColumn
bitwise_column :secret, :first_name => 1, :last_name => 2
end
The hash passed to bitwise_column maps the virtual attribute names to their mask value as an exponent. I felt like that was easier to manage than having to remember the powers of 2 myself :-)
Then I created the mixin:
module BitwiseColumn
extend ActiveSupport::Concern
module ClassMethods
def bitwise_column(*args)
mapping = args.extract_options!
column_name = args.shift
mapping.each_pair do |attribute, offset|
mask = 2 ** offset
class_eval %{
def secret_#{attribute}?
#{column_name} & #{mask} > 0 unless #{column_name}.nil?
end
def secret_#{attribute}=(value)
if self.#{column_name}.nil?
self.#{column_name} = 0
end
if value == "1" || value == true || value == 1
if self.#{column_name} & #{mask} == 0
self.#{column_name} += #{mask}
end
else
if self.#{column_name} & #{mask} > 0
self.#{column_name} -= #{mask}
end
end
end
}
end
end
end
end
This mixin creates two instance methods for each virtual attribute, one with a ? and one with a =, since that seems to be what you're after. I used your existing logic for the bitwise operations, which seems to work perfectly.

Rails: nil error where I am sure the object is not nil

This method increments the ActiveRecord attr_accessible attribute current_step:
def next_step
logger.debug "Now at step: " + current_step.inspect
if (current_step == nil)
current_step = 0
end
current_step = current_step + 1
end
At execution of the method, the log shows Now at step: 0 but the +1 line fails:
NoMethodError (You have a nil object when you didn't expect it!
You might have expected an instance of Array.
The error occurred while evaluating nil.+):
app/models/assessment.rb:16:in `next_step'
Is it a miracle? Is current_step nil or not?
EDIT: Thanks fl00r and whitequark for the great answers! Here is what the code was meant to be:
def next_step
current_step ||= 0
self.current_step += 1
save
end
Conditionals and loops do not have their own scope in Ruby; and here you have a variable and a method of self which possess the same name. In the condition of if, the current_step method is used, but in its body a local variable is defined, and all future references to the current_step will refer to the local variable. The pitfall you've encountered is, even if the if body is not executed, the local variable is still defined, and default value of nil is assigned to it.
I'll make it more clear by adding _M to the identifier when the method is accessed, and _L for the local variable.
def next_step
logger.debug "Now at step: " + current_step_M.inspect
if (current_step_M == nil)
current_step_L = 0
### this part is implicit:
# else
# current_step_L = nil
end
current_step_L = current_step_L + 1
end
I'm guessing you was actually trying to do self.current_step = 0, which would invoke the setter.
def next_step
current_step ||= 0
logger.debug "Now at step: " + current_step.inspect
current_step += 1
end

Resources