Skip to content

Instantly share code, notes, and snippets.

@molawson
Last active February 7, 2022 18:32
Show Gist options
  • Save molawson/3af26eedd0106d950ac1e39100368077 to your computer and use it in GitHub Desktop.
Save molawson/3af26eedd0106d950ac1e39100368077 to your computer and use it in GitHub Desktop.
require "bundler/inline"
gemfile do
source "https://rubygems.org"
gem "repeatable"
end
Schedule = Repeatable::Schedule
module ScheduleHelpers
def weekdays
{
union: [
{ weekday: { weekday: 1 } },
{ weekday: { weekday: 2 } },
{ weekday: { weekday: 3 } },
{ weekday: { weekday: 4 } },
{ weekday: { weekday: 5 } },
]
}
end
end
class PCOPTOSchedule
include ScheduleHelpers
def initialize(year)
@year = year
end
def beginning_of_year
Date.new(@year, 1, 1)
end
def end_of_year
Date.new(@year, 12, 31)
end
def all
{
union: [
summer_fridays,
non_summer_fridays_off,
christmas_break,
holidays,
]
}
end
def summer_fridays
{
intersection: [
{ range_in_year: { start_month: 6, end_month: 8 } },
{ weekday: { weekday: 5 } },
]
}
end
def non_summer_fridays_off
{
difference: {
included: all_non_summer_fridays,
excluded: { union: [holidays, christmas_break, non_summer_friday_exceptions].compact },
}
}
end
def all_non_summer_fridays
{
intersection: [
every_other_friday,
union: [
{ range_in_year: { start_month: 1, end_month: 5 } },
{ range_in_year: { start_month: 9, end_month: 12 } },
]
]
}
end
def non_summer_friday_exceptions
exceptions = Schedule.new(all_non_summer_fridays).occurrences(beginning_of_year, end_of_year).select do |d|
# is this friday the week before a friday holiday?
Schedule.new(union: [holidays, christmas_break]).include?(d + 7)
end
{ union: exceptions.map { |d| { exact_date: { date: d } } } } if exceptions.any?
end
def every_other_friday
{ biweekly: { weekday: 5, start_after: Date.new(@year, 1, 7) } }
end
def christmas_break
{
intersection: [
weekdays,
{ range_in_year: { start_month: 12, start_day: 24, end_day: 31 } },
]
}
end
def holidays
{
union: [
new_years_day,
mlk_day,
good_friday,
memorial_day,
july_4,
labor_day,
thanksgiving,
day_after_thanksgiving,
]
}
end
private
def new_years_day
{
intersection: [
weekdays,
{ exact_date: { date: Date.new(@year, 1, 1) } },
]
}
end
def mlk_day
{
intersection: [
{ weekday_in_month: { weekday: 1, count: 3 } },
{ range_in_year: { start_month: 1 } }
]
}
end
def good_friday
{ exact_date: { date: YearSpecificDates.new(@year).good_friday } }
end
def memorial_day
{
intersection: [
{ weekday_in_month: { weekday: 1, count: -1 } },
{ range_in_year: { start_month: 5 } }
]
}
end
def july_4
{ exact_date: { date: YearSpecificDates.new(@year).independence_day_observed } }
end
def labor_day
{
intersection: [
{ weekday_in_month: { weekday: 1, count: 1 } },
{ range_in_year: { start_month: 9 } }
]
}
end
def thanksgiving
{
intersection: [
{ weekday_in_month: { weekday: 4, count: 4 } },
{ range_in_year: { start_month: 11 } }
]
}
end
def day_after_thanksgiving
{
intersection: [
{ weekday_in_month: { weekday: 5, count: 4 } },
{ range_in_year: { start_month: 11 } }
]
}
end
class YearSpecificDates
include ScheduleHelpers
def initialize(year)
@year = year
end
def independence_day_observed
july_4 = Date.new(@year, 7, 4)
candidates = Schedule.new(weekdays).occurrences(Date.new(@year, 7, 3), Date.new(@year, 7, 5))
candidates.sort_by { |d| (july_4 - d).abs }.first
end
def good_friday
easter - 2
end
# this is wacky, but it works! and apparently it's been around for quite a while ¯\_(ツ)_/¯
# https://en.wikipedia.org/wiki/date_of_easter#anonymous_gregorian_algorithm
def easter
a = @year % 19
b = @year / 100
c = @year % 100
d = b / 4
e = b % 4
g = ((8 * b) + 13) / 25
h = ((19 * a) + b - d - g + 15) % 30
i = c / 4
k = c % 4
l = (32 + (2 * e) + (2 * i) - h - k) % 7
m = (a + (11 * h) + (19 * l)) / 433
month = (h + l - (7 * m) + 90) / 25
day = (h + l - (7 * m) + (33 * month) + 19) % 32
Date.new(@year, month, day)
end
end
end
year = 2022
start = Date.new(year, 1, 1)
finish = Date.new(year, 12, 31)
schedule = PCOPTOSchedule.new(year)
all = Schedule.new(schedule.all).occurrences(start, finish)
summer_fridays = Schedule.new(schedule.summer_fridays).occurrences(start, finish)
non_summer_fridays = Schedule.new(schedule.non_summer_fridays_off).occurrences(start, finish)
christmas_break = Schedule.new(schedule.christmas_break).occurrences(start, finish)
holidays = Schedule.new(schedule.holidays).occurrences(start, finish)
puts "summer fri | non-summer fri | xmas break | holidays | total"
puts "-----------------------------------------------------------"
puts "#{"%10s" % summer_fridays.count} | #{"%14s" % non_summer_fridays.count} | #{"%10s" % christmas_break.count} | #{"%8s" % holidays.count} | #{"%5s" % all.count}"
# output =>
# summer fri | non-summer fri | xmas break | holidays | total
# -----------------------------------------------------------
# 13 | 16 | 5 | 7 | 41
puts "Days Off"
all.each { |d| puts d.strftime("%a %b %e") }
# OUTPUT =>
# Fri Jan 14
# Mon Jan 17
# Fri Jan 28
# Fri Feb 11
# Fri Feb 25
# Fri Mar 11
# Fri Mar 25
# Fri Apr 15
# Fri Apr 22
# Fri May 6
# Fri May 20
# Mon May 30
# Fri Jun 3
# Fri Jun 10
# Fri Jun 17
# Fri Jun 24
# Fri Jul 1
# Mon Jul 4
# Fri Jul 8
# Fri Jul 15
# Fri Jul 22
# Fri Jul 29
# Fri Aug 5
# Fri Aug 12
# Fri Aug 19
# Fri Aug 26
# Mon Sep 5
# Fri Sep 9
# Fri Sep 23
# Fri Oct 7
# Fri Oct 21
# Fri Nov 4
# Thu Nov 24
# Fri Nov 25
# Fri Dec 2
# Fri Dec 16
# Mon Dec 26
# Tue Dec 27
# Wed Dec 28
# Thu Dec 29
# Fri Dec 30
puts "YEAR | Summer Fri | Non-Summer Fri | Xmas Break | Holidays | Total"
puts "------------------------------------------------------------------"
(2022..2026).each do |year|
start = Date.new(year, 1, 1)
finish = Date.new(year, 12, 31)
schedule = PCOPTOSchedule.new(year)
all = Schedule.new(schedule.all).occurrences(start, finish)
summer_fridays = Schedule.new(schedule.summer_fridays).occurrences(start, finish)
non_summer_fridays = Schedule.new(schedule.non_summer_fridays_off).occurrences(start, finish)
christmas_break = Schedule.new(schedule.christmas_break).occurrences(start, finish)
holidays = Schedule.new(schedule.holidays).occurrences(start, finish)
puts "#{year} | #{"%10s" % summer_fridays.count} | #{"%14s" % non_summer_fridays.count} | #{"%10s" % christmas_break.count} | #{"%8s" % holidays.count} | #{"%5s" % all.count}"
end
# OUTPUT =>
# YEAR | Summer Fri | Non-Summer Fri | Xmas Break | Holidays | Total
# ------------------------------------------------------------------
# 2022 | 13 | 16 | 5 | 7 | 41
# 2023 | 13 | 16 | 5 | 7 | 41
# 2024 | 13 | 17 | 6 | 8 | 44
# 2025 | 13 | 17 | 6 | 8 | 43
# 2026 | 13 | 17 | 6 | 8 | 43
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment