176 lines
7.2 KiB
Python
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)
|