├── .gitignore ├── .eslintrc.json ├── .github └── workflows │ ├── publish.yml │ └── tests.yml ├── LICENSE.md ├── index.d.ts ├── package.json ├── CONTRIBUTING.md ├── index.js ├── README.md └── index.test.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .nyc_output 3 | coverage 4 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["airbnb-base", "prettier"], 3 | "env": { 4 | "node": true, 5 | "es6": true 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: publish release 2 | 3 | on: 4 | release: 5 | types: [published] 6 | 7 | permissions: 8 | id-token: write 9 | 10 | jobs: 11 | publish: 12 | runs-on: ubuntu-latest 13 | 14 | steps: 15 | - uses: actions/checkout@v3 16 | - uses: actions/setup-node@v3 17 | with: 18 | node-version: 20 19 | registry-url: https://registry.npmjs.org 20 | - name: update to latest npm 21 | run: npm i -g npm 22 | - name: install dependencies 23 | run: npm ci 24 | - name: publish 25 | run: npm publish --provenance --access public 26 | env: 27 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 28 | -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: tests 2 | 3 | on: [pull_request, push] 4 | 5 | jobs: 6 | test: 7 | strategy: 8 | matrix: 9 | node: [16, 18, 20] 10 | tz: [UTC, America/New_York, America/Los_Angeles, America/Phoenix, Asia/Hong_Kong] 11 | 12 | name: Node ${{ matrix.node }}, ${{ matrix.tz }} timezone 13 | 14 | runs-on: ubuntu-latest 15 | 16 | steps: 17 | - uses: actions/setup-node@v3 18 | with: 19 | node-version: ${{ matrix.node}} 20 | - uses: actions/checkout@v3 21 | - name: set timezone 22 | run: sudo timedatectl set-timezone ${{ matrix.tz }} 23 | - name: install dependencies 24 | run: npm install 25 | - name: lint 26 | run: npm run lint 27 | - name: test 28 | run: npm test 29 | 30 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | As a work of the United States Government, this project is in the 2 | public domain within the United States. 3 | 4 | Additionally, we waive copyright and related rights in the work 5 | worldwide through the CC0 1.0 Universal public domain dedication. 6 | 7 | ## CC0 1.0 Universal Summary 8 | 9 | This is a human-readable summary of the [Legal Code (read the full text)](https://creativecommons.org/publicdomain/zero/1.0/legalcode). 10 | 11 | ### No Copyright 12 | 13 | The person who associated a work with this deed has dedicated the work to 14 | the public domain by waiving all of his or her rights to the work worldwide 15 | under copyright law, including all related and neighboring rights, to the 16 | extent allowed by law. 17 | 18 | You can copy, modify, distribute and perform the work, even for commercial 19 | purposes, all without asking permission. 20 | 21 | ### Other Information 22 | 23 | In no way are the patent or trademark rights of any person affected by CC0, 24 | nor are the rights that other persons may have in the work or in how the 25 | work is used, such as publicity or privacy rights. 26 | 27 | Unless expressly stated otherwise, the person who associated a work with 28 | this deed makes no warranties about the work, and disclaims liability for 29 | all uses of the work, to the fullest extent permitted by applicable law. 30 | When using or citing the work, you should not imply endorsement by the 31 | author or the affirmer. 32 | -------------------------------------------------------------------------------- /index.d.ts: -------------------------------------------------------------------------------- 1 | interface WithHolidayShift { 2 | // Whether or not holidays that fall on Saturdays should be 3 | // shifted to Friday observance. If you don't follow the 4 | // US federal standard for observing holidays on weekends, 5 | // you can adjust by setting this value to false. 6 | // Default value is true. 7 | shiftSaturdayHolidays: boolean; 8 | 9 | // Whether or not holidays that fall on Sundays should be 10 | // shifted to Monday observance. If you don't follow the 11 | // US federal standard for observing holidays on weekends, 12 | // you can adjust by setting this value to false. 13 | // Default value is true. 14 | shiftSundayHolidays: boolean; 15 | } 16 | 17 | interface WithUTCDate { 18 | // Whether to treat the first argument as a UTC date instead 19 | // of the local time. Defaults to false. This is useful if 20 | // you're generating dates from UTC timestamps or otherwise 21 | // creating objects from UTC-based dates. 22 | // Default value is false. 23 | // This option only applies to the isAHoliday method. 24 | utc: boolean; 25 | } 26 | 27 | export interface Holiday { 28 | name: string; 29 | date: Date; 30 | dateString: string; 31 | } 32 | 33 | export function isAHoliday(date?: Date, params?: Partial): boolean; 34 | 35 | export function allForYear(year?: number, params?: Partial): Holiday[]; 36 | 37 | export function inRange(startDate?: Date, endDate?: Date, params?: Partial): Holiday[]; 38 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@18f/us-federal-holidays", 3 | "version": "4.0.0", 4 | "description": "All about US federal holidays", 5 | "main": "index.js", 6 | "keywords": [ 7 | "federal", 8 | "holiday", 9 | "holidays", 10 | "date" 11 | ], 12 | "contributors": [ 13 | "Greg Walker (https://github.com/mgwalker)", 14 | "Carter Baxter (https://github.com/tbaxter-18f)", 15 | "Yves Gurcan (https://github.com/yvesgurcan)", 16 | "CreateThis.com (https://github.com/createthis)", 17 | "Alexandr Rodik (https://github.com/arodik)", 18 | "Ian Speers (https://github.com/ian-speers)", 19 | "Tse Kit Yam (https://github.com/tsekityam)", 20 | "Ben Berry (https://github.com/bengerman13)", 21 | "Keith Pops (https://github.com/keithpops)" 22 | ], 23 | "repository": { 24 | "type": "git", 25 | "url": "git+ssh://git@github.com/18F/us-federal-holidays.git" 26 | }, 27 | "engines": { 28 | "node": ">=16.0.0" 29 | }, 30 | "scripts": { 31 | "lint": "eslint index.test.js index.js", 32 | "format": "prettier --write index.test.js index.js", 33 | "test": "tap --reporter=spec index.test.js" 34 | }, 35 | "license": "CC0-1.0", 36 | "devDependencies": { 37 | "eslint": "^8.24.0", 38 | "eslint-config-airbnb-base": "^15.0.0", 39 | "eslint-config-prettier": "^8.3.0", 40 | "eslint-plugin-import": "^2.17.2", 41 | "prettier": "^2.3.2", 42 | "tap": "^18.5.0" 43 | }, 44 | "dependencies": { 45 | "dayjs": "^1.10.6" 46 | }, 47 | "prettier": { 48 | "trailingComma": "none", 49 | "tabWidth": 2, 50 | "semi": true, 51 | "singleQuote": false, 52 | "arrowParens": "avoid" 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | ## Welcome! 2 | 3 | We're so glad you're thinking about contributing to an 18F open source project! 4 | If you're unsure about anything, just ask -- or submit the issue or pull 5 | request anyway. The worst that can happen is you'll be politely asked to 6 | change something. We love all friendly contributions. 7 | 8 | We want to ensure a welcoming environment for all of our projects. Our staff 9 | follow the [18F Code of Conduct](https://github.com/18F/code-of-conduct/blob/master/code-of-conduct.md) 10 | and all contributors should do the same. 11 | 12 | We encourage you to read this project's CONTRIBUTING policy (you are here), its 13 | [LICENSE](LICENSE.md), and its [README](README.md). 14 | 15 | If you have any questions or want to read more, check out the 16 | [18F Open Source Policy GitHub repository](https://github.com/18f/open-source-policy), 17 | or just [shoot us an email](mailto:18f@gsa.gov). 18 | 19 | ## Contributing 20 | 21 | We take contributions via Github pull requests into this repo. In order to 22 | create a pull request, you should first fork this repo into your own 23 | organization or user account. 24 | 25 | Make your changes to `index.js`. If you're changing or adding behaviors, modify 26 | or add tests to `index.test.js`. Run your tests with `npm test`. Once your 27 | changes are finished and all tests pass, commit all your changes and push them 28 | to your fork and open a pull request into this repo. Be sure to update 29 | `package.json` to add yourself to the list of contributors if you want! 30 | 31 | When you open your pull request, please tell us a little about what you're 32 | changing or adding so we can know what we're looking at. Someone from the 33 | 18F team will have to review the pull request before we can merge it which 34 | might take a little while - please be patient with us! 35 | 36 | ## Public domain 37 | 38 | This project is in the public domain within the United States, and 39 | copyright and related rights in the work worldwide are waived through 40 | the [CC0 1.0 Universal public domain dedication](https://creativecommons.org/publicdomain/zero/1.0/). 41 | 42 | All contributions to this project will be released under the CC0 43 | dedication. By submitting a pull request, you are agreeing to comply 44 | with this waiver of copyright interest. 45 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | const dayjs = require("dayjs"); 2 | const utcPlugin = require("dayjs/plugin/utc"); 3 | 4 | dayjs.extend(utcPlugin); 5 | 6 | const getDateFor = ({ day = 1, month, year }) => 7 | dayjs(`${year}-${month}-${day}`, "YYYY-M-D"); 8 | 9 | const getNthDayOf = (n, day, month, year) => { 10 | let result = dayjs(getDateFor({ month, year })).day(day); 11 | 12 | // dayjs.day(x) can return a time in the past (relative to the date being 13 | // operated on), because it returns a time from within the operand date's 14 | // current week. E.g.: 15 | // 16 | // date = July 1, 2021 # Thursday 17 | // dayjs(date).day(0) # Get Sunday 18 | // # returns June 27, 2021 19 | if (result.month() !== month - 1) { 20 | result = result.add(1, "week"); 21 | } 22 | 23 | result = result.add(n - 1, "week"); 24 | 25 | return result; 26 | }; 27 | 28 | const getLastDayOf = (day, month, year) => { 29 | const daysInMonth = dayjs(getDateFor({ month, year })).daysInMonth(); 30 | const lastDayOfMonth = dayjs(`${year}-${month}-${daysInMonth}`, "YYYY-M-D"); 31 | 32 | let result = lastDayOfMonth.day(day); 33 | 34 | // See above comment for more details. TL;DR is dayjs.day(x) is not 35 | // constrained to the same month as the operand object. 36 | if (result.month() !== month - 1) { 37 | result = result.subtract(1, "week"); 38 | } 39 | 40 | return result; 41 | }; 42 | 43 | const allFederalHolidaysForYear = ( 44 | year = new Date().getFullYear(), 45 | { shiftSaturdayHolidays = true, shiftSundayHolidays = true } = {} 46 | ) => { 47 | const holidays = []; 48 | 49 | // New Year's Day 50 | holidays.push({ 51 | name: `New Year's Day`, 52 | date: getDateFor({ day: 1, month: 1, year }) 53 | }); 54 | 55 | // Birthday of Martin Luther King, Jr. 56 | // Third Monday of January; fun fact: actual birthday is January 15 57 | holidays.push({ 58 | name: `Birthday of Martin Luther King, Jr.`, 59 | date: getNthDayOf(3, 1, 1, year) 60 | }); 61 | 62 | // Washington's Birthday 63 | // Third Monday of February; fun fact: actual birthday is February 22 64 | // Fun fact 2: officially "Washington's Birthday," not "President's Day" 65 | holidays.push({ 66 | name: `Washington's Birthday`, 67 | alsoObservedAs: "Presidents' Day", 68 | date: getNthDayOf(3, 1, 2, year) 69 | }); 70 | 71 | // Memorial Day 72 | // Last Monday of May 73 | holidays.push({ 74 | name: `Memorial Day`, 75 | date: getLastDayOf(1, 5, year) 76 | }); 77 | 78 | if (year > 2020) { 79 | // Juneteenth 80 | holidays.push({ 81 | name: `Juneteenth National Independence Day`, 82 | date: getDateFor({ day: 19, month: 6, year }) 83 | }); 84 | } 85 | 86 | // Independence Day 87 | holidays.push({ 88 | name: `Independence Day`, 89 | date: getDateFor({ day: 4, month: 7, year }) 90 | }); 91 | 92 | // Labor Day 93 | // First Monday in September 94 | holidays.push({ 95 | name: `Labor Day`, 96 | date: getNthDayOf(1, 1, 9, year) 97 | }); 98 | 99 | // Columbus Day 100 | // Second Monday in October 101 | holidays.push({ 102 | name: `Columbus Day`, 103 | alsoObservedAs: "Indigenous Peoples' Day", 104 | date: getNthDayOf(2, 1, 10, year) 105 | }); 106 | 107 | // Veterans Day 108 | holidays.push({ 109 | name: `Veterans Day`, 110 | date: getDateFor({ day: 11, month: 11, year }) 111 | }); 112 | 113 | // Thanksgiving Day 114 | // Fourth Thursday of November 115 | holidays.push({ 116 | name: `Thanksgiving Day`, 117 | date: getNthDayOf(4, 4, 11, year) 118 | }); 119 | 120 | // Christmas Day 121 | holidays.push({ 122 | name: `Christmas Day`, 123 | date: getDateFor({ day: 25, month: 12, year }) 124 | }); 125 | 126 | return holidays.map(holiday => { 127 | let date = dayjs(holiday.date); 128 | 129 | if (date.day() === 0 && shiftSundayHolidays) { 130 | // Actual holiday falls on Sunday. Shift the observed date forward to 131 | // Monday. 132 | date = date.add(1, "day"); 133 | } 134 | 135 | if (date.day() === 6 && shiftSaturdayHolidays) { 136 | // Actual holiday falls on Saturday. Shift the observed date backward 137 | // to Friday. 138 | date = date.subtract(1, "day"); 139 | } 140 | 141 | return { 142 | name: holiday.name, 143 | alsoObservedAs: holiday.alsoObservedAs, 144 | date: date.toDate(), 145 | dateString: date.format("YYYY-MM-DD") 146 | }; 147 | }); 148 | }; 149 | 150 | const isAHoliday = ( 151 | date = new Date(), 152 | { shiftSaturdayHolidays = true, shiftSundayHolidays = true, utc = false } = {} 153 | ) => { 154 | const newDate = utc ? dayjs.utc(date) : dayjs(date); 155 | const year = newDate.year(); 156 | 157 | const shift = { shiftSaturdayHolidays, shiftSundayHolidays }; 158 | 159 | // Get the holidays this year, plus check if New Year's Day of next year is 160 | // observed on December 31 and if so, add it to this year's list. 161 | const allForYear = allFederalHolidaysForYear(year, shift); 162 | const nextYear = allFederalHolidaysForYear(year + 1, shift); 163 | allForYear.push(nextYear[0]); 164 | 165 | // If any dates in this year's holiday list match the one passed in, then 166 | // the passed-in date is a holiday. Otherwise, it is not. 167 | return allForYear.some( 168 | holiday => holiday.dateString === newDate.format("YYYY-MM-DD") 169 | ); 170 | }; 171 | 172 | const getOneYearFromNow = () => { 173 | const future = new Date(); 174 | future.setUTCFullYear(future.getUTCFullYear() + 1); 175 | return future; 176 | }; 177 | 178 | const federalHolidaysInRange = ( 179 | startDate = new Date(), 180 | endDate = getOneYearFromNow(), 181 | options = undefined 182 | ) => { 183 | const startYear = startDate.getFullYear(); 184 | const endYear = endDate.getFullYear(); 185 | 186 | const candidates = []; 187 | for (let year = startYear; year <= endYear; year += 1) { 188 | candidates.push(...allFederalHolidaysForYear(year, options)); 189 | } 190 | return candidates.filter(h => h.date >= startDate && h.date <= endDate); 191 | }; 192 | 193 | module.exports = { 194 | isAHoliday, 195 | allForYear: allFederalHolidaysForYear, 196 | inRange: federalHolidaysInRange 197 | }; 198 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # US Federal Holidays 2 | 3 | Builds and returns a list of all US federal holidays for a given year, and 4 | provides a helper method to determine if a given date is a US federal holiday. 5 | Handles shifting holidays to the nearest weekday if the holiday falls on a 6 | weekend. 7 | 8 | US federal holidays are [as defined by OPM](https://www.opm.gov/policy-data-oversight/pay-leave/federal-holidays/). 9 | 10 | ### Installation 11 | 12 | ``` 13 | npm install @18f/us-federal-holidays 14 | ``` 15 | 16 | Requires Node.js 12 or higher. 17 | 18 | ### Usage 19 | 20 | To get a list of all US federal holidays in a given year, use the `allForYear` 21 | method. If no year is passed in, uses the current year. 22 | 23 | ```javascript 24 | const fedHolidays = require('@18f/us-federal-holidays'); 25 | 26 | const options = { shiftSaturdayHolidays: true, shiftSundayHolidays: true }; 27 | const holidays = fedHolidays.allForYear(2016, options); 28 | 29 | // Returns 30 | [ { name: 'New Year\'s Day', 31 | date: 2016-01-01T00:00:00.000Z, 32 | dateString: '2016-1-1' }, 33 | { name: 'Birthday of Martin Luther King, Jr.', 34 | date: 2016-01-18T00:00:00.000Z, 35 | dateString: '2016-1-18' }, 36 | { name: 'Washington\'s Birthday', 37 | alsoObservedAs: 'Presidents\' Day', 38 | date: 2016-02-15T00:00:00.000Z, 39 | dateString: '2016-2-15' }, 40 | { name: 'Memorial Day', 41 | date: 2016-05-30T00:00:00.000Z, 42 | dateString: '2016-5-30' }, 43 | { 44 | name: 'Juneteenth National Independence Day', 45 | date: 2016-06-20T00:00:00.000Z, 46 | dateString: '2016-6-20' }, 47 | { name: 'Independence Day', 48 | date: 2016-07-04T00:00:00.000Z, 49 | dateString: '2016-7-4' }, 50 | { name: 'Labor Day', 51 | date: 2016-09-05T00:00:00.000Z, 52 | dateString: '2016-9-5' }, 53 | { name: 'Columbus Day', 54 | alsoObservedAs: 'Indigenous Peoples\' Day', 55 | date: 2016-10-10T00:00:00.000Z, 56 | dateString: '2016-10-10' }, 57 | { name: 'Veterans Day', 58 | date: 2016-11-11T00:00:00.000Z, 59 | dateString: '2016-11-11' }, 60 | { name: 'Thanksgiving Day', 61 | date: 2016-11-24T00:00:00.000Z, 62 | dateString: '2016-11-24' }, 63 | { name: 'Christmas Day', 64 | date: 2016-12-26T00:00:00.000Z, 65 | dateString: '2016-12-26' } ] 66 | ``` 67 | 68 | To get a list of all US federal holidays within a date range, use the `inRange` 69 | method. If no `start` date is provided in, uses the current date. If the end 70 | date is omitted, one year from the current date is used. 71 | 72 | ```javascript 73 | const fedHolidays = require('@18f/us-federal-holidays'); 74 | const start = new Date('2016-02-13'); 75 | const end = new Date('2017-07-23'); 76 | const options = { shiftSaturdayHolidays: true, shiftSundayHolidays: true }; 77 | const holidays = fedHolidays.inRange(start, end, options); 78 | 79 | // Returns 80 | [ { name: 'Washington\'s Birthday', 81 | date: 2016-02-15T00:00:00.000Z, 82 | dateString: '2016-2-15' }, 83 | { name: 'Memorial Day', 84 | date: 2016-05-30T00:00:00.000Z, 85 | dateString: '2016-5-30' }, 86 | { 87 | name: 'Juneteenth National Independence Day' 88 | date: 2016-06-20T00:00:00.000Z, 89 | dateString: '2016-6-20' }, 90 | { name: 'Independence Day', 91 | date: 2016-07-04T00:00:00.000Z, 92 | dateString: '2016-7-4' }, 93 | { name: 'Labor Day', 94 | date: 2016-09-05T00:00:00.000Z, 95 | dateString: '2016-9-5' }, 96 | { name: 'Columbus Day', 97 | alsoObservedAs: 'Indigenous Peoples\' Day', 98 | date: 2016-10-10T00:00:00.000Z, 99 | dateString: '2016-10-10' }, 100 | { name: 'Veterans Day', 101 | date: 2016-11-11T00:00:00.000Z, 102 | dateString: '2016-11-11' }, 103 | { name: 'Thanksgiving Day', 104 | date: 2016-11-24T00:00:00.000Z, 105 | dateString: '2016-11-24' }, 106 | { name: 'Christmas Day', 107 | date: 2016-12-26T00:00:00.000Z, 108 | dateString: '2016-12-26' }, 109 | { name: 'New Year\'s Day', 110 | date: 2017-01-02T00:00:00.000Z, 111 | dateString: '2017-1-2' }, 112 | { name: 'Birthday of Martin Luther King, Jr.', 113 | date: 2017-01-16T00:00:00.000Z, 114 | dateString: '2017-1-16' }, 115 | { name: 'Washington\'s Birthday', 116 | alsoObservedAs: 'Presidents\' Day', 117 | date: 2017-02-20T00:00:00.000Z, 118 | dateString: '2017-2-20' }, 119 | { name: 'Memorial Day', 120 | date: 2017-05-29T00:00:00.000Z, 121 | dateString: '2017-5-29' }, 122 | { 123 | name: 'Juneteenth National Independence Day' 124 | date: 2017-06-19T00:00:00.000Z, 125 | dateString: '2017-6-19' }, 126 | { name: 'Independence Day', 127 | date: 2017-07-04T00:00:00.000Z, 128 | dateString: '2017-7-4' } ] 129 | ``` 130 | 131 | To determine if a date is a federal holiday, use the `isAHoliday` method. If no 132 | argument is provided, defaults to the current date: 133 | 134 | ```javascript 135 | const fedHolidays = require("@18f/us-federal-holidays"); 136 | 137 | const options = { 138 | shiftSaturdayHolidays: true, 139 | shiftSundayHolidays: true, 140 | utc: false 141 | }; 142 | const isAHoliday = fedHolidays.isAHoliday(myDate, options); 143 | // Returns true or false 144 | ``` 145 | 146 | All three methods take `options` as a second argument. This argument is a plain 147 | object which accepts the following properties: 148 | 149 | ```javascript 150 | { 151 | // Whether or not holidays that fall on Saturdays should be 152 | // shifted to Friday observance. If you don't follow the 153 | // US federal standard for observing holidays on weekends, 154 | // you can adjust by setting this value to false. 155 | // Default value is true. 156 | shiftSaturdayHolidays: boolean, 157 | 158 | // Whether or not holidays that fall on Sundays should be 159 | // shifted to Monday observance. If you don't follow the 160 | // US federal standard for observing holidays on weekends, 161 | // you can adjust by setting this value to false. 162 | // Default value is true. 163 | shiftSundayHolidays: boolean 164 | } 165 | ``` 166 | 167 | Additionally, `isAHoliday` takes an `options.utc` parameter: 168 | 169 | ```javascript 170 | { 171 | // Whether to treat the first argument as a UTC date instead 172 | // of the local time. Defaults to false. This is useful if 173 | // you're generating dates from UTC timestamps or otherwise 174 | // creating objects from UTC-based dates. 175 | // Default value is false. 176 | // This option only applies to the isAHoliday method. 177 | utc: boolean; 178 | } 179 | ``` 180 | 181 | ### Public domain 182 | 183 | This project is in the worldwide [public domain](LICENSE.md). As stated in 184 | [CONTRIBUTING](CONTRIBUTING.md): 185 | 186 | > This project is in the public domain within the United States, and copyright 187 | > and related rights in the work worldwide are waived through the 188 | > [CC0 1.0 Universal public domain dedication](https://creativecommons.org/publicdomain/zero/1.0/). 189 | > 190 | > All contributions to this project will be released under the CC0 dedication. 191 | > By submitting a pull request, you are agreeing to comply with this waiver of 192 | > copyright interest. 193 | -------------------------------------------------------------------------------- /index.test.js: -------------------------------------------------------------------------------- 1 | const customParseFormat = require("dayjs/plugin/customParseFormat"); 2 | const dayjs = require("dayjs"); 3 | const tap = require("tap"); 4 | 5 | // The customParseFormat plugin changes the way the dayjs() utility method 6 | // handles format strings. Specifically, MM and DD format tokens will REQUIRE 7 | // two-digit months and days, whereas the default constructor will happily take 8 | // single-digit months and days. Add the plugin to our tests to make sure we 9 | // still work in environments where the plugin is being used. 10 | dayjs.extend(customParseFormat); 11 | 12 | const federalHolidays = require("./index"); 13 | 14 | const getDate = dateString => new Date(`${dateString} 00:00:00`); 15 | 16 | const getDateUTC = dateString => new Date(`${dateString}T00:00:00Z`); 17 | 18 | tap.test("handles standard federal holidays", async tests => { 19 | tests.test( 20 | "gets observed holidays, accounting for actual holidays on weekends", 21 | async test => { 22 | [ 23 | "2010-12-31", // New Year's Day falls on a Saturday, observed before 24 | "2012-01-02", // New Year's Day falls on a Sunday, observed after 25 | "2014-01-01", 26 | "2014-01-20", 27 | "2014-02-17", 28 | "2014-05-26", 29 | "2014-07-04", 30 | "2014-09-01", 31 | "2014-10-13", 32 | "2014-11-11", 33 | "2014-11-27", 34 | "2014-12-25", 35 | "2015-07-03", // Independence Day falls on a Saturday, observed before 36 | "2016-12-26", // Christmas Day falls on a Sunday, observed after 37 | "2017-12-25", 38 | "2021-05-31" // https://github.com/18F/us-federal-holidays/issues/28 39 | ].forEach(dateString => { 40 | const date = getDate(dateString); 41 | const utcDate = getDateUTC(dateString); 42 | 43 | test.ok( 44 | federalHolidays.isAHoliday(date), 45 | `${dateString} is a holiday (observed)` 46 | ); 47 | 48 | test.ok( 49 | federalHolidays.isAHoliday(utcDate, { utc: true }), 50 | `${dateString} UTC is a holiday (observed)` 51 | ); 52 | }); 53 | } 54 | ); 55 | 56 | tests.test( 57 | "actual holidays on weekends are not listed as observed holidays", 58 | async test => { 59 | [ 60 | "2011-01-01", // New Year's Day falls on a Saturday, so is not a holiday 61 | "2012-01-01", // New Year's Day falls on a Sunday, so is not a holiday 62 | "2015-07-04", // Independence Day falls on a Saturday, so is not a holiday 63 | "2016-12-25", // Christmas Day falls on a Sunday, so is not a holiday 64 | "2021-05-24" // https://github.com/18F/us-federal-holidays/issues/28 65 | ].forEach(dateString => { 66 | const date = getDate(dateString); 67 | const utcDate = getDateUTC(dateString); 68 | 69 | test.notOk( 70 | federalHolidays.isAHoliday(date), 71 | `${dateString} is not a holiday (observed)` 72 | ); 73 | 74 | test.notOk( 75 | federalHolidays.isAHoliday(utcDate, { utc: true }), 76 | `${dateString} UTC is not a holiday (observed)` 77 | ); 78 | }); 79 | } 80 | ); 81 | 82 | tests.test("Juneteenth is only included from 2021 onwards", async test => { 83 | test.notOk( 84 | federalHolidays.isAHoliday(getDate("2020-06-19")), 85 | "Juneteenth is not a holiday in 2020" 86 | ); 87 | 88 | // In 2021, Juneteenth fell on a Saturday, so the observed holiday was the 89 | // 18th instead of the 19th. 90 | test.ok( 91 | federalHolidays.isAHoliday(getDate("2021-06-18")), 92 | "Juneteenth is a holiday in 2021" 93 | ); 94 | 95 | // 2023 is the first year that the observation of Juneteenth falls on the 96 | // actual holiday. 97 | test.ok( 98 | federalHolidays.isAHoliday(getDate("2023-06-19")), 99 | "Juneteenth is a holiday in 2023" 100 | ); 101 | 102 | test.notOk( 103 | federalHolidays 104 | .allForYear(2020) 105 | .some(({ name }) => name === "Juneteenth National Independence Day"), 106 | "Juneteenth is not included in the list of holidays for 2020" 107 | ); 108 | 109 | test.ok( 110 | federalHolidays 111 | .allForYear(2021) 112 | .some(({ name }) => name === "Juneteenth National Independence Day"), 113 | "Juneteenth is included in the list of holidays for 2021" 114 | ); 115 | }); 116 | 117 | tests.test( 118 | "honors requests not to shift holidays on weekends", 119 | async test => { 120 | test.ok( 121 | federalHolidays.isAHoliday(getDate("2011-01-01"), { 122 | shiftSaturdayHolidays: false 123 | }), 124 | `2011-01-01 is a holiday if Sundays are not shifted` 125 | ); 126 | test.ok( 127 | federalHolidays.isAHoliday(getDate("2012-01-01"), { 128 | shiftSundayHolidays: false 129 | }), 130 | `2012-01-01 is a holiday if Sundays are not shifted` 131 | ); 132 | test.ok( 133 | federalHolidays.isAHoliday(getDate("2015-07-04"), { 134 | shiftSaturdayHolidays: false 135 | }), 136 | `2015-07-04 is a holiday if Saturdays are not shifted` 137 | ); 138 | test.ok( 139 | federalHolidays.isAHoliday(getDate("2016-12-25"), { 140 | shiftSundayHolidays: false 141 | }), 142 | `2016-12-25 is a holiday if Saturdays are not shifted` 143 | ); 144 | } 145 | ); 146 | 147 | tests.test( 148 | "handles federal holidays within a range (Saturday and Sunday holidays shifted)", 149 | async test => { 150 | const holidays = federalHolidays.inRange( 151 | new Date("2015-07-03"), 152 | new Date("2016-12-26") 153 | ); 154 | holidays.forEach(holiday => { 155 | test.ok( 156 | federalHolidays.isAHoliday(getDate(holiday.dateString)), 157 | `${holiday.dateString} is a holiday (observed)` 158 | ); 159 | 160 | // Make sure the alsoObservedAs property gets pulled through 161 | if ( 162 | holiday.name === "Washington's Birthday" || 163 | holiday.name === "Columbus Day" 164 | ) { 165 | test.ok(holiday.alsoObservedAs); 166 | } 167 | }); 168 | } 169 | ); 170 | 171 | tests.test( 172 | "handles federal holidays within a range (Saturday shifted only)", 173 | async test => { 174 | const shiftSaturdayHolidays = true; 175 | const shiftSundayHolidays = false; 176 | const holidays = federalHolidays.inRange( 177 | new Date("2015-07-03"), 178 | new Date("2016-12-26"), 179 | { shiftSaturdayHolidays, shiftSundayHolidays } 180 | ); 181 | holidays.forEach(holiday => { 182 | test.ok( 183 | federalHolidays.isAHoliday(getDate(holiday.dateString), { 184 | shiftSaturdayHolidays, 185 | shiftSundayHolidays 186 | }), 187 | `${holiday.dateString} is a holiday (observed)` 188 | ); 189 | }); 190 | } 191 | ); 192 | 193 | tests.test( 194 | "handles federal holidays within a range (Sunday shifted only)", 195 | async test => { 196 | const shiftSaturdayHolidays = false; 197 | const shiftSundayHolidays = true; 198 | const holidays = federalHolidays.inRange( 199 | new Date("2015-07-03"), 200 | new Date("2016-12-26"), 201 | { shiftSaturdayHolidays, shiftSundayHolidays } 202 | ); 203 | holidays.forEach(holiday => { 204 | test.ok( 205 | federalHolidays.isAHoliday(getDate(holiday.dateString), { 206 | shiftSaturdayHolidays, 207 | shiftSundayHolidays 208 | }), 209 | `${holiday.dateString} is a holiday (observed)` 210 | ); 211 | }); 212 | } 213 | ); 214 | 215 | tests.test( 216 | "handles federal holidays within a range (none shifted)", 217 | async test => { 218 | const shiftSaturdayHolidays = false; 219 | const shiftSundayHolidays = false; 220 | const holidays = federalHolidays.inRange( 221 | new Date("2015-07-03"), 222 | new Date("2016-12-26"), 223 | { shiftSaturdayHolidays, shiftSundayHolidays } 224 | ); 225 | holidays.forEach(holiday => { 226 | test.ok( 227 | federalHolidays.isAHoliday(getDate(holiday.dateString), { 228 | shiftSaturdayHolidays, 229 | shiftSundayHolidays 230 | }), 231 | `${holiday.dateString} is a holiday (observed)` 232 | ); 233 | }); 234 | } 235 | ); 236 | 237 | tests.test("handles default dates and ranges", async defaultTests => { 238 | const testYear = 2000; 239 | let GlobalDate; 240 | 241 | // Create a proxy for the global Date object. This way we can control the 242 | // date that is created for default arguments. 243 | defaultTests.beforeEach(() => { 244 | GlobalDate = global.Date; 245 | 246 | const ProxiedDate = new Proxy(Date, { 247 | construct: (_, args) => { 248 | // We only want to override the constructor if there aren't any 249 | // arguments. In that case, use our magic date of July 1. Otherwise, 250 | // pass the arguments on to the real Date constructor. 251 | if (args.length === 0) { 252 | return new GlobalDate(`${testYear}-07-01T00:00:00.000Z`); 253 | } 254 | return new GlobalDate(...args); 255 | } 256 | }); 257 | 258 | global.Date = ProxiedDate; 259 | }); 260 | 261 | defaultTests.afterEach(() => { 262 | // Put the real Date object back. 263 | global.Date = GlobalDate; 264 | }); 265 | 266 | // We've already tested isAHoliday and allForYear with args, so let's assume 267 | // they're correct. If not, our earlier tests should have caught that. If 268 | // they didn't... uhoh. 269 | 270 | defaultTests.test("indicates whether today is a holiday", async test => { 271 | // July 1 should not be a holiday in any year. 272 | test.same(federalHolidays.isAHoliday(), false, "is not a holiday"); 273 | }); 274 | 275 | defaultTests.test( 276 | "fetches all holidays for the current year", 277 | async test => { 278 | const expected = federalHolidays.allForYear(testYear); 279 | 280 | const holidays = federalHolidays.allForYear(); 281 | test.match(holidays, expected, "gets the expected holidays"); 282 | test.same( 283 | holidays.length, 284 | expected.length, 285 | "gets exactly the expected holidays" 286 | ); 287 | } 288 | ); 289 | 290 | defaultTests.test( 291 | "defaults to a range from now to one year from now", 292 | async test => { 293 | const holidays = federalHolidays.inRange(); 294 | 295 | // Safe-ify these tests against changing test years. In 2021, Juneteenth 296 | // was added to the holiday calendar. If the test year is before 2020, 297 | // there are 4 holidays preceding July 1, so we start our slice at 298 | // holiday #5 (index 4). From 2021 onwards, there are 5 holidays before 299 | // July 1, so we start our slice at holiday #6 (index 5). 300 | const slice = testYear > 2020 ? 5 : 4; 301 | 302 | const expected = [ 303 | ...federalHolidays.allForYear(testYear).slice(slice), 304 | ...federalHolidays.allForYear(testYear + 1).slice(0, slice) 305 | ]; 306 | 307 | test.match(holidays, expected, "get the expected holidays"); 308 | test.same( 309 | holidays.length, 310 | expected.length, 311 | "gets exactly the expected holidays" 312 | ); 313 | } 314 | ); 315 | }); 316 | }); 317 | --------------------------------------------------------------------------------