How to handle NilClass elegantly - ruby-on-rails

I am trying to avoid NoMethod errors due to NilClass. My code looks like this:
#branded, #nonbranded, #unknown, #affiliate, #social, #referral, #paid, #direct, #email = [], [], [], [], [], [], [], [], []
count = 0
while count < 6
date = SDLW + count
#TODO There has to be a better way than this.
branded_check = Metric.first(start_date: date, end_date: date, source: 'branded')
nonbranded_check = Metric.first(start_date: date, end_date: date, source: 'nonbranded')
unknown_check = Metric.first(start_date: date, end_date: date, source: 'unknown')
affiliate_check = Metric.first(start_date: date, end_date: date, source: 'affiliate')
social_check = Metric.first(start_date: date, end_date: date, source: 'social')
referral_check = Metric.first(start_date: date, end_date: date, source: 'referral')
paid_check = Metric.first(start_date: date, end_date: date, source: 'paid')
direct_check = Metric.first(start_date: date, end_date: date, source: 'direct')
email_check = Metric.first(start_date: date, end_date: date, source: 'email')
branded_check = branded_check.nil? ? 0 : branded_check.visits
nonbranded_check = nonbranded_check.nil? ? 0 : nonbranded_check.visits
unknown_check = unknown_check.nil? ? 0 : unknown_check.visits
affiliate_check = affiliate_check.nil? ? 0 : affiliate_check.visits
social_check = social_check.nil? ? 0 : social_check.visits
referral_check = referral_check.nil? ? 0 : referral_check.visits
paid_check = paid_check.nil? ? 0 : paid_check.visits
direct_check = direct_check.nil? ? 0 : direct_check.visits
email_check = email_check.nil? ? 0 : email_check.visits
#branded << branded_check
#nonbranded << nonbranded_check
#unknown << unknown_check
#affiliate << affiliate_check
#social << social_check
#referral << referral_check
#paid << paid_check
#direct << direct_check
#email << email_check
count += 1
end
I am sure there has to be a cleaner, more concise way to do this. Despite googling and reading around I haven't been able to figure it out. Any ideas on how to refactor would be much appreciated.

Definitely, create a method to avoid to repeat yourself
def visits(tag, date)
check = Metric.first(start_date: date, end_date: date, source: tag)
check.present? ? check.visits : 0 # or 'check.nil? ? 0 : check.visits' if you prefer
end
And in your method
count = 0
while count < 6
date = SDLW + count
#branded << visits('branded', date)
#nonbranded << visits('nonbranded', date)
...
count += 1
end

Let's put calls to Metric.first in a block and set instance variables using instance_variable_set.
['branded', 'nonbranded', ...].each do |source|
visits = 0.upto(5).map do |count|
date = SDLW + count
metric = Metric.first(start_date: date, end_date: date, source: source)
if metric.nil? then 0 else metric.visits end
end
instance_variable_set "##{source}".to_sym, visits
end

In addition to the other solutions, the while-loop can probably be replaced by:
(SDLW...SDLW+6).each do |date|
# date = SDLW + count #This line is superfluous
#branded << visits('branded', date)
#or one of the other solutions
#etc
# count += 1 # remove this line
end

I've made only for one source just for example.
#branded, #nonbranded, #unknown, #affiliate, #social, #referral, #paid, #direct, #email = [], [], [], [], [], [], [], [], []
class Metric < ActiveRecord::Base
scope :dates, lambda { |start_date, end_date| where(:start_date => start_date, :end_date => end_date) }
scope :branded, lambda { where(:source => "branded") }
# ... other scopes
end
# ...
6.times do |count|
date = SDLW + count
by_date = Metric.dates(date, date)
#branded << by_date.branded.try(:visits).to_i
# ... other sources
end
update:
Refactored solution using instance_variable_set idea from Jan solution and Dave Newton request.
6.times do |count|
date = SDLW + count
[:branded, :nonbranded, :unknown, :affiliate, :social, :referral, :paid, :direct, :email].each do |source|
instance_variable_set :"##{source}", [] unless instance_variable_get :"##{source}"
visits = Metric.first(start_date: date, end_date: date, source: source).try(:visits).to_i
instance_variable_set :"##{source}", instance_variable_get(:"##{source}").push(visits)
end
end

