Skip to content

Commit

Permalink
Merge pull request #152 from tswistak/holiday-generator
Browse files Browse the repository at this point in the history
Automated holiday generator
  • Loading branch information
naveensingh authored Mar 14, 2024
2 parents 6409003 + 894b49f commit 1a0ca55
Show file tree
Hide file tree
Showing 5 changed files with 258 additions and 0 deletions.
31 changes: 31 additions & 0 deletions .github/workflows/holiday-generator.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
name: Generate holiday ICS files

on:
schedule:
# Run every year on 1st December, midnight
- cron: '0 0 1 12 *'
workflow_dispatch:

jobs:
generate:
name: Generate ICS
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install NodeJS
uses: actions/setup-node@v4
with:
node-version: 20
- name: Run script
env:
LOG_ENABLED: true
working-directory: ./.github/workflows/holiday-generator
run: 'npm install && npm start'
- name: Create PR
uses: peter-evans/create-pull-request@v6
with:
commit-message: 'Autogenerated holidays update'
title: 'Holidays update'
body: ' '
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
2 changes: 2 additions & 0 deletions .github/workflows/holiday-generator/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
/node_modules
package-lock.json
85 changes: 85 additions & 0 deletions .github/workflows/holiday-generator/config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import { cwd } from "node:process";
import { join } from "node:path";

export const SHOULD_LOG = process.env.LOG_ENABLED === "true";

export const ICS_PATH = join(cwd(), "../../../app/src/main/assets");

// country codes from https://github.com/commenthol/date-holidays?tab=readme-ov-file#supported-countries-states-regions
export const COUNTRIES = [
["algeria.ics", "DZ"],
["argentina.ics", "AR"],
["australia.ics", "AU"],
["austria.ics", "AT"],
["belgium.ics", "BE"],
["bolivia.ics", "BO"],
["brazil.ics", "BR"],
["bulgaria.ics", "BG"],
["canada.ics", "CA"],
["china.ics", "CN"],
["colombia.ics", "CO"],
["costarica.ics", "CR"],
["croatia.ics", "HR"],
["czech.ics", "CZ"],
["denmark.ics", "DK"],
["estonia.ics", "EE"],
["finland.ics", "FI"],
["france.ics", "FR"],
["germany.ics", "DE"],
["greece.ics", "GR"],
["haiti.ics", "HT"],
["hungary.ics", "HU"],
["iceland.ics", "IS"],
// TODO add India: https://github.com/commenthol/date-holidays/issues/137
// ["india.ics", ""],
["indonesia.ics", "ID"],
["ireland.ics", "IE"],
["israel.ics", "IL"],
["italy.ics", "IT"],
["japan.ics", "jp"],
// TODO add Kazakhstan: (no GH issue)
// ["kazakhstan.ics", ""],
["latvia.ics", "LV"],
["liechtenstein.ics", "LI"],
["lithuania.ics", "LT"],
["luxembourg.ics", "LU"],
["macedonia.ics", "MK"],
["malaysia.ics", "MY"],
["mexico.ics", "MX"],
["morocco.ics", "MA"],
["netherlands.ics", "NL"],
["nicaragua.ics", "NI"],
["nigeria.ics", "NG"],
["norway.ics", "NO"],
// TODO add Pakistan: https://github.com/commenthol/date-holidays/pull/138
// ["pakistan.ics", ""],
["poland.ics", "PL"],
["portugal.ics", "PT"],
["romania.ics", "RO"],
["russia.ics", "RU"],
["serbia.ics", "RS"],
["singapore.ics", "SG"],
["slovakia.ics", "SK"],
["slovenia.ics", "SI"],
["southafrica.ics", "ZA"],
["southkorea.ics", "KR"],
["spain.ics", "ES"],
// TODO add Sri Lanka: (no GH issue)
// ["srilanka.ics", ""],
["sweden.ics", "SE"],
["switzerland.ics", "CH"],
["taiwan.ics", "TW"],
["thailand.ics", "TH"],
["turkey.ics", "TR"],
["ukraine.ics", "UA"],
["unitedkingdom.ics", "GB"],
["unitedstates.ics", "US"],
["uruguay.ics", "UY"],
];

export const START_YEAR = new Date().getFullYear(); // start with current year
export const END_YEAR = START_YEAR + 1;
export const FIXED_DATE_START_YEAR = 1970; // start recurring events from start of Unix epoch

