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)