Related

Ruby Cyclomatic and Perceived complexity

I have this service object that parses an excel file and persists the data in it. Rubocop is flagging it as:
app/services/csv_upload.rb:17:3: C: Metrics/AbcSize: Assignment Branch Condition size for parse_spreadsheet is too high. [<14, 34, 11> 38.38/35]
def parse_spreadsheet ...
^^^^^^^^^^^^^^^^^^^^^
app/services/csv_upload.rb:17:3: C: Metrics/CyclomaticComplexity: Cyclomatic complexity for parse_spreadsheet is too high. [10/6]
def parse_spreadsheet ...
^^^^^^^^^^^^^^^^^^^^^
app/services/csv_upload.rb:17:3: C: Metrics/PerceivedComplexity: Perceived complexity for parse_spreadsheet is too high. [11/7]
How do i fix this? How should I refactor this code?
The parse_spreadsheet method is where it is being flagged on.
require "spreadsheet"
class CsvUpload
attr_accessor :review
def initialize(file)
Spreadsheet.client_encoding = "UTF-8"
tempfile = File.open(file.tempfile)
#spreadsheet = Spreadsheet.open tempfile
#exception_sheet = #spreadsheet.worksheet 3
#review = []
parse_spreadsheet
end
def parse_spreadsheet
#exception_sheet.each_with_index 4 do |row, index|
employee_no = row[1]
department = row[2]
date = row[3].to_datetime
time_in = row[4].to_datetime unless row[4].nil?
time_out = row[5].to_datetime unless row[5].nil?
if department == "Night"
next_row = #exception_sheet.to_a[index + 5]
next_date = next_row[3].to_datetime
next_time_in = next_row[4].to_datetime unless next_row[4].nil?
start_time = set_date_time(date, time_out) unless time_out.nil?
end_time = set_date_time(next_date, next_time_in) unless next_time_in.nil?
else
start_time = set_date_time(date, time_in) unless time_in.nil?
end_time = set_date_time(date, time_out) unless time_out.nil?
end
#employee = Employee.find_by(employee_no: employee_no)
next if #employee.nil?
#attendance_item = AttendanceItem.create(
start_time: start_time, end_time: end_time, employee_id: #employee.id
)
end
end
def to_review
#review
end
private
def set_date_time(date, time)
DateTime.new(
date.year,
date.month,
date.day,
time.hour,
time.min,
time.sec,
"+0800"
).in_time_zone
end
end
I'd start by having a separate method for each of your items in #exception_sheet:
def parse_spreadsheet
#exception_sheet.each_with_index 4 do |row, index|
handle_exception_row(row, index)
end
end
def handle_exception_row(row, index)
employee_no = row[1]
department = row[2]
date = row[3].to_datetime
time_in = row[4].to_datetime unless row[4].nil?
time_out = row[5].to_datetime unless row[5].nil?
if department == "Night"
next_row = #exception_sheet.to_a[index + 5]
next_date = next_row[3].to_datetime
next_time_in = next_row[4].to_datetime unless next_row[4].nil?
start_time = set_date_time(date, time_out) unless time_out.nil?
end_time = set_date_time(next_date, next_time_in) unless next_time_in.nil?
else
start_time = set_date_time(date, time_in) unless time_in.nil?
end_time = set_date_time(date, time_out) unless time_out.nil?
end
#employee = Employee.find_by(employee_no: employee_no)
next if #employee.nil?
#attendance_item = AttendanceItem.create(
start_time: start_time, end_time: end_time, employee_id: #employee.id
)
end
end
That fixes the parse_spreadsheet method but creates new errors with handle_exception_row. Then stripping that down:
def handle_exception_row(row, index)
employee_no = row[1]
department = row[2]
date = row[3].to_datetime
time_in = row[4].to_datetime unless row[4].nil?
time_out = row[5].to_datetime unless row[5].nil?
handle_attendance_creation(employee_no: employee_no, department: department, date: date, time_in: time_in, time_out: time_out)
end
def handle_attendance_creation(employee_no:, department:, date:, time_in:, time_out:)
if department == "Night"
next_row = #exception_sheet.to_a[index + 5]
next_date = next_row[3].to_datetime
next_time_in = next_row[4].to_datetime unless next_row[4].nil?
start_time = set_date_time(date, time_out) unless time_out.nil?
end_time = set_date_time(next_date, next_time_in) unless next_time_in.nil?
else
start_time = set_date_time(date, time_in) unless time_in.nil?
end_time = set_date_time(date, time_out) unless time_out.nil?
end
#employee = Employee.find_by(employee_no: employee_no)
next if #employee.nil?
#attendance_item = AttendanceItem.create(
start_time: start_time, end_time: end_time, employee_id: #employee.id
)
end
end
You'll probably still get an error on handle_attendance_creation, but hopefully you can see where this is going. Strip out further smaller methods that take the required arguments and you'll get there

