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.
Swiss public holidays for Dart — cantonal and communal, with legal sources #
Swiss public holidays are set by cantonal law, and sometimes at commune level. There is no single national list. Good Friday is a holiday in Zürich and not in Ticino; Corpus Christi is the reverse. Zürich city staff get the afternoon of Sechseläuten off, on a date that is not the third Monday in April in four of the next ten years.
This package encodes that, and — the point of it — every single entry carries a citation to an official source with a URL that was actually fetched.
import 'package:swiss_holidays/swiss_holidays.dart';
// Corpus Domini: a holiday in Ticino, an ordinary working day in Zürich.
SwissHolidays.isHoliday(DateTime(2026, 6, 4), 'CH-TI').status; // full
SwissHolidays.isHoliday(DateTime(2026, 6, 4), 'CH-ZH').status; // none
// Half days carry the hour, because a time-tracking system needs it.
final s = SwissHolidays.isHoliday(DateTime(2026, 4, 20), 'CH-ZH', communeId: 261);
s.status; // half
s.startsAt; // "12:00"
// Why is it a holiday? Ask.
s.occurrences.single.holiday.legalBasis.first.citation;
// Stadt Zürich, Ausführungsbestimmungen zum Personalrecht (AB PR),
// AS 177.101, Art. 169 Abs. 1; Stadtratsbeschluss vom 27. März 2002 (447) …
Not legal advice #
This package is a convenience, not a legal authority.
- Collective labour agreements (CCL / GAV / CCT) can add holidays and are entirely out of scope. So can individual contracts and company rules. A payroll system must layer those on top of this data.
- Verify against the cited sources before using this for payroll, invoicing deadlines, or anything else with money or legal consequences attached. Every entry exposes its
legalBasiswith a URL and acheckedOndate precisely so you can.- 24 of 26 cantons are not covered. An uncovered canton returns "not a holiday" for every date — indistinguishable from a real answer unless you check
SwissHolidays.coveredCantonsfirst. See GAPS.md.- "Is a holiday" is not the same as "the Labour Act's Sunday rules apply." Six of Ticino's fifteen official holidays are holidays without being Sunday-equated. Read
legalEffect— see Legal effect below.- The half-day start times are derived, not quoted from the source. See GAPS.md §5.
Cantonal law changes. The
checkedOndate on each citation tells you when it was last verified.
Install #
dependencies:
swiss_holidays: ^0.1.0
The package has no runtime dependencies. The dataset is compiled from YAML into Dart source at build time, so there is no asset loading and no YAML parser at runtime — it works in pure Dart, Flutter, and on the web.
Usage #
Is a given date a holiday? #
isHoliday returns a HolidayLookup whose status is an enum — none, full
or half — never a bool, because "half day" is a real and common case that a
boolean silently destroys. The lookup also carries the half-day start time, which
an enum alone cannot.
final lookup = SwissHolidays.isHoliday(DateTime(2026, 12, 25), 'CH-ZH');
switch (lookup.status) {
case HolidayStatus.none: print('working day');
case HolidayStatus.full: print('full holiday');
case HolidayStatus.half: print('free from ${lookup.startsAt}');
}
All holidays in a year #
for (final o in SwissHolidays.holidaysIn(2026, 'CH-TI')) {
print('${o.date} ${o.holiday.name('it')} ${o.status}');
}
// 2026-01-01 Capodanno full
// 2026-01-06 Epifania full
// 2026-03-19 San Giuseppe full
// … 15 entries
Names are available in de, fr, it and en via holiday.name(langCode),
falling back to English.
Legal effect #
Being called a public holiday and having legal consequences under the federal Labour Act are two different things, decided by two different statutes. Ticino lists fifteen official holidays in one law (RL 843.200) and then splits them in another (Art. 6 LALL, RL 843.100): nine are parificati alle domeniche, six are not. On the six, the Sunday-work provisions of Art. 18–19 LL do not apply and the day counts as a working day for labour-law purposes.
There is a hard ceiling that makes this checkable: Art. 20a Abs. 2 ArG lets a canton equate at most eight further days per year to Sunday beyond 1 August. No canton can have more than nine. Both cantons here sit exactly at that ceiling, and a test asserts it.
final corpusDomini = SwissHolidays.isHoliday(DateTime(2026, 6, 4), 'CH-TI');
corpusDomini.status; // full — it is a holiday
corpusDomini.isSundayEquivalent; // false — but Art. 18-19 LL do not apply
corpusDomini.legalEffects; // {LegalEffect.cantonalRestDay}
Zürich is structured differently and was verified separately: § 1 Abs. 3 RLG
equates all nine of its rest days in a single sentence, so every Zürich entry is
sundayEquivalent. Do not infer one canton's structure from another's — 1 May
and Whit Monday are Sunday-equated in Zürich and not in Ticino.
Filter to the subset you need:
// Payroll: only days governed by the Labour Act's Sunday rules.
SwissHolidays.holidaysIn(2026, 'CH-TI',
effects: {LegalEffect.sundayEquivalent}); // 9 entries
// Booking or opening hours: every official holiday.
SwissHolidays.holidaysIn(2026, 'CH-TI'); // 15 entries
The three values are sundayEquivalent, cantonalRestDay and
personnelLawRestDay (work-free under a public administration's own personnel
law, binding that employer only). Every entry carries one, sourced — often from a
different statute than the one that lists the holiday.
Working-day arithmetic #
// Thu 2 April 2026 + 1 working day.
// Zürich: Good Friday, weekend, Easter Monday → Tue 7 April.
SwissHolidays.addWorkingDays(DateTime(2026, 4, 2), 1, 'CH-ZH'); // 2026-04-07
// Ticino: no Good Friday → Fri 3 April.
SwissHolidays.addWorkingDays(DateTime(2026, 4, 2), 1, 'CH-TI'); // 2026-04-03
SwissHolidays.nextWorkingDay(DateTime(2026, 12, 24), 'CH-ZH'); // 2026-12-28
SwissHolidays.addWorkingDays(DateTime(2026, 6, 17), -5, 'CH-ZH'); // backwards
By default Saturday and Sunday are non-working, full holidays are non-working,
and half days count as working days (work is performed for part of them).
Override with WorkingDayPolicy:
const policy = WorkingDayPolicy(
halfDaysAreWorkingDays: false,
weekendWeekdays: {DateTime.sunday}, // six-day week
include: {AppliesTo.generalPublic}, // ignore staff-only entries
effects: {LegalEffect.sundayEquivalent}, // only Labour-Act Sunday days
);
Communes #
Communes are identified by official FSO/BFS commune number, never by name — communes merge and names change, numbers are stable and official.
// 261 = Zürich. Verified against the BFS Amtliches Gemeindeverzeichnis.
SwissHolidays.holidaysIn(2026, 'CH-ZH', communeId: 261);
Passing a commune with no sourced data falls back to the cantonal set; it never inherits another commune's entries.
Check coverage before you trust a result #
if (!SwissHolidays.coveredCantons.contains(canton)) {
throw StateError('$canton is not covered — see GAPS.md');
}
Reading the citations #
for (final b in holiday.legalBasis) {
print('${b.citation}\n ${b.url}\n checked ${b.checkedOn} (${b.kind})');
if (b.quote != null) print(' "${b.quote}"');
}
Coverage #
Honest status. "Sourced" means every entry has a citation to an official source that was fetched, with the URL recorded.
| Scope | Entries | In force from | Status |
|---|---|---|---|
| CH-ZH (canton Zürich) | 9 full days, all Sunday-equated | 2000-12-01 | Sourced — § 1 Abs. 1 lit. b and Abs. 3 RLG (LS 822.4) |
| CH-TI (canton Ticino) | 15 full days: 9 Sunday-equated, 6 not | 2010-02-09 | Sourced — Art. 1 RL 843.200 and Art. 6 LALL (RL 843.100) |
| Commune 261 (Stadt Zürich) | 1 full + 3 half days, personnel-law only | 2002-07-01 | Sourced, staff-scoped — Art. 169 AB PR (AS 177.101). Half-day times derived, not quoted |
| Other 24 cantons | — | — | Not yet sourced |
| All other communes | — | — | Not yet sourced |
Sechseläuten dates cover 2024–2035 and Knabenschiessen 2026–2035, bounded by the published tables. Outside those ranges the engine returns nothing rather than extrapolating.
Full detail, including every known hole and every deliberate modelling decision, is in GAPS.md. It is worth reading before you depend on this.
Sources actually fetched #
| Source | Used for |
|---|---|
| Kanton Zürich, RLG (LS 822.4), § 1 | The nine Zürich rest days |
| Kanton Zürich, AWA, «Feiertage» | Corroboration; 2025 & 2026 golden dates |
| Cantone Ticino, Legge RL 843.200, Art. 1 | The fifteen Ticino holidays |
| Cantone Ticino, LALL (RL 843.100), Art. 6 | Which nine Ticino days are Sunday-equated |
| Cantone Ticino, Gran Consiglio, IE 547 rapporto di minoranza | Independent confirmation of the nine/six split and the Art. 20a ceiling |
| Cantone Ticino, UIL, «Giorni festivi in Ticino» | Corroboration; 2026 & 2027 golden dates |
| Stadt Zürich, AB PR (AS 177.101), Art. 169 | The four extra city days |
| Stadt Zürich, «Feiertage und Betriebsferientage» | Corroboration of the half days |
| ZZZ, «Sechseläuten-Daten 2024–2035» | Sechseläuten dates |
| Schützengesellschaft der Stadt Zürich, «Zukünftige Festdaten» | Knabenschiessen dates |
| BAK, Lebendige Traditionen, «Knabenschiessen» | Evidence the date rule is not computable |
| BFS, Amtliches Gemeindeverzeichnis (snapshot API) | Commune number 261 |
Wikipedia is a fine place to learn what to look for. It is never a citation here, and a test enforces that.
Architecture #
data/
schema/holiday.schema.json JSON Schema (draft 2020-12) — the contract
cantons/ch-zh.yaml sourced cantonal definitions
cantons/ch-ti.yaml
communes/ch-zh-261.yaml sourced communal definitions
lib/
src/easter.dart Meeus/Jones/Butcher. Pure.
src/rules.dart rule → date. Pure, no I/O.
src/models.dart value types
src/query.dart lookup + working-day arithmetic
src/generated/dataset.g.dart GENERATED — do not edit
swiss_holidays.dart public API
tool/
build_dataset.dart YAML → validated Dart asset
Data is separate from code. The build step validates every YAML file against the schema and refuses to emit anything if validation fails:
dart run tool/build_dataset.dart # regenerate
dart run tool/build_dataset.dart --check # CI: fail if stale
Rule types #
| Type | Example |
|---|---|
fixed |
1 August; 8 December |
easter_relative |
Good Friday −2, Easter Monday +1, Ascension +39, Whit Monday +50, Corpus Christi +60 |
nth_weekday |
third Monday in April (nth: -1 for last) |
explicit_dates |
Sechseläuten, Knabenschiessen — see below |
explicit_dates exists because of a real finding: Sechseläuten is not the third
Monday in April. The Zentralkomitee der Zünfte Zürich's own published table puts
it on the fourth Monday in 2025, 2028, 2031 and 2033, and the second Monday
(8 April) in 2030. Any nth_weekday encoding would produce wrong dates. The
federal BAK dossier likewise describes Knabenschiessen as "am zweiten Wochenende
im September oder eine Woche vor dem Eidgenössischen Bettag" — a disjunction,
not a calendar rule. So both are encoded as the official date tables they are, and
the engine returns nothing for unlisted years instead of extrapolating.
Tests #
dart test # 138 tests
- Easter — verified against an independently-structured second algorithm
(Gauss) over all 717 years from 1583 to 2299, plus the published invariants
(always a Sunday; always 22 March – 25 April, both extremes exercised), plus
the Easter-derived dates published by the Ticino labour inspectorate for 2026
and 2027. (A published multi-decade table was the first choice; the USNO page
turned out to publish the algorithm and the bounds but no table, so it is not
cited as one. The reasoning is recorded at the top of
test/easter_test.dart.) - Golden tests per canton per year — CH-ZH 2025 & 2026 and CH-TI 2026 & 2027, asserted against the official published calendars, with the source URL in the test file header.
- Round-trip — every YAML entry validates against the schema, every entry survives into the generated dataset, and the generated file is checked to be up to date. Plus ~20 malformed fixtures asserted to fail, so the round-trip test cannot pass vacuously.
- Provenance — every entry has a citation with an
httpsURL and acheckedOndate; an allow-list of official hosts; a ban-list including Wikipedia and the commercial calendar sites. - Edge cases — half days, holidays on weekends (and not shifted to Monday,
because Switzerland has no such rule),
valid_from/valid_toboundaries at exactly ±1 day, 29 February, non-existent 5th weekdays, fixed-date overflow, time-zone independence.
Contributing a canton #
Short version: a data contribution is only accepted with a citation and a URL. Fetch the cantonal act, quote the article, record the URL and the date you checked it. If you cannot verify something, add it to GAPS.md instead.
Full instructions in CONTRIBUTING.md.
Prior art #
Nothing on pub.dev covers Switzerland (checked 2026-07-29). The Python
vacanza/holidays package covers all 26
cantons with HALF_DAY/OPTIONAL categories and Stadt Zürich, and is the more
complete dataset today. This package differs in being Dart-native, in carrying a
cited legal source per entry, in modelling commune scope by BFS number, in giving
half days an explicit clock time with recorded provenance, and in shipping
working-day arithmetic. If you need breadth today, use the Python package. If you
need Dart and auditability, use this.
Maintained by #
Built and maintained by The Hexagon Swiss Sagl — https://hexagonswiss.com.
Corrections to the data are especially welcome: if an entry is wrong, unsourced, or its link has rotted, open an issue. Removing bad data counts as a contribution. See CONTRIBUTING.md.
Licence #
MIT, Copyright (c) 2026 The Hexagon Swiss Sagl — see LICENSE.
The holiday data is compiled from official cantonal and communal legal sources, each cited in the dataset. Legal texts published by Swiss public authorities are excluded from copyright protection under Art. 5 of the Swiss Copyright Act (URG). See NOTICE.md.