swiss_holidays 0.1.0
swiss_holidays: ^0.1.0 copied to clipboard
Swiss public holidays for Dart — cantonal and communal, with a cited legal source for every entry, half-day start times, and working-day arithmetic.
example/swiss_holidays_example.dart
// Run with: dart run example/swiss_holidays_example.dart
//
// NOT LEGAL ADVICE. Collective labour agreements (CCL/GAV/CCT) can add holidays
// and are out of scope. Verify against the cited sources before using this for
// payroll. See README.md and GAPS.md.
import 'package:swiss_holidays/swiss_holidays.dart';
void main() {
print('Coverage: ${SwissHolidays.coveredCantons.toList()..sort()}');
print('Communes: ${SwissHolidays.coveredCommunes}\n');
_cantonsDiffer();
_yearListing();
_halfDays();
_legalEffect();
_workingDays();
_citations();
_historyMatters();
_uncoveredScope();
}
/// The same date is a holiday in one canton and a working day in the other.
void _cantonsDiffer() {
print('--- Same date, different answer ---');
const dates = {
'Good Friday 2026': (2026, 4, 3),
'Corpus Christi 2026': (2026, 6, 4),
'Epiphany 2026': (2026, 1, 6),
'Christmas 2026': (2026, 12, 25),
};
for (final entry in dates.entries) {
final (y, m, d) = entry.value;
final date = DateTime(y, m, d);
final zh = SwissHolidays.isHoliday(date, 'CH-ZH').status.name;
final ti = SwissHolidays.isHoliday(date, 'CH-TI').status.name;
print(' ${entry.key.padRight(22)} ZH=${zh.padRight(5)} TI=$ti');
}
print('');
}
void _yearListing() {
print('--- Ticino 2026 (${SwissHolidays.holidaysIn(2026, 'CH-TI').length} '
'official holidays) ---');
for (final o in SwissHolidays.holidaysIn(2026, 'CH-TI')) {
print(' ${_iso(o.date)} ${_weekday(o.date)} ${o.holiday.name('it')}');
}
print('');
}
/// Half days carry a clock time, not a boolean.
void _halfDays() {
print('--- Stadt Zürich (commune 261) half days in 2026 ---');
for (final o in SwissHolidays.holidaysIn(2026, 'CH-ZH', communeId: 261)) {
if (o.status != HolidayStatus.half) continue;
final observance = o.holiday.observance as HalfDay;
print(' ${_iso(o.date)} ${o.holiday.name('de')}');
print(' free from ${o.startsAt} (time_basis: '
'${observance.timeBasis.name})');
if (observance.timeBasis == TimeBasis.derived) {
print(' NOTE: the source says only "Nachmittag" — this time is '
'derived, not quoted.');
}
}
// These bind the City towards its own staff, not the general public.
final generalOnly = SwissHolidays.holidaysIn(
2026,
'CH-ZH',
communeId: 261,
include: {AppliesTo.generalPublic},
);
print(' Filtering to generalPublic leaves ${generalOnly.length} entries '
'(the cantonal nine).\n');
}
/// Being a holiday and attracting the Labour Act's Sunday rules differ.
void _legalEffect() {
print('--- Legal effect: Ticino 2026 ---');
final all = SwissHolidays.holidaysIn(2026, 'CH-TI');
final sundayEquated =
all.where((o) => o.legalEffect == LegalEffect.sundayEquivalent).toList();
final restDaysOnly =
all.where((o) => o.legalEffect == LegalEffect.cantonalRestDay).toList();
print(' ${all.length} official holidays, of which:');
print(' ${sundayEquated.length} Sunday-equated (Art. 6 lit. a LALL) '
'- Art. 18-19 LL apply');
print(' ${restDaysOnly.length} not Sunday-equated (Art. 6 lit. b LALL) '
'- those rules do NOT apply');
print(' Art. 20a LL caps a canton at nine, so nine is the ceiling.');
print(' Not Sunday-equated:');
for (final o in restDaysOnly) {
print(' ${_iso(o.date)} ${o.holiday.name('it')}');
}
// The same calendar date answers a payroll question and a booking question
// differently.
final corpusDomini = SwissHolidays.isHoliday(DateTime(2026, 6, 4), 'CH-TI');
print(' Corpus Domini 2026: status=${corpusDomini.status.name}, '
'isSundayEquivalent=${corpusDomini.isSundayEquivalent}');
// 1 May is Sunday-equated in Zurich but not in Ticino.
final mayFirst = DateTime(2026, 5, 1);
print(' 1 May 2026: ZH sundayEquivalent='
'${SwissHolidays.isHoliday(mayFirst, 'CH-ZH').isSundayEquivalent}, '
'TI sundayEquivalent='
'${SwissHolidays.isHoliday(mayFirst, 'CH-TI').isSundayEquivalent}');
print('');
}
void _workingDays() {
print('--- Working-day arithmetic ---');
final thu = DateTime(2026, 4, 2);
print(' From ${_iso(thu)} (Thursday), +1 working day:');
print(' CH-ZH -> ${_iso(SwissHolidays.addWorkingDays(thu, 1, 'CH-ZH'))} '
'(Good Friday, weekend, Easter Monday all skipped)');
print(' CH-TI -> ${_iso(SwissHolidays.addWorkingDays(thu, 1, 'CH-TI'))} '
'(no Good Friday in Ticino)');
final xmasEve = DateTime(2026, 12, 24);
print(' nextWorkingDay after ${_iso(xmasEve)}:');
print(' CH-ZH -> ${_iso(SwissHolidays.nextWorkingDay(xmasEve, 'CH-ZH'))}');
print(' 20 working days from 2026-01-05:');
print(' CH-ZH -> '
'${_iso(SwissHolidays.addWorkingDays(DateTime(2026, 1, 5), 20, 'CH-ZH'))}');
print(' CH-TI -> '
'${_iso(SwissHolidays.addWorkingDays(DateTime(2026, 1, 5), 20, 'CH-TI'))}');
// Holidays on a weekend are not moved: Switzerland has no such rule.
final aug1 = DateTime(2026, 8, 1);
print(' 1 August 2026 is a ${_weekday(aug1)} and is NOT shifted; '
'Mon 3 Aug is a working day: '
'${SwissHolidays.isWorkingDay(DateTime(2026, 8, 3), 'CH-ZH')}\n');
}
/// Every entry can say why it exists.
void _citations() {
print('--- Provenance ---');
final corpusDomini = SwissHolidays.isHoliday(DateTime(2026, 6, 4), 'CH-TI')
.occurrences
.single
.holiday;
print(' ${corpusDomini.name('it')} (${corpusDomini.id})');
for (final b in corpusDomini.legalBasis) {
print(' [${b.kind.name}] ${b.citation}');
print(' ${b.url}');
print(' checked ${_iso(b.checkedOn)}');
}
print('');
}
/// A query about a past year uses the rules in force then.
void _historyMatters() {
print('--- Validity windows ---');
for (final year in [2009, 2010, 2015, 2026]) {
final ti = SwissHolidays.holidaysIn(year, 'CH-TI').length;
final zh = SwissHolidays.holidaysIn(year, 'CH-ZH').length;
print(' $year: CH-TI $ti entries, CH-ZH $zh entries');
}
print(
' Ticino RL 843.200 entered into force 2010-02-09; the 1934 decree it');
print(' replaced is not sourced, so 2009 is empty rather than guessed.\n');
}
/// Uncovered scopes are empty, which callers must detect rather than trust.
void _uncoveredScope() {
print('--- Uncovered cantons ---');
const canton = 'CH-BE';
if (!SwissHolidays.coveredCantons.contains(canton)) {
print(' $canton is NOT covered. isHoliday would return '
'"${SwissHolidays.isHoliday(DateTime(2026, 12, 25), canton).status.name}"'
' for Christmas —');
print(' indistinguishable from a real answer. Always check '
'coveredCantons first. See GAPS.md.');
}
}
String _iso(DateTime d) => '${d.year.toString().padLeft(4, '0')}-'
'${d.month.toString().padLeft(2, '0')}-'
'${d.day.toString().padLeft(2, '0')}';
const _weekdayNames = [
'Monday',
'Tuesday',
'Wednesday',
'Thursday',
'Friday',
'Saturday',
'Sunday',
];
String _weekday(DateTime d) => _weekdayNames[d.weekday - 1];