Callback not saving value

I am writing some tests for a relatively complicated action that I need to occur when a record is destroyed.
These are my CheckIn functions and a CheckIn has_one :weigh_in:
def weight_loss
prev_ci = CheckIn.joins(:weigh_in)
.where(client_id: self.client_id)
.where('week < ?', self.week)
.where('weigh_ins.current_weight IS NOT NULL')
.order('week desc').first()
return 0 if self.weigh_in.nil? or prev_ci.nil? or prev_ci.weigh_in.nil?
change = prev_ci.weigh_in.current_weight.to_f - self.weigh_in.current_weight.to_f
return change.round(2)
end
def prev_ci
prev_ci = CheckIn.joins(:weigh_in)
.where(client_id: self.client_id)
.where('week < ?', self.week)
.where('weigh_ins.current_weight IS NOT NULL')
.order('week desc').first()
return prev_ci
end
def post_ci
post_ci = CheckIn.joins(:weigh_in)
.where(client_id: self.client_id)
.where('week > ?', self.week)
.where('weigh_ins.current_weight IS NOT NULL')
.order('week desc').first()
return nil if post_ci.nil? or post_ci.weigh_in.nil?
return post_ci
end
In my WeighIn model I have the following callbacks:
class WeighIn < ActiveRecord::Base
belongs_to :check_in
before_save :add_weightloss
after_destroy :adjust_weightloss
def add_weightloss
self.weightloss = 0
self.weightloss = self.check_in.weight_loss
end
def adjust_weightloss
post_ci = self.check_in.post_ci
unless post_ci.nil?
post_ci.weigh_in.weightloss = 0
post_ci.weigh_in.weightloss = post_ci.weight_loss
# Prints the correct value to pass the test (39.7)
p 6, post_ci.weigh_in
post_ci.weigh_in.save!
end
end
end
But my final test (for deletion) still fails:
RSpec.describe WeighIn, type: :model do
it "before_save weigh_in" do
ci1 = CheckIn.create!(client_id: 1, program_id: 1, week: 1)
ci2 = CheckIn.create!(client_id: 1, program_id: 1, week: 2)
ci3 = CheckIn.create!(client_id: 1, program_id: 1, week: 3)
ci4 = CheckIn.create!(client_id: 1, program_id: 1, week: 4)
wi1 = WeighIn.create!(check_in_id: ci1.id, client_id: 1, date: Date.today, current_weight: 100)
wi2 = WeighIn.create!(check_in_id: ci2.id, client_id: 1, date: Date.today, current_weight: 90)
wi3 = WeighIn.create!(check_in_id: ci4.id, client_id: 1, date: Date.today, current_weight: 70.5)
# Verifies functionality of before save
expect(wi1.weightloss).to eq 0
expect(wi2.weightloss).to eq 10
expect(wi3.weightloss).to eq 19.5
# Verifies fucntionality of update
wi_params = { check_in_id: ci4.id, client_id: 1, date: Date.today, current_weight: 60.3 }
wi3.update(wi_params)
expect(wi3.weightloss).to eq 29.7
# Verifies functionality of destroy
wi2.destroy
# Prints incorrect, old value, 29.7
p 'in test', wi3
expect(wi3.weightloss).to eq 39.7
end
end
It seems a though the functions are working properly but the record reverts back or is never saved / updated?
Can you try this to reload the object to check for a change?
expect(wi3.reload.weightloss).to eq 39.7

