How to recalculate values when the new year comes, Ruby - ruby

I'm trying to write an app that calculates sick/vacation days and how much an employee has available in either category.
Next step is adding a method that every year on January first recalculates all the employee's available vacation/sick time (without scrubbing/overwriting the data from the previous year, as that information needs to be accessed).
I have an Employee model (which is posted below), which :has_many Furloughs (which have their own model which I won't post unless requested, but basically handles how the date ranges are calculated).
class Employee < ActiveRecord::Base
has_many :furloughs, :dependent => :destroy
def years_employed
(DateTime.now - hire_date).round / 365
end
def vacation_days
if years_employed < 1 && newbie
((12 - hire_date.month) * 0.8).ceil
elsif years_employed <= 6
10
elsif years_employed <= 16
years_employed + 4
else
20
end
end
def newbie
Time.now.year == hire_date.year
end
def sick_days
5.0
end
def vacation_days_used
self.furloughs.collect(&:vacation_duration).inject(0) { | memo, n | memo + n }
end
def sick_days_used
self.furloughs.collect(&:sick_duration).inject(0) { | memo, n | memo + n }
end
def remaining_vacation_days
vacation_days - vacation_days_used
end
def remaining_sick_days
sick_days - sick_days_used
end
end
On January 1st, vacation/sick_days and vacation/sick_days_used methods need to reset, and then I also need to add a rollover method like
def rollover
if some_method_determining_last_year(remaining_vacation_days) >= 2
2
else
remaining_vacation_days
end
end
that needs to add to the newly calculated total as well. I'd appreciate any thoughts on what how I should approach this.

A database solution would probably be the simplest way to go, but since you're already on a track of trying to store as little information as possible, I might have a solution for you. You already have a method for the number of vacation/sick days (probably really should be hours) an employee has. From there you can have a method that calculates the total vacation and sick days an employee has ever earned. Then it's just a simple calculation between the number of days they have used and the number of hours the have earned. You will probably have to add a filter to those methods for expired hours, but you get the idea. I also assume that vacation_days_used are days used for the entirety of their employment (can't tell since I can't see the furlough model):
def total_vacation_days_earned
# vacation_days has to be recalculate, probably should be a mapped function.
# for each year, how many vacation days did you earn, but this is a rough method
vacation_days * years_employed # can put cutoffs like, first 2 years employed, etc.
end
def total_sick_days_earned
sick_days * years_employed
end
def vacation_days_used
self.furloughs.collect(&:vacation_duration).inject(0) { | memo, n | memo + n }
end
def sick_days_used
self.furloughs.collect(&:sick_duration).inject(0) { | memo, n | memo + n }
end
def remaining_vacation_days
total_vacation_days_earned - vacation_days_used
end
def remaining_sick_days
total_sick_days_earned - sick_days_used
end
Now you shouldn't need a rollover method since at the beginning of the year each employee has a running total of time they have ever accrued minus a running total of time they have ever used.
Most employers will give you a fraction of the holidays you have accrued each payroll period, not upfront at the beginning of the year. You will probably eventually need to segment it out by payroll period as well as years_employed by payroll (i.e., Employee has been here 24 payroll cycles, or 2 years). You will need a more robust years_employed method, I've written up a gist that can be used as a starting point for a method that calculates years served as incremented calendar (not always 365 days) year from the hire_date. You could modify this to increment every two weeks, fifteen days, whatever the payroll cycle is. You could also add a service or additional model to handle payroll methods.

I would recommend extending the Employee model/schema to include two additional columns: remaining_vacation_days and sick_days.
Upon creation of Employee, set these values appropriately and decrement remaining_vacation_days and increment sick_days after a Furlough save (see after_save).
Finally, on Jan 1st use your vacation_days and sick_days methods to reset these values for the new year.

Related

Iterating over big arrays with limited memory and time of execution

I’m having trouble using Ruby to pass some tests that make the array too big and return an error.
Solution.rb: failed to allocate memory (NoMemoryError)
I have failed to pass it twice.
The problem is about scheduling meetings. The method receives two parameters in order: a matrix with all the first days that investors can meet in the company, and a matrix with all the last days.
For example:
firstDay = [1,5,10]
lastDay = [4,10,10]
This shows that the first investor will be able to find himself between the days 1..4, the second between the days 5..10 and the last one in 10..10.
I need to return the largest number of investors that the company will serve. In this case, all of them can be attended to, the first one on day 1, the second one on day 5, and the last one on day 10.
So far, the code works normally, but with some hidden tests with at least 1000 investors, the error I mentioned earlier appears.
Is there a best practice in Ruby to handle this?
My current code is:
def countMeetings(firstDay, lastDay)
GC::Profiler.enable
GC::Profiler.clear
first = firstDay.sort.first
last = lastDay.sort.last
available = []
#Construct the available days for meetings
firstDay.each_with_index do |d, i|
available.push((firstDay[i]..lastDay[i]).to_a)
end
available = available.flatten.uniq.sort
investors = {}
attended_day = []
attended_investor = []
#Construct a list of investor based in their first and last days
firstDay.each_index do |i|
investors[i+1] = (firstDay[i]..lastDay[i]).to_a
end
for day in available
investors.each do |key, value|
next if attended_investor.include?(key)
if value.include?(day)
next if attended_day.include?(day)
attended_day.push(day)
attended_investor.push(key)
end
end
end
attended_investor.size
end
Using Lazy as far as I could understand, I escaped the MemoryError, but I started receiving a runtime error:
Your code was not executed on time. Allowed time: 10s
And my code look like this:
def countMeetings(firstDay, lastDay)
loop_size = firstDay.size
first = firstDay.sort.first
last = lastDay.sort.last
daily_attendance = {}
(first..last).each do |day|
for ind in 0...loop_size
(firstDay[ind]..lastDay[ind]).lazy.each do |investor_day|
next if daily_attendance.has_value?(ind)
if investor_day == day
daily_attendance[day] = ind
end
end
end
end
daily_attendance.size
end
And it went through the cases with few investors. I thought about using multi-thread and the code became the following:
def countMeetings(firstDay, lastDay)
loop_size = firstDay.size
first = firstDay.sort.first
last = lastDay.sort.last
threads = []
daily_attendance = {}
(first..last).lazy.each_slice(25000) do |slice|
slice.each do |day|
threads << Thread.new do
for ind in 0...loop_size
(firstDay[ind]..lastDay[ind]).lazy.each do |investor_day|
next if daily_attendance.has_value?(ind)
if investor_day == day
daily_attendance[day] = ind
end
end
end
end
end
end
threads.each{|t| t.join}
daily_attendance.size
end
Unfortunately, it went back to the MemoryError.
This can be done without consuming any more memory than the range of days. The key is to avoid Arrays and keep things as Enumerators as much as possible.
First, rather than the awkward pair of Arrays that need to be converted into Ranges, pass in an Enumerable of Ranges. This both simplifies the method, and it allows it to be Lazy if the list of ranges is very large. It could be read from a file, fetched from a database or an API, or generated by another lazy enumerator. This saves you from requiring big arrays.
Here's an example using an Array of Ranges.
p count_meetings([(1..4), (5..10), (10..10)])
Or to demonstrate transforming your firstDay and lastDay Arrays into a lazy Enumerable of Ranges...
firstDays = [1,5,10]
lastDays = [4,10,10]
p count_meetings(
firstDays.lazy.zip(lastDays).map { |first,last|
(first..last)
}
)
firstDays.lazy makes everything that comes after lazy. .zip(lastDays) iterates through both Arrays in pairs: [1,4], [5,10], and [10,10]. Then we turn them into Ranges. Because it's lazy it will only map them as needed. This avoids making another big Array.
Now that's fixed, all we need to do is iterate over each Range and increment their attendance for the day.
def count_meetings(attendee_ranges)
# Make a Hash whose default values are 0.
daily_attendance = Hash.new(0)
# For each attendee
attendee_ranges.each { |range|
# For each day they will attend, add one to the attendance for that day.
range.each { |day| daily_attendance[day] += 1 }
}
# Get the day/attendance pair with the maximum value, and only return the value.
daily_attendance.max[1]
end
Memory growth is limited to how big the day range is. If the earliest attendee is on day 1 and the last is on day 1000 daily_attendance is just 1000 entries which is a long time for a conference.
And since you've built the whole Hash anyway, why waste it? Write one function that returns the full attendance, and another that extracts the max.
def count_meeting_attendance(attendee_ranges)
daily_attendance = Hash.new(0)
attendee_ranges.each { |range|
range.each { |day| daily_attendance[day] += 1 }
}
return daily_attendance
end
def max_meeting_attendance(*args)
count_meeting_attendance(*args).max[1]
end
Since this is an exercise and you're stuck with the wonky arguments, we can do the same trick and lazily zip firstDays and lastDays together and turn them into Ranges.
def count_meeting_attendance(firstDays, lastDays)
attendee_ranges = firstDays.lazy.zip(lastDays).map { |first,last|
(first..last)
}
daily_attendance = Hash.new(0)
attendee_ranges.each { |range|
range.each { |day| daily_attendance[day] += 1 }
}
return daily_attendance
end

Having a method update/reload its self everytime it gets called? Ruby

My long term goal is replicate this spreadsheet in a ruby program and then to a rails app.
Currently I am trying to make it determine which of the two debts has the highest interest then subtract the minimum amount from that debt as well as any extra amounts the person is willing to pay as well as subtracting the minimum value from the other debt.
example:
card = balance: $10,000, Minimum: $200, i% = 20%
loan = balance: $40,000, Minimum: $400, i% = 5%
payments made per month = $1000
In this case, the program would every month firstly take $600 ($1000 -($200 + $400) + $200) from the card until it's balance was 0 then take $1000 ($1000 - $400 + $400) until the loan was payed off and return how many months that would take.
Currently, I am trying to get the amount to subtract each month take into account the balance of the debt and have this update whenever the method is called - however this does not seem to be working and will stay at $400 for both debts (snowball_amount method). EDIT: missing method issue fixed. Needed to change attr_reader to attr_accessorAlso for some reason, when I pass a debt object into highest_interest, i'm getting an undefined method 'balance=' error. Would be grateful for some help with this!
Create a Debt Class
class Debt
def initialize(balance: b, monthly_payment: m, annual_interest_rate: a)
#balance = balance
#monthly_min = monthly_payment
#int_rate = annual_interest_rate
end
attr_reader :monthly_min, :balance, :int_rate
end
Create two debt objects
#debt1 = Debt.new(balance: 14000.0, monthly_payment: 200.0, annual_interest_rate: 0.06)
#debt2 = Debt.new(balance: 40000.0, monthly_payment: 400.0, annual_interest_rate: 0.08)
Put them into array
#debts_array = [#debt1, #debt2]
Set the amount the person is willing to pay each month
#payment = 1000.0
Determine how much extra is being payed i.e. #payment - each debts monthly minimum, only if that debt's balance is over 0
def snowball_amount
#payments_less_mins = #payment
#debts_array.each do |debt|
if debt.balance <= 0
#payments_less_mins
elsif debt.balance > 0
#payments_less_mins = #payments_less_mins - debt.monthly_min
end
end
puts #payments_less_mins
return #payments_less_mins
end
Method for calculating the balance of that debt for that month
def compounding_interest(balance, apr)
return balance * (1 + apr/12)**1
end
Determing how long it will take to pay the debt off. While the balance of the debt is above 0 firstly update the balance in line with the addition of interest, then subtract from the debt balance the minimum monthly payment and the snowball(extra amount) from the balance. Then set the debts balance to 0
def highest_interest(debt, snowball)
months_to_pay = 0
while debt.balance > 0
debt.balance = compounding_interest(debt.balance, debt.int_rate)
debt.balance = debt.balance - (debt.monthly_min + snowball)
months_to_pay += 1
end
debt.balance = 0
snowball_amount
puts months_to_pay
end
Determine which debt has the highest balance and then do the highest interest method on that debt.
def which_has_higher_interest
debts_array = #debts_array.sort{|i| i.int_rate}.reverse!
puts debts_array[0].balance
debts_array.each do |debt|
highest_interest(debt, snowball_amount)
end
end
Calling the which_has_higher_interest method
puts which_has_higher_interest
In lines 3, 4, and 7 of your highest_interest method, you are calling a method called balance= on a Debt object, but your Debt objects do not have such a method. You need to define it somehow, possibly by changing the line
attr_reader :monthly_min, :balance, :int_rate
to
attr_reader :monthly_min, :int_rate
attr_accessor :balance

Time-of-day range in Ruby?

I want to know if a time belongs to an schedule or another.
In my case is for calculate if the time is in night schedule or normal schedule.
I have arrived to this solution:
NIGHT = ["21:00", "06:00"]
def night?( date )
date_str = date.strftime( "%H:%M" )
date_str > NIGHT[0] || date_str < NIGHT[1]
end
But I think is not very elegant and also only works for this concrete case and not every time range.
(I've found several similar question is SO but all of them make reference to Date ranges no Time ranges)
Updated
Solution has to work for random time ranges not only for this concrete one. Let's say:
"05:00"-"10:00"
"23:00"-"01:00"
"01:00"-"01:10"
This is actually more or less how I would do it, except maybe a bit more concise:
def night?( date )
!("06:00"..."21:00").include?(date.strftime("%H:%M"))
end
or, if your schedule boundaries can remain on the hour:
def night?(date)
!((6...21).include? date.hour)
end
Note the ... - that means, basically, "day time is hour 6 to hour 21 but not including hour 21".
edit: here is a generic (and sadly much less pithy) solution:
class TimeRange
private
def coerce(time)
time.is_a? String and return time
return time.strftime("%H:%M")
end
public
def initialize(start,finish)
#start = coerce(start)
#finish = coerce(finish)
end
def include?(time)
time = coerce(time)
#start < #finish and return (#start..#finish).include?(time)
return !(#finish..#start).include?(time)
end
end
You can use it almost like a normal Range:
irb(main):013:0> TimeRange.new("02:00","01:00").include?(Time.mktime(2010,04,01,02,30))
=> true
irb(main):014:0> TimeRange.new("02:00","01:00").include?(Time.mktime(2010,04,01,01,30))
=> false
irb(main):015:0> TimeRange.new("01:00","02:00").include?(Time.mktime(2010,04,01,01,30))
=> true
irb(main):016:0> TimeRange.new("01:00","02:00").include?(Time.mktime(2010,04,01,02,30))
=> false
Note, the above class is ignorant about time zones.
In Rails 3.2 it has added Time.all_day and similars as a way of generating date ranges. I think you must see how it works. It may be useful.

Calculating a time before 1970 (fails with negative seconds)

I am new in ruby,and when I learn the Time class in ruby(In fact I follow the VTC video) I found something I can not make ,I want to caculate the born year of one person according to his age,
for example,when a person tell his age is "20",then I should caculate his born year.
class Person
attr_accessor :name,:age,:year_born
def initialize(name,age)
#name=name
#age=age
#year_born=(Time.now - age*31556962).year
end
def days_alive
#age*365
end
end
In the following code everything works well except the
#year_born=(Time.now - age*31556962).year
I got an error when I try
Person.new("name",43).year_born
which says:
ArgumentError: time must be positive
./person.rb:6:in `-'
./person.rb:6:in `initialize'
I know Time.now will return the seconds from 1970,that's to say
(2011-1970)<43
So the Time.now-43*31556962 return a invalid value,but I want to know how to implement my requirement?
According to Programming Ruby:
Time is an abstraction of dates and
times. Time is stored internally as
the number of seconds and microseconds
since the epoch, January 1, 1970 00:00
UTC. On some operating systems, this
offset is allowed to be negative. Also
see the Date library module on page
742. (emphasis mine)
Which implies that on some operating systems, the offset is not allowed to be negative. So any of us elderly folks who were born before 1970 may blow up your code. Also keep in mind you're actually calculating number_of_seconds_per_year * age_in_years, which won't be very accurate.
why not do it like this:
note that i am using
(Time.now.year - age)
and that i have year_born as a method.
class Person
attr_accessor :name,:age
def initialize(name,age)
#name=name
#age=age
end
def year_born
(Time.now.year - age)
end
def days_alive
#age*365
end
end
However do not store the age in your DB(if you are going to save this in your DB). Just save the birth date.
The problem is you're using Time, but should be using either Date or DateTime, which have a greater range. Date doesn't know about times, which might fit your application better since you want day granularity.
require 'date'
Date.today - 20 * 365 # => #<Date: 1991-05-07 (4896767/2,0,2299161)>
Date.today - 50 * 365 # => #<Date: 1961-05-14 (4874867/2,0,2299161)>
(Date.today - 50 * 365).year # => 1961

Iterate over Ruby Time object with delta

Is there a way to iterate over a Time range in Ruby, and set the delta?
Here is an idea of what I would like to do:
for hour in (start_time..end_time, hour)
hour #=> Time object set to hour
end
You can iterate over the Time objects, but it returns every second between the two. What I really need is a way to set the offset or delta (such as minute, hour, etc.)
Is this built in to Ruby, or is there a decent plugin available?
Prior to 1.9, you could use Range#step:
(start_time..end_time).step(3600) do |hour|
# ...
end
However, this strategy is quite slow since it would call Time#succ 3600 times. Instead,
as pointed out by dolzenko in his answer, a more efficient solution is to use a simple loop:
hour = start_time
while hour < end_time
# ...
hour += 3600
end
If you're using Rails you can replace 3600 with 1.hour, which is significantly more readable.
If your start_time and end_time are actually instances of Time class then the solution with using the Range#step would be extremely inefficient since it would iterate over every second in this range with Time#succ. If you convert your times to integers the simple addition will be used but this way you will end up with something like:
(start_time.to_i..end_time.to_i).step(3600) do |hour|
hour = Time.at(hour)
# ...
end
But this also can be done with simpler and more efficient (i.e. without all the type conversions) loop:
hour = start_time
begin
# ...
end while (hour += 3600) < end_time
Range#step method is very slow in this case. Use begin..end while, as dolzenko posted here.
You can define a new method:
def time_iterate(start_time, end_time, step, &block)
begin
yield(start_time)
end while (start_time += step) <= end_time
end
then,
start_time = Time.parse("2010/1/1")
end_time = Time.parse("2010/1/31")
time_iterate(start_time, end_time, 1.hour) do |t|
puts t
end
if in rails.

Resources