soff-date
v1.2.0
Published
Lightweight, tree-shakeable holiday calculator with algorithmic date computation
Maintainers
Readme
Zero dependencies · TypeScript · ~3KB per locale
Table of Contents
- Table of Contents
- 🤔 Why?
- 📦 Install
- 🚀 Quick Start
- 🌍 i18n Support
- Available Locales
- Available Languages
- Shift Rules Explained
- Advanced: Create Your Own Locale
- Advanced: Use Algorithms Directly
- Bundle Size
- API Reference
- Business Days Calculation
- Types
- Contributing
- License
- Documentation
- Contributors
🤔 Why?
Most holiday libraries ship giant JSON files with dates until 2050. This library calculates dates algorithmically, supporting:
| Feature | Description | Example | | ---------------------- | ----------------------------- | ------------------------------- | | 📅 Fixed Dates | Static dates every year | December 25 (Christmas) | | 📆 Nth Weekday | Relative weekday calculations | 3rd Monday of January (MLK Day) | | ✨ Easter-relative | Based on Easter calculation | Good Friday = Easter - 2 days | | 🔄 Shift Rules | Move holidays to workdays | Colombia's Emiliani Law |
Result: Tiny bundle size (~3KB) with unlimited year support! 🎉
📦 Install
# npm
npm install soff-date
# pnpm
pnpm add soff-date
# yarn
yarn add soff-date
# bun
bun add soff-date🚀 Quick Start
// Only Colombia included in bundle (~3KB)
import { getHolidays, isHoliday, getNextHoliday } from 'soff-date/locales/co';
// 🏆 Get all holidays for a year
getHolidays(2025);
// → [{ date: '2025-01-01', key: 'newYear', name: 'Año Nuevo' }, ...]
// ❓ Check if a date is a holiday
isHoliday(new Date('2025-01-01'));
// → { date: '2025-01-01', key: 'newYear', name: 'Año Nuevo' }
isHoliday(new Date('2025-01-02'));
// → null
// ➡️ Get next holiday from a date
getNextHoliday(new Date('2025-01-02'));
// → { date: '2025-01-06', key: 'epiphany', name: 'Día de los Reyes Magos' }🌍 i18n Support
import { getHolidays } from 'soff-date/locales/co';
import { en } from 'soff-date/i18n/en';
getHolidays(2025, { lang: en });
// → [{ date: '2025-01-01', key: 'newYear', name: "New Year's Day" }, ...]
// Custom override
getHolidays(2025, { lang: { ...en, newYear: 'Happy New Year!' } });Available Locales
| Locale | Import | Holidays | Shift Rule |
| ------------ | ---------------------- | -------- | ------------------- |
| 🇨🇴 Colombia | soff-date/locales/co | 18 | Emiliani |
| 🇺🇸 USA | soff-date/locales/us | 10 | Observed |
| 🇲🇽 México | soff-date/locales/mx | 8 | NthWeekday + Custom |
| 🇦🇷 Argentina | soff-date/locales/ar | 16 | NearestMonday |
| 🇧🇷 Brasil | soff-date/locales/br | 13 | None |
If you need to select a locale dynamically, you can import all locale namespaces from the public barrel:
import { co, mx, us } from 'soff-date/locales';
co.getHolidays(2025);
mx.isHoliday(new Date('2025-09-16'));
us.getNextHoliday(new Date('2025-07-01'));Available Languages
| Language | Import |
| --------- | ------------------- |
| Español | soff-date/i18n/es |
| English | soff-date/i18n/en |
| Português | soff-date/i18n/pt |
Shift Rules Explained
Emiliani (Colombia, Argentina)
Holidays falling on weekends move to Monday.
// January 6, 2024 = Saturday → Monday January 8
{ date: '2024-01-08', key: 'epiphany', isShifted: true }Observed US (USA, UK)
- Saturday → Friday (before)
- Sunday → Monday (after)
// July 4, 2026 = Saturday → Friday July 3
{ date: '2026-07-03', key: 'independenceDayUS', isShifted: true }Nearest Monday (Argentina)
- Tuesday or Wednesday → previous Monday
- Thursday or Friday → next Monday
- Monday, Saturday and Sunday stay unchanged
// June 17, 2025 = Tuesday → Monday June 16
{ date: '2025-06-16', key: 'guemesDay', isShifted: true }
// November 20, 2025 = Thursday → Monday November 24
{ date: '2025-11-24', key: 'sovereigntyDay', isShifted: true }Mexico's Presidential Transition Holiday
Mexico includes a custom holiday for the transmission of executive power.
- From 2024 onward, it is observed on October 1 every 6 years: 2024, 2030, 2036, ...
- For historical years before 2024, the library preserves the previous December 1 cycle: 2018, 2012, ...
Advanced: Create Your Own Locale
import type { HolidayDefinition, Holiday, HolidayNames } from 'soff-date';
import { resolveHolidays, checkIsHoliday, findNextHoliday } from 'soff-date';
const definitions: HolidayDefinition[] = [
// Fixed date
{ key: 'newYear', rule: { type: 'fixed', month: 1, day: 1 } },
// Fixed with shift
{ key: 'christmas', rule: { type: 'fixed', month: 12, day: 25 }, shift: 'observedUS' },
// Nth weekday: 3rd Monday of January
{ key: 'mlkDay', rule: { type: 'nthWeekday', month: 1, weekday: 1, n: 3 } },
// Last Monday of May
{ key: 'memorialDay', rule: { type: 'nthWeekday', month: 5, weekday: 1, n: -1 } },
// Easter relative: Good Friday = Easter - 2
{ key: 'goodFriday', rule: { type: 'easterRelative', offset: -2 } },
// Custom calculation
{
key: 'custom',
rule: {
type: 'custom',
calc: (year) => new Date(year, 5, 15), // June 15
},
},
];
const names: HolidayNames = {
newYear: "New Year's Day",
christmas: 'Christmas',
// ...
};
export function getHolidays(year: number): Holiday[] {
return resolveHolidays(definitions, year, names);
}Advanced: Use Algorithms Directly
import { getEasterSunday, getNthWeekday, applyShift } from 'soff-date';
// Easter Sunday 2025
getEasterSunday(2025); // → Date(2025, 3, 20)
// 4th Thursday of November 2025 (Thanksgiving)
getNthWeekday(2025, 11, 4, 4); // → Date(2025, 10, 27)
// Apply observed shift
applyShift(new Date('2026-07-04'), 'observedUS');
// → { date: Date(2026-07-03), shifted: true }getNthWeekday() returns Invalid Date when the requested occurrence does not exist in the target month or when the input is out of range.
getEasterSunday() returns Invalid Date for unsupported years outside the Gregorian calendar (< 1583) and for non-integer years.
applyShift() preserves Invalid Date inputs and returns shifted: false instead of marking a move on invalid data.
Bundle Size
| Import | Size (minified) |
| ------------ | --------------- |
| locales/co | ~5.8KB |
| locales/us | ~4.5KB |
| i18n/es | ~1.9KB |
| i18n/en | ~1.1KB |
| Core only | ~2.7KB |
Tree-shaking ensures you only ship what you import.
API Reference
getHolidays(year, options?)
Returns all holidays for a given year.
Note: Some holidays might fall on the same date (e.g., a fixed holiday coinciding with a movable one). Always use
holiday.keyas the unique identifier, not the date.
interface GetHolidaysOptions {
lang?: HolidayNames; // Custom translations
}
interface Holiday {
date: string; // ISO date: '2025-01-01'
key: string; // Identifier: 'newYear'
name: string; // Display name: 'Año Nuevo'
isShifted?: boolean; // True if moved by shift rule
}year must be an integer. Non-integer values return an empty array.
isHoliday(date, options?)
Returns holiday info if the date is a holiday, null otherwise.
getNextHoliday(from?, options?)
Returns the next holiday from a given date (defaults to today).
formatRelativeTime(date, options?)
Formats a date relative to another date using Intl.RelativeTimeFormat.
import { formatRelativeTime } from 'soff-date';
formatRelativeTime(new Date('2024-01-02T12:00:00Z'), {
baseDate: new Date('2024-01-01T12:00:00Z'),
locale: 'en',
});
// → in 1 day
formatRelativeTime(new Date('2024-01-01T12:00:00Z'), {
baseDate: new Date('2024-01-02T12:00:00Z'),
locale: 'en',
numeric: 'auto',
});
// → yesterdayReturns an empty string for invalid dates instead of throwing.
Business Days Calculation
In addition to holiday calculation, soff-date provides utilities to work with business days (skipping weekends and holidays).
import { isBusinessDay, businessDays, diffBusinessDays } from 'soff-date/locales/co';
// Check if a date is a business day
isBusinessDay(new Date('2025-01-01')); // false (Holiday)
isBusinessDay(new Date('2025-01-04')); // false (Saturday)
isBusinessDay(new Date('2025-01-02')); // true
// Add business days
businessDays(new Date('2025-01-03'), 1);
// → Date('2025-01-06') (Friday + 1 business day = Monday)
// Calculate difference in business days
diffBusinessDays(new Date('2025-01-06'), new Date('2025-01-10'));
// → 4Invalid inputs are handled safely:
isBusinessDay(invalidDate)returnsfalsebusinessDays(invalidDate, amount)returnsInvalid DatebusinessDays(date, Infinity)returnsInvalid DatediffBusinessDays(invalidDate, date)returnsNaN
Types
type ShiftRule = 'none' | 'emiliani' | 'observedUS' | 'nextMonday' | 'nearestMonday';
type HolidayRule =
| { type: 'fixed'; month: number; day: number }
| { type: 'nthWeekday'; month: number; weekday: number; n: number }
| { type: 'easterRelative'; offset: number }
| { type: 'custom'; calc: (year: number) => Date | null };
interface HolidayDefinition {
key: string;
rule: HolidayRule;
shift?: ShiftRule;
}Contributing
Please read CONTRIBUTING.md for details on our code of conduct, and the process for submitting pull requests to us.
License
This project is licensed under the MIT License - see the LICENSE file for details.
Documentation
Contributors
Thanks goes to these wonderful people (emoji key):
This project follows the all-contributors specification. Contributions of any kind welcome!