Filter created_at with 3 functions by parsing DateTime rails

currently i am getting values from database with a query
created_ats = Snapshot.connection.select_all("SELECT created_at from snapshots WHERE snapshot_id >= '#{camera_id}_#{from_date}' AND snapshot_id <= '#{camera_id}_#{to_date}'")
This query is giving me all created_ats according to conditions.
i want to filter all these created_ats according to some inputs which users have been passed to database which are
days and times
"{"Monday":["3:0-7:0","15:0-17:30"],"Tuesday":[],"Wednesday":["11:0-16:0"],"Thursday":["5:0-10:0"],"Friday":["15:30-22:30"],"Saturday":[],"Sunday":[]}"
its just an example days and times are can be totally filled or partially.
there is a field named as INTERVAL. which is being considered as MINUTES.
Now what is the whole scenario, from query i am getting created_at which is general timestamps in ruby on rails.
i want to filter those created_at with the giving Days and timings provided along with everyday in info above. + adding X(interval) to that created at as well.
such as after refining between days and timings. we will have a list of created_ats so after that starting from first created_at will add x.minutes in that created at and will assure that (created_at+x.minutes) is also present in created_ats(which we got after days and hours refining), is YESS then save it else leave it.
what i have tried so far is this and its not working as i want it to work according to me it seems no error in that but still dont give me specific value
class SnapshotExtractor < ActiveRecord::Base
establish_connection "evercam_db_#{Rails.env}".to_sym
belongs_to :camera
require "rmega"
require "aws-sdk-v1"
require 'open-uri'
def self.connect_mega
storage = Rmega.login("#{ENV['MEGA_EMAIL']}", "#{ENV['MEGA_PASSWORD']}")
storage
end
def self.connect_bucket
access_key_id = "#{ENV['AWS_ACCESS_KEY']}"
secret_access_key = "#{ENV['AWS_SECRET_KEY']}"
s3 = AWS::S3.new(
access_key_id: access_key_id,
secret_access_key: secret_access_key,
)
bucket = s3.buckets["evercam-camera-assets"]
bucket
end
# def self.test
# snapshot_bucket = connect_bucket
# storage = connect_mega
# folder = storage.root.create_folder("dongi")
# s3_object = snapshot_bucket.objects["gpo-cam/snapshots/1452136326.jpg"]
# snap_url = s3_object.url_for(:get, {expires: 1.years.from_now, secure: true}).to_s
# File.open("formula.txt", 'w') { |file| file.write(snap_url) }
# open('image.jpg', 'wb') do |file|
# file << open(snap_url).read
# end
# folder.upload("image.jpg")
# end
def self.extract_snapshots
running = SnapshotExtractor.where(status: 1).any?
unless running
#snapshot_request = SnapshotExtractor.where(status: 0).first
#snapshot_request.update_attribute(:status, 1)
camera_id = #snapshot_request.camera_id
exid = Camera.find(camera_id).exid
mega_id = #snapshot_request.id
from_date = #snapshot_request.from_date.strftime("%Y%m%d")
to_date = #snapshot_request.to_date.strftime("%Y%m%d")
interval = #snapshot_request.interval
#days = ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"]
set_days = []
set_timings = []
index = 0
#days.each do |day|
if #snapshot_request.schedule[day].present?
set_days[index] = day
set_timings[index] = #snapshot_request.schedule[day]
index += 1
end
end
begin
created_ats = Snapshot.connection.select_all("SELECT created_at from snapshots WHERE snapshot_id >= '#{camera_id}_#{from_date}' AND snapshot_id <= '#{camera_id}_#{to_date}'")
created_at_spdays = refine_days(created_ats, set_days)
created_at_sptime = refine_times(created_at_spdays, set_timings, set_days)
created_at = refine_intervals(created_at_sptime, interval)
File.open("test.txt", 'w') { |file| file.write(created_at) }
storage = connect_mega
creatp = storage.root.create_folder("created_at")
creatp.upload("test.txt")
rescue => error
notify_airbrake(error)
end
begin
storage = connect_mega
snapshot_bucket = connect_bucket
new_folder = storage.root.create_folder("#{exid}")
folder = storage.nodes.find do |node|
node.type == :folder and node.name == "#{exid}"
end
folder.create_folder("#{mega_id}")
created_at.each do |snap|
snap_i = DateTime.parse(snap).to_i
s3_object = snapshot_bucket.objects["#{exid}/snapshots/#{snap_i}.jpg"]
if s3_object.exists?
snap_url = s3_object.url_for(:get, {expires: 1.years.from_now, secure: true}).to_s
File.open("formula_#{snap_i}.txt", 'w') { |file| file.write(snap_url) }
open('#{snap_i}.jpg', 'wb') do |file|
file << open(snap_url).read
end
folder.upload('#{snap_i}.jpg')
end
end
#snapshot_request.update_attribute(:status, 3)
rescue => error
error
end
end
# created_at
end
private
def self.refine_days(created_ats, days)
created_at = []
index = 0
created_ats.each do |single|
days.each do |day|
if day == Date.parse(single["created_at"]).strftime("%A")
created_at[index] = single["created_at"]
index += 1
end
end
end
created_at
end
def self.refine_times(created_ats, timings, days)
created_at = []
index = 0
day_index = 0
days_times = days.zip(timings.flatten)
one = 1
zero = 0
created_ats.each do |single|
days_times.each do |day_time|
if Date.parse(single).strftime("%A") == day_time[day_index]
start_time = DateTime.parse(day_time[one].split("-")[zero]).strftime("%H:%M")
end_time = DateTime.parse(day_time[one].split("-")[one]).strftime("%H:%M")
created_at_time = DateTime.parse(single).strftime("%H:%M")
if created_at_time >= start_time && created_at_time <= end_time
created_at[index] = single
index += 1
end
end
end
end
created_at
end
def self.refine_intervals(created_ats, interval)
created_at = [created_ats.first]
last_created_at = DateTime.parse(created_ats.last)
index = 1
index_for_dt = 0
length = created_ats.length
(1..length).each do |single|
if (DateTime.parse(created_at[index_for_dt]) + interval.minutes) <= last_created_at
temp = DateTime.parse(created_at[index_for_dt]) + interval.minutes
created_at[index] = temp.to_s
index_for_dt += 1
index += 1
end
end
created_at
end
end

