I am trying to write a payroll program and one of the payment methods is twice a month (the 15th and 30th and/or 31st). I am able to get the payment dates for the current month, but I need to get them printed for the following year. My code thus far is:
require "active_support/core_ext"
require 'date'
def twice_a_month_payment_schedule
middle_of_month = Date.today.at_end_of_month - 15
end_of_month = Date.today.at_end_of_month
one_year = Date.today.at_end_of_month + 365
starting_payment_date = Date.parse(start_date) ### start_date is an argument in the format of "%Y-%m-%d".
while starting_payment_date < one_year
if middle_of_month.saturday?
middle_of_month - 1
elsif middle_of_month.sunday?
middle_of_month - 2
elsif end_of_month.saturday?
end_of_month - 1
elsif end_of_month.sunday?
end_of_month - 2
end
starting_payment_date += 1
end
pay_dates << middle_of_month.strftime("%m-%d-%Y")
pay_dates << end_of_month.strftime("%m-%d-%Y")
pay_dates
end
Any advice would be appreciated!
Thanks
Why not generalize the helper to get the dates for any year/month:
require "active_support/core_ext"
require 'date'
def twice_a_month_payment_schedule(year, month)
base = Date.new(year, month)
middle_of_month = 14.days.since(base)
end_of_month = base.at_end_of_month
if middle_of_month.saturday?
middle_of_month - 1
elsif middle_of_month.sunday?
middle_of_month - 2
elsif end_of_month.saturday?
end_of_month - 1
elsif end_of_month.sunday?
end_of_month - 2
end
[middle_of_month.strftime("%m-%d-%Y"), end_of_month.strftime("%m-%d-%Y")]
end
puts "Jan 2014: #{twice_a_month_payment_schedule(2014, 1)}"
puts "Mar 2014: #{twice_a_month_payment_schedule(2014, 3)}"
puts "Aug 2014: #{twice_a_month_payment_schedule(2014, 8)}"
puts "May 2015: #{twice_a_month_payment_schedule(2015, 5)}"
puts "Feb 2024: #{twice_a_month_payment_schedule(2024, 2)}"
puts "Dec 2135: #{twice_a_month_payment_schedule(2135, 12)}"
Results:
⚡️ ruby derp.rb
Jan 2014: ["01-15-2014", "01-31-2014"]
Mar 2014: ["03-15-2014", "03-31-2014"]
Aug 2014: ["08-15-2014", "08-31-2014"]
May 2015: ["05-15-2015", "05-31-2015"]
Feb 2024: ["02-15-2024", "02-29-2024"]
Dec 2135: ["12-15-2135", "12-31-2135"]
This is a comment, not an answer. I wanted to point out a small problem with your code and also suggest how you can organize it a little better. I've taken #Nick's code and made a few changes.
I understand you do not want paydays to fall on weekends. Therefore, instead of:
if middle_of_month.saturday?
middle_of_month - 1
elsif middle_of_month.sunday?
middle_of_month - 2
elsif end_of_month.saturday?
end_of_month - 1
elsif end_of_month.sunday?
end_of_month - 2
end
you want:
if middle_of_month.saturday?
middle_of_month - 1
elsif middle_of_month.sunday?
middle_of_month - 2
end
if end_of_month.saturday?
end_of_month - 1
elsif end_of_month.sunday?
end_of_month - 2
end
As it stands, if both paydays initially fall on a weekend, only the mid-month payday will be changed. With this modification, the operations for each payday are nearly the same, so it makes sense to eliminate the repetitive bits. Here's how you might do it:
require "active_support/core_ext"
require 'date'
def twice_a_month_payment_schedule(year, month)
base = Date.new(year, month)
[check_and_format_payday(base+14),
check_and_format_payday(base.at_end_of_month)]
end
def check_and_format_payday(payday)
case
when payday.saturday?
payday -= 1
when payday.sunday?
payday -= 2
end
payday.strftime("%a %m-%d-%Y")
end
puts "Jan 2014: #{twice_a_month_payment_schedule(2014, 1)}"
# Jan 2014: ["Wed 01-15-2014", "Fri 01-31-2014"]
"Feb 2014: #{twice_a_month_payment_schedule(2014, 2)}"
# Feb 2014: ["Fri 02-14-2014", "Fri 02-28-2014"]
"Mar 2014: #{twice_a_month_payment_schedule(2014, 3)}"
# Mar 2014: ["Fri 03-14-2014", "Mon 03-31-2014"]
puts "Aug 2014: #{twice_a_month_payment_schedule(2014, 8)}"
# Aug 2014: ["Fri 08-15-2014", "Fri 08-29-2014"]
puts "May 2015: #{twice_a_month_payment_schedule(2015, 5)}"
# May 2015: ["Fri 05-15-2015", "Fri 05-29-2015"]
puts "Feb 2024: #{twice_a_month_payment_schedule(2024, 2)}"
# Feb 2024: ["Thu 02-15-2024", "Thu 02-29-2024"]
puts "Dec 2135: #{twice_a_month_payment_schedule(2135, 12)}"
# Dec 2135: ["Thu 12-15-2135", "Fri 12-30-2135"]
Notice that I changed the output format slightly to include the day-of-week.
require "active_support/core_ext"
is only needed for the method at_end_of_month. You could alternatively define it yourself:
class Date
def at_end_of_month
next_month - 1
end
end
Related
I'm trying to build in a calendar system into my app. The problem I'm currently on is once a month schedule. Basically if a person schedules on the second Wednesday of the month I want to automatically update the schedule to the second Wednesday of the next month. I've tried just adding + 1.month but that doesn't achieve exactly what I want. Because it moves it to the next month date not day of the week. Does anyone know how I can achieve this. Here is what I'm currently doing that is not right.
def call
schedule.update!(
end_at: end_at_next_month,
start_at: start_at_next_month,
status: "monthly",
arranged: true
)
end
private
def schedule
context.schedule
end
def end_at_next_month
schedule.end_at + 1.month
end
def start_at_next_month
schedule.start_at + 1.month
end
end
require 'date'
def date_of_nth_wday(month, year, wday, nth)
base_date = Date.new(year, month)
base_wday = base_date.wday
ret_date = base_date +
( wday > base_wday ? wday - base_wday : 7 - base_wday + wday) +
7*(nth-1)
ret_date.month == month ? ret_date : nil
end
Obtain the date of the second Sunday of August, 2017
date_of_nth_wday(8, 2017, 0, 2)
#=> #<Date: 2017-08-13 ((2457979j,0s,0n),+0s,2299161j)>
Obtain the date of the third Friday of August, 2017
date_of_nth_wday(8, 2017, 5, 3)
#=> #<Date: 2017-08-18 ((2457984j,0s,0n),+0s,2299161j)>
Obtain the date of the fifth Monday of August, 2017
date_of_nth_wday(8, 2017, 1, 5)
#=> nil (there are only 4 Mondays in August, 2017)
You can try something like this:
DAYS_MAPPING = { 0=>"Sunday",
1=>"Monday",
2=>"Tuesday",
3=>"Wednesday",
4=>"Thursday",
5=>"Friday",
6=>"Saturday" }
# Returns week of month in integer
# for example: 1 for matching day of 1st week of the month
def get_week_number(date)
week_number = date.strftime('%U').to_i - date.beginning_of_month.strftime('%U').to_i
offset = (Date.parse(DAYS_MAPPING[date.wday]) < date.beginning_of_month) ? 0 : 1
week_number + offset
end
def specific_day_of_next_month(schedule)
# wday => day of week in integer
# for example: 0 for Sunday; 1 for Monday & so on ...
wday = schedule.wday
week_number = get_week_number(schedule)
next_month = schedule + 1.month
days = (next_month.beginning_of_month..next_month.end_of_month).select { |d| d.wday == wday }
days[week_number-1]
end
Example 1:
schedule = Date.today
# => Sat, 01 Jul 2017 (First Saturday of July)
specific_day_of_next_month(schedule)
# => Sat, 05 Aug 2017 (First Saturday of August)
Example 2:
schedule = Date.new(2017, 7, 10)
# => Mon, 10 Jul 2017 (Second Monday of July)
specific_day_of_next_month(schedule)
# => Mon, 14 Aug 2017 (Second Monday of August)
Your title is different to your explanation in the text. With my example you don't get the last friday, but you could select the 1,2,3,4,5th wednesday (or any other day) of a month.
require 'date'
class Date
#~ class DateError < ArgumentError; end
#Get the third monday in march 2008: new_by_mday( 2008, 3, 1, 3)
#
#Based on http://forum.ruby-portal.de/viewtopic.php?f=1&t=6157
def self.new_by_mday(year, month, weekday, nr)
raise( ArgumentError, "No number for weekday/nr") unless weekday.respond_to?(:between?) and nr.respond_to?(:between?)
raise( ArgumentError, "Number not in Range 1..5: #{nr}") unless nr.between?(1,5)
raise( ArgumentError, "Weekday not between 0 (Sunday)and 6 (Saturday): #{nr}") unless weekday.between?(0,6)
day = (weekday-Date.new(year, month, 1).wday)%7 + (nr-1)*7 + 1
if nr == 5
lastday = (Date.new(year, (month)%12+1, 1)-1).day # each december has the same no. of days
raise "There are not 5 weekdays with number #{weekday} in month #{month}" if day > lastday
end
Date.new(year, month, day)
end
end
#2nd monday (weekday 1) in july
p Date.new_by_mday(2017,7,1,2)
If you look for a gem: date_tools supports ths function.
I am working on a project where I have to calculate the total previous expereince of an employee. So lets say an employee worked
Company 1 - 11th Feb 2008 to 23rd Feb 2010
Company 2 - 14 May 2010 to 17 Oct 2014
Company 3 - 22 Dec 2014 to 14 Jan 2017
I want to be able to Diff the dates for these three rows that will give me the difference in start and end dates in days, then add the three together to get the total number of days worked. But the problem I am running into is that I want to be able to show the total experience something like
7 years, 3 months and 14 days. In the Difference of time in words kind of format.
Any ideas on how this can be achieved.
Thanks
Here is a pure-Ruby answer.
Code
require 'date'
R1 = /
\s*-\s* # match a hypen optionally surrounded by whitespace
| # or
\s*to\s* # match 'to' optionally surrounded by whitespace
/x # free-spacing regex definition mode
R2 = /st|nd|rd|th # match one of the four pairs of letters
/x
def total_days(data)
data.lines.reduce(0) do |tot, str|
start_date, end_date = str.
chomp.
split(R1).
drop(1).
map { |s| Date.strptime(s.sub(R2, ''), '%d %b %Y') }
tot + (end_date - start_date).to_i
end
end
Example
data =<<-_
Company 1 - 11th Feb 2008 to 23rd Feb 2010
Company 2 - 14 May 2010 to 17 Oct 2014
Company 3 - 22 Dec 2014 to 14 Jan 2017
_
total_days(data)
#=> 3114
Explanation
See Date::strptime and (for date format codes) DateTime#strftime.
The steps are as follows.
a = data.lines
#=> ["Company 1 - 11th Feb 2008 to 23rd Feb 2010\n",
# "Company 2 - 14 May 2010 to 17 Oct 2014\n",
# "Company 3 - 22 Dec 2014 to 14 Jan 2017\n"]
The block variable tot is set to reduce's argument (0) and the first element of a is generated and passed to the block, becoming the value of the block variable str:
tot = 0
str = a.first
#=> "Company 1 - 11th Feb 2008 to 23rd Feb 2010\n"
The block calculation is now performed
a = str.chomp
#=> "Company 1 - 11th Feb 2008 to 23rd Feb 2010"
b = a.split(R1)
#=> ["Company 1", "11th Feb 2008", "23rd Feb 2010"]
c = b.drop(1)
#=> ["11th Feb 2008", "23rd Feb 2010"]
d = c.map { |s| Date.strptime(s.sub(R2, ''), '%d %b %Y') }
#=> [#<Date: 2008-02-11 ((2454508j,0s,0n),+0s,2299161j)>,
# #<Date: 2010-02-23 ((2455251j,0s,0n),+0s,2299161j)>]
In calculating d, the first element of c passed to the block is
s = c.first
# => "11th Feb 2008"
and the block calculation for that string is
g = s.sub(R2, '')
#=> "11 Feb 2008"
Date.strptime(g, '%d %b %Y')
#=> #<Date: 2008-02-11 ((2454508j,0s,0n),+0s,2299161j)>
Continuing,
start_date, end_date = d
#=> [#<Date: 2008-02-11 ((2454508j,0s,0n),+0s,2299161j)>,
# #<Date: 2010-02-23 ((2455251j,0s,0n),+0s,2299161j)>]
start_date
#=> #<Date: 2008-02-11 ((2454508j,0s,0n),+0s,2299161j)>
end_date
#=> #<Date: 2010-02-23 ((2455251j,0s,0n),+0s,2299161j)>
e = end_date - start_date
#=> (743/1) <rational>
f = e.to_i
#=> 743
tot + 743
#=> 743
f is number of days the person worked in the first job. The last value is the new value of the block variable tot, the cumulative number of days worked in all jobs processed so far.
The only thing that is wrong in this code is the return :P
How would you display how many Friday 13ths there in a year?
def unlucky_days(year)
require 'date'
start_date = Date.new(year)
end_date = Date.new(year+1)
my_fridays = [4]
thirteen = "13"
result = (start_date..end_date).to_a.select {|k| my_fridays.include?(k.wday) && thirteen.include?(k.strftime('%d'))}
result.length
end
I'd write:
require 'date'
(1..12).count { |month| Date.new(year, month, 13).friday? }
+1 to #MarkReed's comments. Also, why call .to_a on a range, and why use variables when the Date class in Ruby already has methods like .day and .friday? Here is how I would do it:
def unlucky_days(year)
s = Date.new(year, 1, 1)
e = Date.new(year, 12, 31)
((s...e).select {|d| d.friday? && d.day == 13 }).count
end
Your code is wrong on a few points.
Friday is weekday number 5, not 4.
Why [4].include?(n) instead of just n==4?
"13".include?("#{n}") is not just strange but incorrect, since it returns true for 1 and 3 as well as 13.
You can go cut down on the level of brute force by just looking at the twelve 13ths and counting how many are Fridays, rather than looking at all 365 or 366 days and seeing which of them are both 13th's and Fridays, as in #tokland's answer, reproduced here:
def unlucky_days(year)
(1..12).count { |month| Date.new(year, month, 13).friday? }
end
Or, since there are only 14 possibilities, you could also just use a prebuilt table:
# number of Friday the 13ths in a given year is given by
# UnluckyDays[weekday of Jan 1][0 if common, 1 if leap]
UnluckyDays = [ [2,3], [2,2], [2,1], [1,2], [3,2], [1,1], [1,1] ]
def unlucky_days(year)
UnluckyDays[Date.new(year,1,1).wday][Date.leap?(year) ? 1 : 0 ]
end
This is a variant of #Tokland's answer.
require 'date'
def count_em(year)
d = Date.new(year, 1, 13) << 1
12.times.count { (d >>= 1).friday? }
end
(2010..2016).each { |y| puts "%d Friday the 13ths in %s" % [count_em(y), y] }
# 1 Friday the 13ths in 2010
# 1 Friday the 13ths in 2011
# 3 Friday the 13ths in 2012
# 2 Friday the 13ths in 2013
# 1 Friday the 13ths in 2014
# 3 Friday the 13ths in 2015
# 1 Friday the 13ths in 2016
If this calculation (or one like it) were done often and performance was important, two hashes could be constructed, one for leap years, the other for non-leap years, with keys the day of week on which the first day of the year falls and the values the number of Friday the 13ths in such years.
I'm trying to build reporting for retail sales in a Rails app. I'm trying to write a Gem which can find the start and end dates of a retail month/quarter/season in a Retail Calendar
I'm having difficulty in determining the start of the year.
Ex: in 2014, the year starts on 2nd Feb. For 2015, it's 1st Feb and 2016 is 31st January.
Has anyone encountered such a requirement in Ruby or any other language.
require 'date'
# Class for 5-4-4, 4-5-4, and 4-4-5 calendars
class Calendar13
def initialize (type = 454, yr_end_mo = 1)
case type
when 544, 454, 445 then #type = type
else raise ArgumentError, "Bad calendar type #{type}"
end
if (1..12) === yr_end_mo then #yr_end_mo = yr_end_mo
else raise ArgumentError, "Bad year-end month #{yr_end_mo}"
end
end
# Return the ending date for a particular year
def end_of_year (year)
year += 1 unless #yr_end_mo == 12
year_end = Date.new year, #yr_end_mo, -1
wday = (year_end.wday + 1) % 7 # Saturday-origin day-of-week
# Advance or retreat to closest Saturday
if wday > 3 then year_end += 7 - wday
elsif wday > 0 then year_end -= wday
end
year_end
end
# Return starting date for a particular year
def start_of_year (year); end_of_year(year - 1) + 1; end
# Return starting date for a particular month
def start_of_month (year, month)
start = start_of_year(year) + ((month - 1) / 3).to_i * 91
case #type * 10 + (month - 1) % 3
when 4451, 4541 then start += 28
when 5441 then start += 35
when 4452 then start += 56
when 4542, 5442 then start += 63
end
start
end
# Return the ending date for a particular month
def end_of_month (year, month)
if month == 12 then end_of_year year
else start_of_month(year, month + 1) - 1
end
end
# Return the starting date for a particular quarter
def start_of_quarter (year, quarter)
start_of_month year, (quarter - 1) * 3 + 1
end
# Return the ending date for a particular quarter
def end_of_quarter (year, quarter)
if quarter == 4 then end_of_year year
else start_of_quarter(year, quarter + 1) - 1
end
end
# Return the number of weeks in a particular year
def weeks_in_year (year)
((start_of_year(year + 1) - start_of_year(year)) / 7).to_i
end
end
See also http://www.smythretail.com/general-retailing/how-to-set-up-a-4-5-4-calendar/.
I want to find the first weekday of the month after the 1st which also happens to fall on a weekday in Ruby, something like the following:
Min ( DayOfMonth in (2...8) && DayOfWeek in ( monday ... friday ) )
How can I state this appropriately?
We can simply start at the second day of the desired month, and keep going to the next day until we find a weekday. This will require 2 increments at most, since the worst case is when day 2 is a Saturday and 3 a Sunday. Our target then would be the 4th day. If Sunday is the 2nd day, our target is 3, and otherwise the desired day is the 2nd of the month. My first_wday method returns a Date object as well. You can use .day to get the day of the month, or .wday to get the day of the week on it.
require 'date'
def first_wday(date)
d = Date.new(date.year, date.month, 2)
d += 1 while d.wday == 6 || d.wday == 0 # Saturday or Sunday
d
end
(1..12).each do |month|
date = Date.new(2014, month, 1)
wday = first_wday(date)
puts wday.strftime('%9B %Y: %-d (%A)')
end
# January 2014: 2 (Thursday)
# February 2014: 3 (Monday)
# March 2014: 3 (Monday)
# April 2014: 2 (Wednesday)
# May 2014: 2 (Friday)
# June 2014: 2 (Monday)
# July 2014: 2 (Wednesday)
# August 2014: 4 (Monday)
# September 2014: 2 (Tuesday)
# October 2014: 2 (Thursday)
# November 2014: 3 (Monday)
# December 2014: 2 (Tuesday)
This would be one way of doing it.
Code
require 'date'
d = Date.today
d = (d-d.day+2).next_month
(d..d+2).find { |i| (1..5).cover?(i.wday) }.strftime("%m-%d-%Y %a")
Explanation
d = Date.today #=> #<Date:2014-04-28((2456776j,0s,0n),+0s,2299161j)>
d = (d-d.day+2) #=> #<Date:2014-04-02((2456750j,0s,0n),+0s,2299161j)>
d = d.next_month #=> #<Date:2014-05-02((2456780j,0s,0n),+0s,2299161j)>
d.month #=> 5
d.year #=> 2014
d.wday #=> 5 (Friday)
At least one of the first three days of the month is a weekday, so we need only consider the range (d..d+2). The first value of (d..d+2) passed to the block is d, so:
i = d #=> #<Date: 2014-05-02 ((2456780j,0s,0n),+0s,2299161j)>
i.wday #=> 5
As Date's week starts with 0 on Sunday, the five week days are 1..5:
(1..5).cover?(i.wday) #=> true
(1..5).cover?(4) #=> true
So
i.strftime("%m-%d-%Y %a") #=> "05-02-2014 Fri"
is returned.