├── .gitignore ├── tsconfig.json ├── .github └── workflows │ ├── build.yml │ └── publish.yml ├── package.json ├── tests ├── getHolidaysByYear.test.js └── isHoliday.test.js ├── README.md └── src └── index.ts /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | /build -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "module": "commonjs", 5 | "lib": [ 6 | "es2015" 7 | ], 8 | "declaration": true, 9 | "outDir": "./build", 10 | "rootDir": "./src", 11 | "removeComments": true, 12 | "strict": true, 13 | "esModuleInterop": true 14 | } 15 | } -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build and Test 2 | 3 | on: [push, pull_request, repository_dispatch] 4 | 5 | jobs: 6 | build: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/checkout@v2 10 | - uses: actions/setup-node@v1 11 | with: 12 | node-version: '12.x' 13 | - name: npm install, build, and test 14 | run: | 15 | npm ci 16 | npm run build --if-present 17 | env: 18 | CI: true 19 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish Node.js Package 2 | 3 | on: 4 | release: 5 | types: [created] 6 | 7 | jobs: 8 | build: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v2 12 | - uses: actions/setup-node@v1 13 | with: 14 | node-version: '12.x' 15 | registry-url: 'https://registry.npmjs.org' 16 | - name: npm install, publish 17 | run: | 18 | npm install 19 | npm publish 20 | env: 21 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "moment-feiertage", 3 | "version": "2.0.9", 4 | "description": "Moment.js Plugin for german holidays; check if a given Date is a german holiday", 5 | "main": "build/index.js", 6 | "types": "build/index.d.ts", 7 | "files": [ 8 | "build/**/*" 9 | ], 10 | "homepage": "https://github.com/DaniSchenk/moment-feiertage", 11 | "scripts": { 12 | "build": "npm run lint && npm run format && tsc && npm run test", 13 | "format": "prettier --write src/**/*.ts", 14 | "lint": "eslint --fix src/**/*.ts", 15 | "prepare": "npm run build", 16 | "test": "jest" 17 | }, 18 | "repository": { 19 | "type": "git", 20 | "url": "https://github.com/DaniSchenk/moment-feiertage.git" 21 | }, 22 | "publishConfig": { 23 | "access": "public" 24 | }, 25 | "bugs": { 26 | "url": "https://github.com/DaniSchenk/moment-feiertage/issues" 27 | }, 28 | "author": { 29 | "name": "Daniel Schenk", 30 | "url": "https://github.com/DaniSchenk" 31 | }, 32 | "keywords": [ 33 | "moment", 34 | "js", 35 | "momentjs", 36 | "plugin", 37 | "german", 38 | "holidays", 39 | "deutsche", 40 | "feiertage" 41 | ], 42 | "license": "ISC", 43 | "peerDependencies": { 44 | "moment": ">=2.15.x" 45 | }, 46 | "devDependencies": { 47 | "@typescript-eslint/eslint-plugin": "^5.30.6", 48 | "@typescript-eslint/parser": "^5.30.6", 49 | "eslint": "^8.19.0", 50 | "eslint-config-prettier": "^8.5.0", 51 | "eslint-plugin-prettier": "^4.2.1", 52 | "jest": "^28.1.3", 53 | "moment": "^2.29.4", 54 | "prettier": "2.7.1", 55 | "typescript": "^4.7.4" 56 | }, 57 | "eslintConfig": { 58 | "env": { 59 | "browser": true, 60 | "commonjs": true, 61 | "es6": true, 62 | "node": true 63 | }, 64 | "extends": [ 65 | "eslint:recommended", 66 | "plugin:@typescript-eslint/eslint-recommended", 67 | "plugin:prettier/recommended" 68 | ], 69 | "globals": { 70 | "Atomics": "readonly", 71 | "SharedArrayBuffer": "readonly" 72 | }, 73 | "parser": "@typescript-eslint/parser", 74 | "parserOptions": { 75 | "ecmaVersion": 2018 76 | }, 77 | "plugins": [ 78 | "@typescript-eslint", 79 | "prettier" 80 | ], 81 | "rules": { 82 | "prettier/prettier": "error", 83 | "no-unused-vars": "off", 84 | "@typescript-eslint/no-unused-vars": [ 85 | "error" 86 | ] 87 | } 88 | }, 89 | "prettier": { 90 | "arrowParens": "always", 91 | "trailingComma": "es5", 92 | "tabWidth": 2, 93 | "semi": true, 94 | "singleQuote": true, 95 | "endOfLine": "lf" 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /tests/getHolidaysByYear.test.js: -------------------------------------------------------------------------------- 1 | const moment = require('../build/index'); 2 | 3 | test('getHolidaysByYear 2020', () => { 4 | const hU = moment.getHolidaysByYear(2020); 5 | const hO = {} 6 | for (const k in hU) { 7 | hU[k].date = hU[k].date.toISOString(); 8 | } 9 | Object.keys(hU).sort().forEach((i) => { 10 | hO[i] = hU[i]; 11 | }); 12 | 13 | const cU = { 14 | Neujahrstag: { 15 | date: moment('2020-01-01').toISOString(), 16 | state: [] 17 | }, 18 | 'Heilige Drei Könige': { 19 | date: moment('2020-01-06').toISOString(), 20 | state: ['BW', 'BY', 'ST'] 21 | }, 22 | Karfreitag: { 23 | date: moment('2020-04-10').toISOString(), 24 | state: [] 25 | }, 26 | Ostersonntag: { 27 | date: moment('2020-04-12').toISOString(), 28 | state: ['BB'] 29 | }, 30 | Ostermontag: { 31 | date: moment('2020-04-13').toISOString(), 32 | state: [] 33 | }, 34 | Maifeiertag: { 35 | date: moment('2020-05-01').toISOString(), 36 | state: [] 37 | }, 38 | 'Christi Himmelfahrt': { 39 | date: moment('2020-05-21').toISOString(), 40 | state: [] 41 | }, 42 | Pfingstsonntag: { 43 | date: moment('2020-05-31').toISOString(), 44 | state: ['BB'] 45 | }, 46 | Pfingstmontag: { 47 | date: moment('2020-06-01').toISOString(), 48 | state: [] 49 | }, 50 | Fronleichnam: { 51 | date: moment('2020-06-11').toISOString(), 52 | state: ['BW', 'BY', 'HE', 'NW', 'RP', 'SL'] 53 | }, 54 | 'Mariä Himmelfahrt': { 55 | date: moment('2020-08-15').toISOString(), 56 | state: ['SL'] 57 | }, 58 | 'Tag der deutschen Einheit': { 59 | date: moment('2020-10-03').toISOString(), 60 | state: [] 61 | }, 62 | Reformationstag: { 63 | date: moment('2020-10-31').toISOString(), 64 | state: [ 65 | 'BB', 'HB', 'HH', 66 | 'MV', 'NI', 'SN', 67 | 'ST', 'SH', 'TH' 68 | ] 69 | }, 70 | Allerheiligen: { 71 | date: moment('2020-11-01').toISOString(), 72 | state: ['BW', 'BY', 'NW', 'RP', 'SL'] 73 | }, 74 | 'Buß- und Bettag': { 75 | date: moment('2020-11-18').toISOString(), 76 | state: ['SN'] 77 | }, 78 | '1. Weihnachtsfeiertag': { 79 | date: moment('2020-12-25').toISOString(), 80 | state: [] 81 | }, 82 | '2. Weihnachtsfeiertag': { 83 | date: moment('2020-12-26').toISOString(), 84 | state: [] 85 | }, 86 | 'Internationaler Frauentag': { 87 | date: moment('2020-03-08').toISOString(), 88 | state: ['BE'] 89 | }, 90 | Weltkindertag: { 91 | date: moment('2020-09-20').toISOString(), 92 | state: ['TH'] 93 | }, 94 | 'Tag der Befreiung': { 95 | date: moment('2020-05-08').toISOString(), 96 | state: ['BE'] 97 | } 98 | }; 99 | const cO = {} 100 | Object.keys(cU).sort().forEach((j) => { 101 | cO[j] = cU[j]; 102 | }); 103 | 104 | expect(hO).toStrictEqual(cO); 105 | }); -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # moment-feiertage 2 | moment-feiertage is a [Moment.js](http://momentjs.com/) plugin to determine if a date is a german holiday. Holidays are taken from [Wikipedia (de)](https://de.wikipedia.org/wiki/Gesetzliche_Feiertage_in_Deutschland). Feel free to contribute! 3 | 4 | ## How to use? 5 | 1. Add moment-feiertage to your package.json by running `npm install moment-feiertage --save`. Moment.js is a peer dependency, so don't forget to install it, if you haven't already. 6 | 2. Import `moment` from moment-feiertage like you would from the original Moment.js package. moment-feiertage exports the original moment object with extended functionality. 7 | ```javascript 8 | // Typescript 9 | import * as moment from 'moment-feiertage'; 10 | 11 | // node 12 | const moment = require('moment-feiertage'); 13 | ``` 14 | 3. Check the examples below for functionality, supported arguments and return values. 15 | - getAllStateCodes() 16 | - getHolidaysByYear() 17 | - isHoliday() Array support 18 | - isHoliday() 19 | - [state codes](#State-codes) 20 | - [contribute](#Contribute) 21 | 22 | ## getAllStateCodes() 23 | since 2.0.0 24 | ```javascript 25 | const codes = moment.getAllStateCodes(); 26 | /* returns ['BW','BY','BE','BB','HB','HH','HE','MV','NI','NW','RP','SL','SN','ST','SH','TH']*/ 27 | ``` 28 | 29 | ## getHolidaysByYear(year: number) 30 | since 2.0.0 31 | 32 | Returns an object containing all holidays of a year. Every holiday has a `date` and a `state` property. `date` is holding a moment object representing the holidays date. It's a nationwide holiday, if the `state` value is an empty Array. 33 | ```javascript 34 | const codes = moment.getHolidaysByYear(2020); 35 | /* returns { 36 | 'Neujahrstag': { 37 | date: moment('2020-01-01'), 38 | state: [] // nationwide holiday 39 | }, 40 | 'Heilige Drei Könige': { 41 | date: moment('2020-01-06') 42 | state: ['BW', 'BY', 'ST'] // only these states celebrate 43 | }, 44 | [ ... ] 45 | } */ 46 | ``` 47 | 48 | ## isHoliday(states: Array) 49 | since 1.1.0 50 | 51 | From version `1.1.0` on `isHoliday()` supports Arrays. Pass an empty Array to test against all states, or pass an Array of state codes (e.g. `['BY', 'SH']`). The return value is an Object: 52 | ```javascript 53 | { 54 | allStates: boolean, // default false 55 | holidayName: string, // default: '' 56 | holidayStates: Array, // default: [] 57 | testedStates: Array // default: ...allStates 58 | } 59 | ``` 60 | 61 | - `allStates` is `true`, if the checked date is a nationwide holiday, even if not all states are checked because of the `states` param. 62 | - `holidayName` contains the name of the holiday 63 | - `holidayStates` contains the states, where this holiday is celebrated. If `states` param is provided, `holidayStates` contains only a subset of `states` 64 | - `testedStates` is the same as the `states` param. If `states` param is `[]`, isHoliday will check against all states by default 65 | 66 | ```javascript 67 | const christmasInAllStates = moment('2018-11-01').isHoliday([]); 68 | /* returns { 69 | allStates: true, 70 | holidayName: '1. Weihnachtsfeiertag', 71 | holidayStates: ...allStates, 72 | testedStates: ...allStates 73 | }*/ 74 | const christmasInSomeStates = moment('2018-11-01').isHoliday(['BW', 'SH']); 75 | /* returns { 76 | allStates: true, 77 | holidayName: '1. Weihnachtsfeiertag', 78 | holidayStates: [ 'BW', 'SH' ], 79 | testedStates: [ 'BW', 'SH' ] 80 | }*/ 81 | const someDateInAllStates = moment('2018-11-01').isHoliday([]); 82 | /* returns { 83 | allStates: false, 84 | holidayName: 'Allerheiligen', 85 | holidayStates: [ 'BW', 'BY', 'NW', 'RP', 'SL' ], 86 | testedStates: ...allStates 87 | }*/ 88 | const noHolidayDateInAllStates = moment('2018-12-12').isHoliday([]); 89 | /* returns { 90 | allStates: false, 91 | holidayName: '', 92 | holidayStates: [], 93 | testedStates: ...allStates 94 | }*/ 95 | ``` 96 | 97 | ## isHoliday(state?: string) 98 | since 1.0.0 99 | 100 | Since version `1.0.0` `isHoliday()` checks if there's a holiday at a moment object. A state code can be provided optionally. 101 | 102 | ```javascript 103 | const nowIsHoliday = moment().isHoliday(); 104 | // returns name of holiday (string) if date is a holiday 105 | // retruns false (boolean) if date is not a holiday 106 | 107 | const someDateIsHoliday = moment('2019-12-25').isHoliday(); 108 | // returns '1. Weihnachtsfeiertag' - is a holiday in all states 109 | 110 | const isHolidayInAllStates = moment('2017-08-15').isHoliday(); 111 | // returns false - is not a holiday in all states 112 | 113 | const isHolidayInBavaria = moment('2017-08-15').isHoliday('BY'); 114 | // returns false - is not a holiday in BY 115 | 116 | const isHolidayInSaarland = moment('2017-08-15').isHoliday('SL'); 117 | // returns 'Mariä Himmelfahrt' - is a holiday in SL 118 | ``` 119 | ## State codes 120 | ``` 121 | BW = Baden-Württemberg 122 | BY = Bayern 123 | BE = Berlin 124 | BB = Brandenburg 125 | HB = Bremen 126 | HH = Hamburg 127 | HE = Hessen 128 | MV = Mecklenburg-Vorpommern 129 | NI = Niedersachsen 130 | NW = Nordrhein-Westfalen 131 | RP = Rheinland-Pfalz 132 | SL = Saarland 133 | SN = Sachsen 134 | ST = Sachsen-Anhalt 135 | SH = Schleswig-Holstein 136 | TH = Thüringen 137 | ``` 138 | 139 | ### Mappings 140 | - Google Places Api: https://github.com/DaniSchenk/moment-feiertage/issues/17#issuecomment-780445461 provided by [@t-wark](https://github.com/t-wark) 141 | 142 | # Contribute 143 | 1. fork 144 | 2. `npm install` and add your desired version of Moment.js: `npm install moment --no-save` 145 | 3. code 146 | 4. `npm run build`: linting, formating, building, testing 147 | 5. PR 148 | -------------------------------------------------------------------------------- /tests/isHoliday.test.js: -------------------------------------------------------------------------------- 1 | const moment = require('../build/index'); 2 | const allStates = moment.getAllStateCodes(); 3 | 4 | test('current date', () => { 5 | try { 6 | moment().isHoliday(); 7 | moment().isHoliday([]); 8 | } 9 | catch(e) { 10 | fail(); 11 | } 12 | }); 13 | 14 | test('non holiday date', () => { 15 | expect(moment('2017-01-03').isHoliday()).toBe(false); 16 | expect(moment('2017-01-03').isHoliday([])).toStrictEqual({ 17 | allStates: false, 18 | holidayName: '', 19 | holidayStates: [], 20 | testedStates: allStates 21 | }); 22 | 23 | for (let s of allStates) { 24 | expect(moment('2017-01-03').isHoliday(s)).toBe(false); 25 | expect(moment('2017-01-03').isHoliday([s])).toStrictEqual({ 26 | allStates: false, 27 | holidayName: '', 28 | holidayStates: [], 29 | testedStates: [s] 30 | }); 31 | } 32 | }); 33 | 34 | test('1. Weihnachtsfeiertag', () => { 35 | expect(moment('2019-12-25').isHoliday()).toBe('1. Weihnachtsfeiertag'); 36 | expect(moment('2019-12-25').isHoliday([])).toStrictEqual({ 37 | allStates: true, 38 | holidayName: '1. Weihnachtsfeiertag', 39 | holidayStates: allStates, 40 | testedStates: allStates 41 | }); 42 | 43 | for (const s of allStates) { 44 | expect(moment('2019-12-25').isHoliday(s)).toBe('1. Weihnachtsfeiertag'); 45 | expect(moment('2019-12-25').isHoliday([s])).toStrictEqual({ 46 | allStates: true, 47 | holidayName: '1. Weihnachtsfeiertag', 48 | holidayStates: [s], 49 | testedStates: [s] 50 | }); 51 | } 52 | }); 53 | 54 | test('Reformationstag', () => { 55 | const before2017 = ['BB', 'MV', 'SN', 'ST', 'TH']; 56 | const after2017 = ['BB', 'HB', 'HH', 'MV', 'NI', 'SN', 'ST', 'SH', 'TH']; 57 | 58 | expect(moment('2016-10-31').isHoliday()).toBe(false); 59 | expect(moment('2017-10-31').isHoliday()).toBe('Reformationstag'); 60 | expect(moment('2018-10-31').isHoliday()).toBe(false); 61 | 62 | expect(moment('2016-10-31').isHoliday([])).toStrictEqual({ 63 | allStates: false, 64 | holidayName: 'Reformationstag', 65 | holidayStates: before2017, 66 | testedStates: allStates 67 | }); 68 | expect(moment('2017-10-31').isHoliday([])).toStrictEqual({ 69 | allStates: true, 70 | holidayName: 'Reformationstag', 71 | holidayStates: allStates, 72 | testedStates: allStates 73 | }); 74 | expect(moment('2018-10-31').isHoliday([])).toStrictEqual({ 75 | allStates: false, 76 | holidayName: 'Reformationstag', 77 | holidayStates: after2017, 78 | testedStates: allStates 79 | }); 80 | 81 | for (const s of allStates) { 82 | let result = false; 83 | if (before2017.includes(s)) { 84 | result = 'Reformationstag'; 85 | } 86 | expect(moment('2016-10-31').isHoliday(s)).toBe(result); 87 | result = false; 88 | if (after2017.includes(s)) { 89 | result = 'Reformationstag'; 90 | } 91 | expect(moment('2018-10-31').isHoliday(s)).toBe(result); 92 | } 93 | }); 94 | 95 | test('Internationaler Frauentag', () => { 96 | expect(moment('2018-03-08').isHoliday()).toBe(false); 97 | expect(moment('2019-03-08').isHoliday()).toBe(false); 98 | expect(moment('2023-03-08').isHoliday()).toBe(false); 99 | 100 | expect(moment('2018-03-08').isHoliday([])).toStrictEqual({ 101 | allStates: false, 102 | holidayName: '', 103 | holidayStates: [], 104 | testedStates: allStates, 105 | }); 106 | expect(moment('2019-03-08').isHoliday([])).toStrictEqual({ 107 | allStates: false, 108 | holidayName: 'Internationaler Frauentag', 109 | holidayStates: ['BE'], 110 | testedStates: allStates, 111 | }); 112 | expect(moment('2023-03-08').isHoliday([])).toStrictEqual({ 113 | allStates: false, 114 | holidayName: 'Internationaler Frauentag', 115 | holidayStates: ['BE', 'MV'], 116 | testedStates: allStates, 117 | }); 118 | }); 119 | 120 | test('return objects', () => { 121 | expect(moment('2020-10-03').isHoliday([])).toStrictEqual({ 122 | allStates: true, 123 | holidayName: 'Tag der deutschen Einheit', 124 | holidayStates: allStates, 125 | testedStates: allStates 126 | }); 127 | 128 | expect(moment('2020-10-03').isHoliday(['BW', 'BY', 'BE'])).toStrictEqual({ 129 | allStates: true, 130 | holidayName: 'Tag der deutschen Einheit', 131 | holidayStates: ['BW', 'BY', 'BE'], 132 | testedStates: ['BW', 'BY', 'BE'] 133 | }); 134 | 135 | expect(moment('2020-01-06').isHoliday([])).toStrictEqual({ 136 | allStates: false, 137 | holidayName: 'Heilige Drei Könige', 138 | holidayStates: ['BW', 'BY', 'ST'], 139 | testedStates: allStates 140 | }); 141 | 142 | expect(moment('2020-01-06').isHoliday(['BW', 'BY', 'BB', 'HB', 'HH'])).toStrictEqual({ 143 | allStates: false, 144 | holidayName: 'Heilige Drei Könige', 145 | holidayStates: ['BW', 'BY'], 146 | testedStates: ['BW', 'BY', 'BB', 'HB', 'HH'] 147 | }); 148 | }); 149 | 150 | test('invalid inputs', () => { 151 | // this is a valid input 152 | expect(moment('2020-01-01').isHoliday([])).toStrictEqual({ 153 | allStates: true, 154 | holidayName: 'Neujahrstag', 155 | holidayStates: allStates, 156 | testedStates: allStates 157 | }); 158 | 159 | // duplicate inputs 160 | expect(moment('2020-01-01').isHoliday(['BY', 'BY', 'BY', 'BW', 'BY'])).toStrictEqual({ 161 | allStates: true, 162 | holidayName: 'Neujahrstag', 163 | holidayStates: ['BY', 'BW'], 164 | testedStates: ['BY', 'BW'] 165 | }); 166 | 167 | // invalid inputs 168 | expect(moment('2020-01-01').isHoliday(['bw', 'ABC', '', null, undefined, 12, -12, 0, false])).toStrictEqual({ 169 | allStates: true, 170 | holidayName: 'Neujahrstag', 171 | holidayStates: [], 172 | testedStates: [] 173 | }); 174 | }); 175 | 176 | test('Buß- und Bettag', () => { 177 | const dates = ['1995-11-22', '2000-11-22', '2005-11-16', '2018-11-21', '2019-11-20', '2020-11-18', '2021-11-17', '2022-11-16', '2023-11-22', '2024-11-20'] 178 | for (const bnb of dates) { 179 | expect(moment(bnb).isHoliday('SN')).toBe('Buß- und Bettag'); 180 | } 181 | }); 182 | 183 | test('Ostermontag', () => { 184 | const dates = ['1995-04-17', '2000-04-24', '2005-03-28', '2018-04-02', '2019-04-22', '2020-04-13', '2021-04-05', '2022-04-18', '2023-04-10', '2024-04-01'] 185 | for (const d of dates) { 186 | expect(moment(d).isHoliday()).toBe('Ostermontag'); 187 | } 188 | }); 189 | 190 | test('DateTimes', () => { 191 | expect(moment('2019-12-26 00:00:00').isHoliday()).toBe('2. Weihnachtsfeiertag'); 192 | expect(moment('2019-12-26 12:00:00').isHoliday()).toBe('2. Weihnachtsfeiertag'); 193 | expect(moment('2019-12-26 23:59:59').isHoliday()).toBe('2. Weihnachtsfeiertag'); 194 | 195 | expect(moment('2019-12-26 00:00:00').isHoliday([])).toStrictEqual({ 196 | allStates: true, 197 | holidayName: '2. Weihnachtsfeiertag', 198 | holidayStates: allStates, 199 | testedStates: allStates 200 | }); 201 | expect(moment('2019-12-26 12:00:00').isHoliday([])).toStrictEqual({ 202 | allStates: true, 203 | holidayName: '2. Weihnachtsfeiertag', 204 | holidayStates: allStates, 205 | testedStates: allStates 206 | }); 207 | expect(moment('2019-12-26 23:59:59').isHoliday([])).toStrictEqual({ 208 | allStates: true, 209 | holidayName: '2. Weihnachtsfeiertag', 210 | holidayStates: allStates, 211 | testedStates: allStates 212 | }); 213 | 214 | expect(moment('2019-12-27 00:00:00').isHoliday()).toBe(false); 215 | expect(moment('2019-12-27 12:00:00').isHoliday()).toBe(false); 216 | expect(moment('2019-12-27 23:59:59').isHoliday()).toBe(false); 217 | }); -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import moment = require('moment'); 2 | export = moment; 3 | 4 | const allStates: Array = [ 5 | 'BW', 6 | 'BY', 7 | 'BE', 8 | 'BB', 9 | 'HB', 10 | 'HH', 11 | 'HE', 12 | 'MV', 13 | 'NI', 14 | 'NW', 15 | 'RP', 16 | 'SL', 17 | 'SN', 18 | 'ST', 19 | 'SH', 20 | 'TH', 21 | ]; 22 | const allHolidays: YearWithHolidays = {}; 23 | 24 | interface IsHolidayResult { 25 | allStates: boolean; 26 | holidayName: string; 27 | holidayStates: Array; 28 | testedStates: Array; 29 | } 30 | interface Holiday { 31 | date: moment.Moment; 32 | state: Array; 33 | } 34 | interface Holidays { 35 | [key: string]: Holiday; 36 | } 37 | interface YearWithHolidays { 38 | [key: string]: Holidays; 39 | } 40 | 41 | declare module 'moment' { 42 | export interface Moment { 43 | isHoliday: ( 44 | _states?: string | Array 45 | ) => boolean | string | IsHolidayResult; 46 | } 47 | export function getHolidaysByYear(_year: number): Holidays; 48 | export function getAllStateCodes(): Array; 49 | } 50 | 51 | const getAllStateCodes = function (): Array { 52 | return allStates; 53 | }; 54 | 55 | const isHoliday = function ( 56 | this: moment.Moment, 57 | _states?: string | Array 58 | ): boolean | string | IsHolidayResult { 59 | const _moment = this; 60 | 61 | // calculate holidays if needed 62 | if ( 63 | !Object.prototype.hasOwnProperty.call( 64 | allHolidays, 65 | _moment.year().toString() 66 | ) 67 | ) { 68 | allHolidays[_moment.year().toString()] = calculateHolidays(_moment.year()); 69 | } 70 | 71 | // call backwards compatible function 72 | if (typeof _states === 'string' || !_states) { 73 | return _isHoliday106(_moment, _states); 74 | } 75 | // return IsHolidayResult if _states parma is Array 76 | if (Array.isArray(_states)) { 77 | return _isHoliday(_moment, _states); 78 | } 79 | 80 | // TODO: return error 81 | return false; 82 | }; 83 | 84 | const getHolidaysByYear = (_year: number): Holidays => { 85 | if (!Object.prototype.hasOwnProperty.call(allHolidays, _year.toString())) { 86 | return calculateHolidays(_year); 87 | } 88 | return allHolidays[_year.toString()]; 89 | }; 90 | 91 | // add custom functions to moment 92 | (moment as any).fn.isHoliday = isHoliday; 93 | (moment as any).getHolidaysByYear = getHolidaysByYear; 94 | (moment as any).getAllStateCodes = getAllStateCodes; 95 | 96 | const _isHoliday106 = ( 97 | _moment: moment.Moment, 98 | _state?: string 99 | ): string | boolean => { 100 | if ( 101 | Object.prototype.hasOwnProperty.call( 102 | allHolidays, 103 | _moment.year().toString() 104 | ) && 105 | allHolidays[_moment.year().toString()] 106 | ) { 107 | const holidays = allHolidays[_moment.year().toString()]; 108 | for (const h of Object.keys(holidays)) { 109 | // test if moment is holiday 110 | if (_moment.isSame(holidays[h].date, 'day')) { 111 | // return name if all states celebrate this holiday 112 | if (holidays[h].state.length === 0) { 113 | return h; 114 | } else { 115 | // return name if passed state celebrates holiday 116 | if (_state && holidays[h].state.indexOf(_state) > -1) { 117 | return h; 118 | } else { 119 | // return false if passed state does not celebrate holiday 120 | return false; 121 | } 122 | } 123 | } 124 | } 125 | } 126 | return false; 127 | }; 128 | 129 | const _isHoliday = ( 130 | _moment: moment.Moment, 131 | _states: Array 132 | ): IsHolidayResult => { 133 | const result: IsHolidayResult = { 134 | allStates: false, 135 | holidayName: '', 136 | holidayStates: [], 137 | testedStates: [], 138 | }; 139 | 140 | // test in all states if _states is empty 141 | if (_states && _states.length < 1) { 142 | result.testedStates = allStates; 143 | } else { 144 | // validate state codes from params 145 | result.testedStates = validateStateCodes(_states); 146 | } 147 | 148 | // test multiple state codes 149 | for (const s of result.testedStates) { 150 | const holiday = _isHoliday106(_moment, s); 151 | if (holiday && typeof holiday === 'string') { 152 | result.holidayStates.push(s); 153 | result.holidayName = holiday; 154 | } 155 | } 156 | 157 | // test for nation wide holiday 158 | const nationHoliday = _isHoliday106(_moment); 159 | if (nationHoliday && typeof nationHoliday === 'string') { 160 | result.allStates = true; 161 | result.holidayName = nationHoliday; 162 | } 163 | 164 | return result; 165 | }; 166 | 167 | const validateStateCodes = (_states: Array): Array => { 168 | // sort out false values 169 | let validCodes: Array = []; 170 | for (const s of _states) { 171 | if (s && allStates.indexOf(s) > -1) { 172 | validCodes.push(s); 173 | } 174 | } 175 | // remove duplicates 176 | validCodes = validCodes.filter((item, i) => validCodes.indexOf(item) === i); 177 | 178 | return validCodes; 179 | }; 180 | 181 | const calculateHolidays = (year: number): Holidays => { 182 | const easter = calculateEasterDate(year); 183 | const holidays: Holidays = { 184 | Neujahrstag: { 185 | date: moment(`${year}-01-01`), 186 | state: [], 187 | }, 188 | 'Heilige Drei Könige': { 189 | date: moment(`${year}-01-06`), 190 | state: ['BW', 'BY', 'ST'], 191 | }, 192 | Karfreitag: { 193 | date: moment(`${easter}`).subtract(2, 'days'), 194 | state: [], 195 | }, 196 | Ostersonntag: { 197 | date: moment(`${easter}`), 198 | state: ['BB'], 199 | }, 200 | Ostermontag: { 201 | date: moment(`${easter}`).add(1, 'days'), 202 | state: [], 203 | }, 204 | Maifeiertag: { 205 | date: moment(`${year}-05-01`), 206 | state: [], 207 | }, 208 | 'Christi Himmelfahrt': { 209 | date: moment(`${easter}`).add(39, 'days'), 210 | state: [], 211 | }, 212 | Pfingstsonntag: { 213 | date: moment(`${easter}`).add(49, 'days'), 214 | state: ['BB'], 215 | }, 216 | Pfingstmontag: { 217 | date: moment(`${easter}`).add(50, 'days'), 218 | state: [], 219 | }, 220 | Fronleichnam: { 221 | date: moment(`${easter}`).add(60, 'days'), 222 | state: ['BW', 'BY', 'HE', 'NW', 'RP', 'SL'], 223 | }, 224 | 'Mariä Himmelfahrt': { 225 | date: moment(`${year}-08-15`), 226 | state: ['SL'], 227 | }, 228 | 'Tag der deutschen Einheit': { 229 | date: moment(`${year}-10-03`), 230 | state: [], 231 | }, 232 | Reformationstag: { 233 | date: moment(`${year}-10-31`), 234 | state: ['BB', 'MV', 'SN', 'ST', 'TH'], 235 | }, 236 | Allerheiligen: { 237 | date: moment(`${year}-11-01`), 238 | state: ['BW', 'BY', 'NW', 'RP', 'SL'], 239 | }, 240 | 'Buß- und Bettag': { 241 | date: calculateBandBDate(year), 242 | state: ['SN'], 243 | }, 244 | '1. Weihnachtsfeiertag': { 245 | date: moment(`${year}-12-25`), 246 | state: [], 247 | }, 248 | '2. Weihnachtsfeiertag': { 249 | date: moment(`${year}-12-26`), 250 | state: [], 251 | }, 252 | }; 253 | 254 | // EXCEPTIONS and ADDITIONS 255 | // 2017 Reformationstag is holiday in all states 256 | if (+year === 2017) { 257 | holidays['Reformationstag'].state = []; 258 | } 259 | // since 2018 HB, HH, NI, SH celebrate 'Reformationstag' as well 260 | if (+year > 2017) { 261 | holidays['Reformationstag'].state = [ 262 | 'BB', 263 | 'HB', 264 | 'HH', 265 | 'MV', 266 | 'NI', 267 | 'SN', 268 | 'ST', 269 | 'SH', 270 | 'TH', 271 | ]; 272 | } 273 | // since 2019 new holidays 274 | if (+year > 2018) { 275 | holidays['Internationaler Frauentag'] = { 276 | date: moment(`${year}-03-08`), 277 | state: ['BE'], 278 | }; 279 | 280 | if (+year > 2022) { 281 | holidays['Internationaler Frauentag'].state.push('MV'); 282 | } 283 | 284 | holidays['Weltkindertag'] = { 285 | date: moment(`${year}-09-20`), 286 | state: ['TH'], 287 | }; 288 | } 289 | // one time only holiday in Berlin 290 | if (year == 2020) { 291 | holidays['Tag der Befreiung'] = { 292 | date: moment(`${year}-05-08`), 293 | state: ['BE'], 294 | }; 295 | } 296 | 297 | return holidays; 298 | }; 299 | 300 | const calculateEasterDate = (Y: number): string => { 301 | const C = Math.floor(Y / 100); 302 | const N = Y - 19 * Math.floor(Y / 19); 303 | const K = Math.floor((C - 17) / 25); 304 | let I = C - Math.floor(C / 4) - Math.floor((C - K) / 3) + 19 * N + 15; 305 | I = I - 30 * Math.floor(I / 30); 306 | // prettier-ignore 307 | I = I - Math.floor(I / 28) * (1 - Math.floor(I / 28) * Math.floor(29 / (I + 1)) * Math.floor((21 - N) / 11)); 308 | let J = Y + Math.floor(Y / 4) + I + 2 - C + Math.floor(C / 4); 309 | J = J - 7 * Math.floor(J / 7); 310 | const L = I - J; 311 | const M = 3 + Math.floor((L + 40) / 44); 312 | const D = L + 28 - 31 * Math.floor(M / 4); 313 | 314 | return `${Y}-${padout(M)}-${padout(D)}`; 315 | }; 316 | const padout = (num: number): string => { 317 | return num < 10 ? `0${num}` : `${num}`; 318 | }; 319 | 320 | const calculateBandBDate = function (year: number): moment.Moment { 321 | for (let i = 1; i < 8; i++) { 322 | const day = moment(`${year}-11-23`).subtract(i, 'days'); 323 | if (day.isoWeekday() === 3) { 324 | return day; 325 | } 326 | } 327 | return moment.invalid(); 328 | }; 329 | --------------------------------------------------------------------------------