What is the best way of categorising segments of a time period?

I am developing a staff rota system. For payroll I need to calculate the correct rate of pay depending on the date/time period the shift covers.
How can I check for various date periods (weekend, holidays, weekday) without using a long chain of conditional statements with lengthy, verbose conditions.
Given any time range (a shift):
eg. 2015-01-20 15:00 --> 2015-01-21 17:00
What would be the best (and most efficient way) of categorising segments of the this period?
I would like to know:
The period (if any) between 22:00 and 07:00 on any weekday (Monday -
Friday) evening.
The period (if any) falling between 08:00 on a Saturday and 22:00 on a Sunday.
The period (if any) falling on a public holiday (using the
holidays gem)
So my two questions then are:
1) Knowing that a time period (shift) could span a weekend (although I would prefer a solution that would support a span of many days), how do I calculate which date/time ranges to compare against?
2) Once I have determined the time periods (weekends, holidays etc) to compare against, how do I best determine the intersection of these periods and determine the duration of them?
I don't fully understand your question, so I've put together some code that is based on many assumptions about the problem you face. I hope some of the issues I've addressed, and the way I've dealt with them, may be helpful to you. For example, if a worker is still working when a shift ends, it is necessary to identify the next shift, which may be the next day.
You'll see that my code is very rough and poorly structured. I have many temporary variables that are there just to help you follow what's going on. In a real app, you might want to add some classes, more methods, etc. Also, assuming the data will be stored in a database, you might want to use SQL for some of the calculations.
First, what I've assumed to be the data.
Data
A list of holidays:
holidays = ["2015:04:05", "2015:04:06"]
Information for each employee, including the employee's job classification, with keys being the employee id:
employees = {
123 => { name: 'Bob', job: :worker_bee1 },
221 => { name: 'Sue', job: :worker_bee2 }
}
Groups of days having the same daily periods, with pay rates the same for all days of the group, for each job and period within the day, unless the day falls on a holiday:
day_groups = { weekdays: [1,2,3,4,5], weekends: [6,0] }
Information for each work period:
work_periods = {
weekday_graveyard: {
day_group: :weekdays,
start_time: "00:00",
end_time: "08:00" },
weekday_day: {
day_group: :weekdays,
start_time: "08:00",
end_time: "16:00" },
weekday_swing: {
day_group: :weekdays,
start_time: "16:00",
end_time: "24:00" },
weekend: {
day_group: :weekends,
start_time: "00:00",
end_time: "24:00" } }
A wage schedule by job, dialy period, for non-holidays and holidays:
wage_by_job = {
worker_bee1: {
weekday_graveyard: { standard: 30.00, holiday: 60.00 },
weekday_day: { standard: 20.00, holiday: 40.00 },
weekday_swing: { standard: 25.00, holiday: 50.00 },
weekend: { standard: 22.00, holiday: 44.00 }
},
worker_bee2: {
weekday_graveyard: { standard: 32.00, holiday: 64.00 },
weekday_day: { standard: 22.00, holiday: 44.00 },
weekday_swing: { standard: 27.00, holiday: 54.00 },
weekend: { standard: 24.00, holiday: 48.00 }
}
}
Hours worked by all employees during the pay period:
shifts_worked = [
{ id: 123, date: "2015:04:03", start_time: "15:30", end_time: "00:15" },
{ id: 221, date: "2015:04:04", start_time: "23:30", end_time: "08:30" },
{ id: 123, date: "2015:04:06", start_time: "08:00", end_time: "16:00" },
{ id: 221, date: "2015:04:06", start_time: "23:00", end_time: "07:00" },
{ id: 123, date: "2015:04:07", start_time: "00:00", end_time: "09:00" }
]
Helpers
require 'set'
require 'date'
def date_str_to_obj(date_str)
Date.strptime(date_str, '%Y:%m:%d')
end
date_str_to_obj("2015:04:04")
#=> #<Date: 2015-04-04 ((2457117j,0s,0n),+0s,2299161j)>
def next_day(day)
(day==6) ? 0 : day+1
end
next_day(6)
#=> 0
Convert holidays to date objects and store in a set for fast lookup:
#holiday_set = Set.new(holidays.map { |date_str|
date_str_to_obj(date_str) }.to_set)
#=> #<Set: {#<Date: 2015-04-05 ((2457118j,0s,0n),+0s,2299161j)>,
# #<Date: 2015-04-06 ((2457119j,0s,0n),+0s,2299161j)>}>
def is_holiday?(date)
#holiday_set.include?(date)
end
is_holiday?(date_str_to_obj("2015:04:04"))
#=> false
is_holiday?(date_str_to_obj("2015:04:05"))
#=> true
Map each day of the week to an element of day_groups:
#day_group_by_dow = day_groups.each_with_object({}) { |(period,days),h|
days.each { |d| h[d] = period } }
#=> {1=>:weekdays, 2=>:weekdays, 3=>:weekdays, 4=>:weekdays,
# 5=>:weekdays, 6=>:weekend, 0=>:weekend}
Map each element of day_groups to an array of work periods:
#work_periods_by_day_group = work_periods.each_with_object({}) { |(k,g),h|
h.update(g[:day_group]=>[k]) { |_,nwp,owp| nwp+owp } }
#=> {:weekdays=>[:weekday_graveyard, :weekday_day, :weekday_swing],
# :weekend=> [:weekends]}
Compute hours worked within a work period:
def start_and_end_times_to_hours(start_time, end_time)
(time_to_minutes_after_midnight(end_time) -
time_to_minutes_after_midnight(start_time))/60.0
end
start_and_end_times_to_hours("10:00", "14:30")
#=> 4.5
A helper for the previous method:
private
def time_to_minutes_after_midnight(time_str)
hrs, mins = time_str.scan(/(\d\d):(\d\d)/).first.map(&:to_i)
60 * hrs + mins
end
public
time_to_minutes_after_midnight("10:00")
#=> 600
time_to_minutes_after_midnight("14:30")
#=> 870
As indicated by the method name:
def date_and_time_to_current_period(date, time, work_periods)
day_grp = #day_group_by_dow[date.wday]
periods = #work_periods_by_day_group[day_grp]
periods.find do |per|
p = work_periods[per]
p[:start_time] <= time && time < p[:end_time]
end
end
date_and_time_to_current_period(date_str_to_obj("2015:04:03"),
#=> :weekday_swing
date_and_time_to_current_period(date_str_to_obj("2015:04:04"),
#=> :weekend
Lastly, another self-explanatory method:
def next_period_and_date_by_period_and_date(work_periods, period, date)
end_time = work_periods[period][:end_time]
if end_time == "24:00" # next_day
day_grp = #day_group_by_dow[next_day(date.wday)]
wp = #work_periods_by_day_group[day_grp]
[wp.find { |period| work_periods[period][:start_time]=="00:00" }, date+1]
else # same day
day_grp = work_periods[period][:day_group]
wp = #work_periods_by_day_group[day_grp]
[wp.find { |period| work_periods[period][:start_time]==end_time }, date]
end
end
next_period_and_date_by_period_and_date(work_periods,
:weekday_day, date_str_to_obj("2015:04:03"))
#=> [:weekday_swing, #<Date: 2015-04-03 ((2457116j,0s,0n),+0s,2299161j)>]
next_period_and_date_by_period_and_date(work_periods,
:weekday_swing, date_str_to_obj("2015:04:02"))
#=> [:weekday_graveyard, #<Date: 2015-04-03...+0s,2299161j)>]
next_period_and_date_by_period_and_date(work_periods,
:weekday_swing, date_str_to_obj("2015:04:03"))
#=> [:weekend, #<Date: 2015-04-04 ((2457117j,0s,0n),+0s,2299161j)>]
next_period_and_date_by_period_and_date(work_periods,
:weekday_swing, date_str_to_obj("2015:04:04"))
#=> [:weekend, #<Date: 2015-04-05 ((2457118j,0s,0n),+0s,2299161j)>]
Calculation of payroll
shifts_worked.each_with_object(Hash.new(0.0)) do |shift, payroll|
id = shift[:id]
date = date_str_to_obj(shift[:date])
start_time = shift[:start_time]
end_time = shift[:end_time]
wage_schedule = wage_by_job[employees[id][:job]]
curr_period = date_and_time_to_current_period(date, start_time, work_periods)
while true
end_time_in_period = work_periods[curr_period][:end_time]
end_time_in_period = end_time if
(end_time > start_time && end_time < end_time_in_period)
hours_in_period =
start_and_end_times_to_hours(start_time, end_time_in_period)
wage = wage_schedule[curr_period][is_holiday?(date) ? :holiday : :standard]
payroll[id] += (wage * hours_in_period).round(2)
break if end_time == end_time_in_period
curr_period, date =
next_period_and_date_by_period_and_date(work_periods,
curr_period, date)
start_time = work_periods[curr_period][:start_time]
end
end
#=> {123=>795.5, 221=>698.0}
I've used the following gem:
https://github.com/fnando/recurrence
I haven't done Holidays yet.
Requirement
class Requirement < ActiveRecord::Base
# Model with attributes:
# start - datetime
# end - datetime
# is_sleepin - boolean
def duration
self.end - self.start
end
def to_range
self.start..self.end
end
def spans_multiple_days?
self.end.to_date != self.start.to_date
end
end
Breakdown of duration of requirement (shift)
class Breakdown
attr_reader :requirement,
:standard_total_duration,
:weekend_total_duration,
:wakein_total_duration
def initialize(requirement)
#requirement = requirement
#rules = Array.new
#rules << Breakdown::StandardRule.new(self)
#rules << Breakdown::WeekendRule.new(self)
#standard_total_duration = components[:standard].total_duration
#weekend_total_duration = components[:weekend].total_duration
if #requirement.is_sleepin?
#standard_total_duration = 0
#weekend_total_duration = 0
end
# Following is a special set of rules for certain Locations where staff work
# If a requirement is_sleepin? that means duration is not counted so set to 0
if ['Home 1', 'Home 2'].include?(#requirement.home.name.strip) and
#requirement.spans_multiple_days? and not #requirement.is_sleepin?
#rules << Aspirations::Breakdown::WakeinRule.new(self)
#wakein_total_duration = components[:wakein].total_duration
#standard_total_duration = 0
#weekend_total_duration = 0
end
end
def components
#rules.hmap{ |k,v| [ k.to_sym, k ] }
end
end
Which uses Rules to specify which parts of a shifts duration should be categorised:
module Breakdown
class Rule
def initialize(breakdown)
#requirement = breakdown.requirement
end
def to_sym
# eg 'Breakdown::StandardRule' becomes :standard
self.class.name.split('::').last.gsub("Rule", "").downcase.to_sym
end
def periods
output = []
occurrences = rule.events.map{ |e| rule_time_range(e) }
occurrences.each do |o|
if (o.max > #requirement.start and #requirement.end > o.min)
output << (o & #requirement.to_range)
end
end
return output
end
def total_duration
periods.reduce(0) { |sum, p| sum + (p.max - p.min).to_i }
end
end
end
Example of a Rule (in this case a weekend rule)
module Breakdown
class WeekendRule < Breakdown::Rule
def period_expansion
# This is an attempt to safely ensure that a weekend period
# is detected even though the start date of the requirement
# may be on Sunday evening, maybe could be just 2 days
4.days
end
def period_range
(#requirement.start.to_date - period_expansion)..(#requirement.end.to_date + period_expansion)
end
def rule
Recurrence.new(:every => :week, :on => :saturday, :starts => period_range.min, :until => period_range.max)
end
def rule_time_range(date)
# Saturday 8am
start = date + Time.parse("08:00").seconds_since_midnight.seconds
_end = (date + 1.day) + Time.parse("22:00").seconds_since_midnight.seconds
start.._end
end
end
end
One possible approach might be to break a week up into individual chunks of, say, 15 minutes (depending on how much resolution you need). Instead of ranges of time that are somewhat hard to deal with, you can then work with sets of these chunks, which Ruby supports very nicely.
Number the chunks of time sequentially:
Monday 12:00 AM-12:15 AM = 0
Monday 12:15 AM-12:30 AM = 1
...
Sunday 11:45 PM-12:00 AM = 671
Then prepare sets of integers for holidays, weekends, each shift, etc., whatever you need. Except for the holidays, those are probably all constants.
For instance, for your weekend from Saturday 8 AM to Sunday 10 PM:
weekend = [ 512..663 ]
Similarly, represent each employee's attendance as a set. For instance, somebody who worked from 8 AM to noon on Monday and from 10 AM to 11 AM on Saturday would be:
attendance = [ 32..47, 520..523 ]
With this approach, you can use set intersections to figure out how many hours were on weekends:
weekendAttendance = weekend & attendance

Rails: How to loop through month?

I made a scope help me to select objects
scope :best_of_the_month, ->(year, month) do
time = Time.new(year, month)
start_time = time.beginning_of_month
end_time = time.end_of_month
where("created_at > ? AND created_at < ?", start_time, end_time).where("likes > ?", 15).where("rating > ?", 4.85).order_by_rating.to_a.uniq(&:author)
end
Then, I want to loop through this method, from 2014/1 to now. How can I do it?
Maybe something like this:
start_date = Date.create(2014,1).month
end_date = Date.today.month
#monthly_videos = []
(start_date..end_date).each do |year, month|
videos = Video.best_of_the_month(year, month)
#monthly_videos << videos
end
I find a solution here, How to Loop through Months in Ruby on Rails. But it seems about looping through the days. Not the month
With the best_of_the_month scope defined to take month and year as params, the following code should work:
date = Date.new(2014,1,1)
#monthly_videos = []
while true
videos = Video.best_of_the_month(date.year, date.month)
#monthly_videos << videos
date += 1.month
break if date == Date.today.beginning_of_month
end
You could use the Date#next_month method
date = Date.new(2014,1,1)
final_date = Date.new(Date.today.year, Date.today.month)
#monthly_video = []
loop do
#monthly_videos << Video.best_of_the_month(date.year, date.month)
break if date == final_date
date = date.next_month
end

Resources