diff --git a/contrib/holiday/README.md b/contrib/holiday/README.md new file mode 100644 index 00000000..1cdf36fe --- /dev/null +++ b/contrib/holiday/README.md @@ -0,0 +1,64 @@ +calcurse-holiday +================ + +`calcurse-holiday` is a Python script that can create holiday entries for +calcurse for a desired list of years based on a configuration file. Please note +that this script is still in an alpha version, so please report bugs (I think +the best way for this will be github, you can mention @atticus-sullivan) + +Why not simply use reocuring events? +------------------------------------ +Since some holiday days are not on the same date each year (e.g.some are +connected to the date Eastersunday is placed on), it is not possible to create +calcurse native reocuring events in calcurse for these events. + +Usage +----- +* Create a `yaml` file which will be the base of which holiday dates will be +inserted. +* Then run `calcurse-holiday` with the path to the yaml-file you just created + and a list of years you'd like to create the holiday-events for. (Duplicates + will be omitted) + +To check if the dates are correct before inserting them, just run the script +with the `-s` option. + +**Tipp:** On failiure the new events should simple by at the end of your `app` file +of calcurse and it should be easy to remove them again + +Example: +-------- +```yaml +holiday.yaml +---------------------- +# specify the description/name of the events that will be created +holiday_msg: "%s - free day" # %s will be replaced with the name/key of the +holiday date + +# current valid date specs: +# "easter [+-]x day" => Date will be x days before/after the day of Eastersunday +# "MM-DD [+-]x weekday" => Date will be the x.th mon/thu/... before/after +# YYYY-MM-DD (with given year). +# The weekday is specified as 1->Monday 2->Thuesday ... +irregular holiday: + - Eastersunday: "easter +0 day" + - Eastermonday: "easter +1 day" + - Thanksgiving: "11-01 +3 4" +``` + +`calcurse-holiday holiday.yaml 2021 2022` will read the holiday template from the +`holiday.yaml` file and create the holiday events for the years `2021` and `2022` + +Notes +----- +* Currently the purpose of this script is ONLY for events that are weekday offset +based or on eastern. More base dates are possible, just suggest them ;) + +* Maybe one day this will support "normal" reoccuring Events too. But since this +can much better be done via calcurses native reoccuring events (which is much +better, since one doesn't need to remember to recreate the events), this is not +the main purpose of this script. + +* The functionallity of avoiding duplicate entries might be added into native + calcurse via an commandline option. If this is complete one can remove the + calls to calcurse to check if an event is already added to calcurse diff --git a/contrib/holiday/calcurse-holiday.py b/contrib/holiday/calcurse-holiday.py new file mode 100755 index 00000000..35b6a095 --- /dev/null +++ b/contrib/holiday/calcurse-holiday.py @@ -0,0 +1,125 @@ +#!/usr/bin/env python3 + +import sys +import os +import subprocess +from datetime import datetime, timedelta +import argparse + +import yaml +import re + +# to calculate the date of the Eastersunday +def spencerFormula(y:int) -> datetime: + a = y % 19 + b = y // 100 + c = y % 100 + d = b // 4 + e = b % 4 + f = (b+8) // 25 + g = (b-f+1) // 3 + 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+22*l) // 451 + n = (h+l-7*m+114) // 31 + p = (h+l-7*m+114) % 31 + + return datetime(year=y, month=n, day=p+1) + + +def fullDayAppointmentIcal(date:datetime, offset:int, desc:str, noDup:bool = True, simu:bool = False) -> str: + ret = [] + ret.append("BEGIN:VEVENT") + ret.append("DTSTART;VALUE=DATE:" + (date + timedelta(days=offset)).strftime("%Y%m%d")) + ret.append("SUMMARY:" + desc) + if simu: + print("Event", desc, "on ", (date + timedelta(days=offset)).strftime("%Y-%m-%d"), "would be created") + ret.append("END:VEVENT") + if noDup: + command = [ + "/usr/bin/calcurse", + "-Q", + "-d", "%s" % (date + timedelta(days=offset)).strftime("%d/%m/%Y"), + "--filter-pattern",'"^%s$"' % re.escape(desc).replace("\\\\", "\\")] + p = subprocess.Popen(" ".join(command), stderr=None, stdin=None, stdout=subprocess.PIPE, encoding="utf-8", shell=True) + stdout, stderr = p.communicate() + if stdout != "": + print(ret, "is already present -> skip") + return "" + + return "\n".join(ret) + + +def toCalcurse(data:list[str]): + if "".join(data) != "": + data = \ + ["BEGIN:VCALENDAR\nVERSION:2.0\nPRODID:-//calcurse-holiday//NONSGML calcurse-holiday//EN"] \ + + data \ + + ["END:VCALENDAR\n"] + + command = ["calcurse"] + [ + '-i', '-', #read from stdin + '--dump-imported', + '-q' # quiet + ] + p = subprocess.Popen(command, stderr=None, stdin=subprocess.PIPE, stdout=None, encoding="utf-8") + p.communicate("\n".join(data)) + + +def createForYears(file:str, years:list[int], simu:bool): + with open(file, 'r') as f: + c = yaml.load(f, Loader=yaml.FullLoader) + + msg = c["holiday_msg"] if "holiday_msg" in c else "%s" + + if "irregular holiday" in c: + ical = [] + for d in c["irregular holiday"]: + d = list(d.items()) + if len(d) > 1: + print("Error while parsing the config", file=sys.stderr) + continue + + name,dateDesc = d[0] + + r1 = re.match(r'^easter ([+-]\d+) day$', dateDesc) + r2 = re.match(r'^(\d{2}\-\d{2}) ([+-]\d+) ([1-7])$', dateDesc) + + for year in years: + if r1: + offset = r1.groups()[0] + dtStart = spencerFormula(year) + ical.append(fullDayAppointmentIcal(dtStart, int(offset), msg % name, simu=simu)) + elif r2: + start,scale,weekday = r2.groups() + dtStart = datetime.strptime(str(year)+"-"+start, "%Y-%m-%d") + # print(int(weekday), "-", dtStart.weekday(),"- 1", "% 7 + ", int(scale), "* 7") + offset = (int(weekday) - dtStart.weekday()-1) % 7 + int(scale)*7 + ical.append(fullDayAppointmentIcal(dtStart, offset, msg % name, simu=simu)) + else: + print("Failed to read entry", dateDesc, file=sys.stderr) + if simu: + print("\nThis ical (+header) would have been imported") + print("\n".join(ical)) + else: + toCalcurse(ical) + + +parser = argparse.ArgumentParser("calcurse-holiday", + description="Add holiday entries based on given holiday.yaml file to calcurse") + +parser.add_argument("file", nargs='?', + default="./holiday.yaml", + help="The yaml file to read the holidays from [defaults to './holiday.yaml']") +parser.add_argument("year", nargs='+', + type=int, + help="Specify a list of years to generate the holidays for") +parser.add_argument("-s", "--simulate", + action='store_true', + help="Only simulate the insertion of the events") + +args = parser.parse_args() + +createForYears(args.file, args.year, args.simulate) diff --git a/contrib/holiday/holiday.yaml b/contrib/holiday/holiday.yaml new file mode 100644 index 00000000..7e624a32 --- /dev/null +++ b/contrib/holiday/holiday.yaml @@ -0,0 +1,11 @@ +# holiday_msg: "Frei %s" + +regular holiday: + - Tag der Arbeit: 1.5. + +# valid specifications are "easter +- x day" or MM.DD. +- weekday (given as 1-7 starting at monday) +irregular holiday: + - Ostersonntag: "easter +0 day" + - Ostermontag: "easter +1 day" + - Buß- und Bettag: "11-23 -1 3" + - Thanksgiving: "11-01 +3 4" diff --git a/contrib/holiday/testing-functions.py b/contrib/holiday/testing-functions.py new file mode 100644 index 00000000..56feebd8 --- /dev/null +++ b/contrib/holiday/testing-functions.py @@ -0,0 +1,33 @@ +#!/bin/python +import math + +# According to wikipedia these are critical years for Eastersunday date calculations +l = [(1590,"22.4") ,(1666,"25.4") ,(1685,"22.4") ,(1924,"20.4") ,(1943,"25.4") ,(1962,"22.4") ,(2019,"21.4") ,(2038,"25.4") ,(2057,"22.4") ,(2076,"19.4") ,(2095,"24.4") ,(2133,"19.4") ,(2152,"23.4") ,(2171,"21.4") ,(2190,"25.4") ,(1876,"16.4") ,(1974,"14.4") ,(2045,"9.4") ,(2069,"14.4") ,(2089,"3.4") ,(2096,"15.4") ,(1802,"18.4") ,(1805,"14.4") ,(1818,"22.3") ,(1825,"3.4") ,(1829,"19.4") ,(1845,"23.3") ,(1900,"15.4") ,(1903,"12.4") ,(1923,"1.4") ,(1927,"17.4") ,(1954,"18.4") ,(1967,"26.3") ,(1981,"19.4") ,(2049,"18.4") ,(2076,"19.4") ,(2106,"18.4") ,(2119,"26.3") ,(2133,"19.4") ,(2147,"16.4") ,(2150,"12.4") ,(2170,"1.4") ,(2174,"17.4")] + +def spencerFormula(y:int): + a = y % 19 + b = y // 100 + c = y % 100 + d = b // 4 + e = b % 4 + f = (b+8) // 25 + g = (b-f+1) // 3 + 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+22*l) // 451 + n = (h+l-7*m+114) // 31 + p = (h+l-7*m+114) % 31 + + return str(p+1) + "." + str(n) + +def checkEastern(foo, verbose:bool=False): + for y,e in l: + x = foo(y) + if x != e: + print(y, "failed, expected", e, "but was", x) + elif verbose: + print("success") + +checkEastern(spencerFormula)