// https://www.npmjs.com/package/date-holidays#types-of-holidays
export const TYPE_WHITELIST = ["public", "bank"];
125 changes: 125 additions & 0 deletions .github/workflows/holiday-generator/main.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
import { createHash } from "node:crypto";
import { writeFile } from "node:fs/promises";
import { join } from "node:path";
import { promisify } from "node:util";

import Holidays from "date-holidays";
import { createEvents as icsCreateEvents } from "ics";

import { COUNTRIES, END_YEAR, FIXED_DATE_START_YEAR, ICS_PATH, SHOULD_LOG, START_YEAR, TYPE_WHITELIST } from "./config.js";

// converting createEvents from ics from function with callback to async function for easier usage
const createEvents = promisify(icsCreateEvents);

/**
* Log info to console
* @param {*} toLog
*/
function log(toLog) {
if (SHOULD_LOG) {
console.log(toLog);
}
}

/**
* Get events for country given by code
* @param {string} countryCode
* @returns
*/
function getEvents(countryCode) {
const generator = new Holidays(countryCode);
const events = [];
for (let i = START_YEAR; i <= END_YEAR; i++) {
events.push(...generator.getHolidays(i).filter((x) => TYPE_WHITELIST.includes(x.type)));
}
return events;
}

/**
* Generates reproducible ID for holiday
* @param {string} countryCode
* @param {string} date
* @param {string} rule
* @returns
*/
function generateUid(countryCode, date, rule) {
const hashGen = createHash("sha256");
hashGen.update(`${countryCode},${date},${rule}`);
return hashGen.digest("hex");
}

/**
* Convert JS Date object to ics.DateTime array
* @param {Date} date
* @returns
*/
function getDateArray(date) {
return [date.getFullYear(), date.getMonth() + 1, date.getDate()];
}

/**
* Checks if holiday is a fixed-date holiday.
* Regex based on https://github.com/commenthol/date-holidays/blob/master/docs/specification.md#fixed-date
* @param {string} rule
* @returns
*/
function isFixedDate(rule) {
return /^\d\d-\d\d( and .*)?$/.test(rule);
}

/**
* Generate ical file from given set of events
* @param {ReturnType<getEvents>} events
* @param {string} countryCode
* @returns {Promise<string>}
*/
async function generateIcal(events, countryCode) {
const eventsMap = new Map();
events.forEach((x) => {
if (isFixedDate(x.rule)) {
const uid = generateUid(countryCode, "", x.rule);
if (!eventsMap.has(uid)) {
const yearDiff = x.end.getFullYear() - x.start.getFullYear();
x.start.setFullYear(FIXED_DATE_START_YEAR);
x.end.setFullYear(FIXED_DATE_START_YEAR + yearDiff);
eventsMap.set(uid, {
title: x.name,
uid,
start: getDateArray(x.start),
end: getDateArray(x.end),
recurrenceRule: "FREQ=YEARLY",
productId: "Fossify Calendar Holiday Generator",
status: "CONFIRMED",
});
}
} else {
const uid = generateUid(countryCode, x.date, x.rule);
eventsMap.set(uid, {
title: x.name,
uid,
start: getDateArray(x.start),
end: getDateArray(x.end),
productId: "Fossify Calendar Holiday Generator",
status: "CONFIRMED",
});
}
});
const ical = await createEvents([...eventsMap.values()]);
return ical;
}

/**
* Function generating ical files
*/
async function doWork() {
for (const [file, code] of COUNTRIES) {
log(`Generating events for ${code}, ${file}`);
const events = getEvents(code);
const ical = await generateIcal(events, code);
const filePath = join(ICS_PATH, file);
await writeFile(filePath, ical, { encoding: "utf-8" });
log(`File saved to ${filePath}`);
}
}

await doWork();
15 changes: 15 additions & 0 deletions .github/workflows/holiday-generator/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"name": "@fossify/holiday-generator",
"version": "1.0.0",
"description": "Holiday generator for Fossify Calendar",
"main": "main.js",
"type": "module",
"scripts": {
"start": "node main.js"
},
"license": "GPL-3.0-only",
"dependencies": {
"date-holidays": "^3",
"ics": "^3.7.2"
}
}

0 comments on commit 1a0ca55

Please sign in to comment.