2024-12-02 17:59:09 +02:00

176 lines
7.2 KiB
Python

import calendar
import math
from collections import deque
from dataclasses import dataclass
from datetime import date, timedelta
TRANSFERABLE_NATIONAL_HOLIDAYS = [
(5, 4), # Restoration of Independence Day
(11, 18), # Proclamation Day of the Republic of Latvia
]
NATIONAL_HOLIDAYS = [
(1, 1), # New Year's Day
(5, 1), # Labour Day
(6, 23), # Midsummer's Eve
(6, 24), # Midsummer's Day
(12, 24), # Christmas Eve
(12, 25), # Christmas Day
(12, 26), # Second Day of Christmas
(12, 31), # New Year's Eve
]
VARIABLE_DATE_HOLIDAYS = []
TIME_FRAME = (
date(2023, 7, 1), # (date.today() - timedelta(days=365)).replace(day=1),
(date.today() + timedelta(days=365 * 2 + 31)).replace(day=1),
)
@dataclass
class MonthHolidayData:
work_hours: int
work_days: int
@dataclass
class HolidayCalendarForYear:
holidays: list[date]
months: dict[int, MonthHolidayData]
def get_gauss_easter_sunday(year: int) -> date:
# https://www.geeksforgeeks.org/how-to-calculate-the-easter-date-for-a-given-year-using-gauss-algorithm/
# All calculations done on the basis of Gauss Easter Algorithm
p = math.floor(year / 100)
d = (19 * (year % 19) + (15 - math.floor((13 + 8 * p) / 25) + p - p // 4) % 30) % 30
e = (2 * (year % 4) + 4 * (year % 7) + 6 * d + (4 + p - p // 4) % 7) % 7
days = (22 + d + e)
if (d == 29) and (e == 6): # A corner case, when D is 29
return date(year, 4, 19)
if (d == 28) and (e == 6): # Another corner case, when D is 28
return date(year, 4, 18)
if days > 31: # If days > 31, move to the April, eg, 4th Month
return date(year, 4, days - 31)
return date(year, 3, days) # Otherwise, stay on March, March = 3rd Month
def get_mothers_day(year) -> date:
# The 8th is the lowest second day in the month
min_day = date(year, 5, 8)
# What day of the week is the 8th?
week_day = min_day.weekday()
# Sunday is weekday 6
if week_day != 6:
# Replace just the day (of month)
min_day = min_day.replace(day=(8 + (6 - week_day) % 7))
return min_day
def get_holidays_in_timeframe(start_date: date, end_date: date) -> list[date]:
holidays = []
for year in range(start_date.year, end_date.year + 1):
for holiday in TRANSFERABLE_NATIONAL_HOLIDAYS:
holiday_date = date(year, *holiday)
holidays.append(holiday_date)
if holiday_date.weekday() == 5:
holidays.append(holiday_date + timedelta(days=2))
elif holiday_date.weekday() == 6:
holidays.append(holiday_date + timedelta(days=1))
holidays += [date(year, *holiday) for holiday in NATIONAL_HOLIDAYS]
easter_sunday = get_gauss_easter_sunday(year)
holidays += [
easter_sunday - timedelta(days=2),
easter_sunday,
easter_sunday + timedelta(days=1),
easter_sunday + timedelta(days=49), # Pentecost
get_mothers_day(year),
]
return sorted(holidays)
def get_holiday_calendar_for_timeframe(start_date: date, end_date: date) -> dict[int, HolidayCalendarForYear]:
holidays_by_year = {}
for holiday in get_holidays_in_timeframe(start_date, end_date):
if holiday.year not in holidays_by_year:
holidays_by_year[holiday.year] = []
holidays_by_year[holiday.year].append(holiday)
holiday_calendar = {}
for year in range(start_date.year, end_date.year + 1):
start_month = start_date.month if start_date.year == year else 1
end_month = end_date.month if end_date.year == year else 13
month_holidays = sorted(h for h in holidays_by_year[year] if start_date <= h <= end_date)
holiday_calendar[year] = HolidayCalendarForYear(holidays=month_holidays, months={})
for month in range(start_month, end_month):
work_days = work_hours = 0
for day in calendar.Calendar().itermonthdates(year, month):
if not day.month == month:
continue
if day.weekday() < 5 and day not in month_holidays:
work_hours += 7 if day + timedelta(days=1) in month_holidays else 8
work_days += 1
holiday_calendar[year].months[month] = MonthHolidayData(work_days=work_days, work_hours=work_hours)
return holiday_calendar
def print_working_day_calendar_for_time_frame(holiday_calendar: dict[int, HolidayCalendarForYear]) -> None:
print("Month,Work Days,Work Hours")
for year in sorted(holiday_calendar):
for month in sorted(holiday_calendar[year].months):
data = holiday_calendar[year].months[month]
print(f"{year}-{month:02}-01,{data.work_days},{data.work_hours}")
print("- - - - - - - - - - - - - - -")
def main(salary: int, holiday_days: int = 5):
start_date, end_date = TIME_FRAME
# start_date = (date.today() - timedelta(days=400)).replace(day=1)
# end_date = (date.today() + timedelta(days=365*2 + 31)).replace(day=1)
holiday_calendar = get_holiday_calendar_for_timeframe(*TIME_FRAME)
print_working_day_calendar_for_time_frame(holiday_calendar)
# List of per month holiday salary coefficient and work day coefficient,
# e.g., [((2024, 12) 0.848, 0.055)]
results: list[tuple[date, float, float]] = []
# Queue consists of last 6 month pay per day coefficients ( 1 ÷
queue: deque[float] = deque(maxlen=6)
# queue: list[float] = []
for year in range(start_date.year, end_date.year + 1):
start_month = start_date.month if start_date.year == year else 1
end_month = end_date.month if end_date.year == year else 13
for month in range(start_month, end_month):
queue.append(1 / holiday_calendar[year].months[month].work_days)
# if len(queue) > 6:
# queue.pop(0)
if not len(queue) == 6:
continue
y, m = (year + 1, 1) if month == 12 else (year, month + 1)
d = date(y, m, 1)
if d < end_date:
results.append(
(
d,
(sum(queue) / 6),
(1 / holiday_calendar[y].months[m].work_days),
)
)
# sorted(results, key=lambda t: t[1] - t[2])
print("Year - Month - Hol €/d - Wrk €/d => Hol d diff (Holiday salary diff) | coeff.")
for month, hol_coeff, wrk_cof in sorted(results, key=lambda t: (t[0])):
# for month, hol_coeff, wrk_cof in sorted(results, key=lambda t: (t[1]-t[2], t[0])):
# for month, hol_coeff, wrk_cof in sorted(results, key=lambda t: (holiday_calendar[t[0].year].months[t[0].month].work_days, t[0])):
print(
f"{month.year} - {month.month:5} - {hol_coeff:7.4f} - {wrk_cof:7.4f} => "
f"{(hol_coeff - wrk_cof):7.4f} ({(hol_coeff - wrk_cof) * holiday_days:7.04f}) | "
f"{hol_coeff / wrk_cof :6.4f} ||"
f" {hol_coeff * holiday_days + wrk_cof * (holiday_calendar[month.year].months[month.month].work_days - holiday_days):7.4f} => {(hol_coeff * holiday_days + wrk_cof * (holiday_calendar[month.year].months[month.month].work_days - holiday_days))*salary:7.4f} "
)
if __name__ == "__main__":
main(1700, 10)