intragile / laravel-hungarian-workdays
Hungarian workday calendar with workday arithmetic, Carbon macros and a validation rule: public holidays, bridge days and working Saturdays from the official decrees. Magyar munkanap-naptár: munkaszüneti napok, áthelyezett munkanapok, ledolgozós szombatok, munkanap-számítás.
Package info
github.com/intragile/laravel-hungarian-workdays
pkg:composer/intragile/laravel-hungarian-workdays
Requires
- php: ^8.3
Requires (Dev)
- illuminate/support: ^12.0|^13.0
- larastan/larastan: ^3.0
- laravel/pint: ^1.20
- orchestra/testbench: ^10.0|^11.0
- phpunit/phpunit: ^12.0
Suggests
None
Provides
None
Conflicts
- illuminate/support: <12.0
Replaces
None
README
Developed and maintained by IntrAgile Kft.
The Hungarian workday calendar: statutory public holidays (munkaszüneti napok), the bridge days and working Saturdays decreed each year (áthelyezett munkanapok, ledolgozós szombatok), workday arithmetic, Carbon macros and a validation rule.
Magyar nyelvű leírás → README.hu.md
- Official data, not a scraped calendar. The 2013–2026 schedules are copied from the decrees in the Nemzeti Jogszabálytár. Every year cites its decree, see DATA-SOURCES.md.
- Explicit coverage. A year without a known schedule throws
UncoveredYearExceptioninstead of silently ignoring bridge days. You can add a year yourself or opt in to a documented fallback. - Framework-independent core.
Calendaris plain PHP and needs nothing else. The Laravel layer adds a facade, Carbon macros, a validation rule and an Artisan command. - Date handling you can reason about. A day is judged by its own
Y-m-d; time and timezone are ignored. Results areDateTimeImmutableat 00:00 in the input's timezone, and inputs are never modified.
What counts as a workday
"Workday" means a working day under the general work schedule (általános munkarend): Monday to Friday, except public holidays and the rest days moved by the yearly decree, plus the Saturdays worked in exchange. Workplaces with a different schedule (shift work, retail) are out of scope.
Only the statutory holidays of Mt. 102. § (1) are listed. Easter Sunday and Whit Sunday are not, as they are Sundays anyway.
24 December becomes a public holiday from 2026. Parliament adopted the amendment on
2026-11-03 (bill T/714); its promulgation in the Magyar Közlöny is pending as of
2026-11-05. The bundled 2026 data already reflects it: the decree's 2026-12-24 rest day and
2026-12-12 worked Saturday pair is removed, and 2026-12-24 is a public holiday. If the
promulgation fails or changes, this will be revisited, see DATA-SOURCES.md.
Requirements
- PHP 8.3+
- Laravel 12 or 13 for the Laravel layer (optional)
Installation
composer require intragile/laravel-hungarian-workdays
The service provider and the Workdays facade are discovered automatically. To publish the
configuration or the validation messages:
php artisan vendor:publish --tag=workdays-config php artisan vendor:publish --tag=workdays-lang
Core usage (any PHP project)
The core has no dependencies:
use IntrAgile\HungarianWorkdays\Calendar; $calendar = new Calendar; $calendar->isWorkday(new DateTimeImmutable('2026-03-16')); // true $calendar->isWorkday(new DateTimeImmutable('2026-01-02')); // false, moved rest day $calendar->isWorkday(new DateTimeImmutable('2026-01-10')); // true, worked Saturday $calendar->dayType(new DateTimeImmutable('2026-08-20')); // DayType::PublicHoliday $calendar->holiday(new DateTimeImmutable('2026-08-20')); // Holiday::StStephensDay $calendar->nextWorkday(new DateTimeImmutable('2026-03-13')); // 2026-03-16 00:00 $calendar->previousWorkday(new DateTimeImmutable('2026-03-16')); // 2026-03-13 00:00 $calendar->addWorkdays(new DateTimeImmutable('2026-03-13'), 5); // 2026-03-20 00:00 $calendar->workdaysBetween($from, $to); // both ends included $calendar->workdaysInMonth(2026, 3); // 22 $calendar->workingHoursInMonth(2026, 3, dailyHours: 8); // 176
| Method | Returns |
|---|---|
dayType($date) |
a DayType: Workday, Weekend, PublicHoliday, RestDay (moved rest day), TransferredWorkday (worked Saturday) |
isWorkday($date) |
true for Workday and TransferredWorkday |
isPublicHoliday($date), holiday($date) |
the statutory holiday check and the Holiday case (or null) |
holidays($year) |
the year's holidays, keyed by Y-m-d |
nextWorkday($date, inclusive: false), previousWorkday(...) |
the first workday after (before) the day; inclusive: true accepts the day itself |
addWorkdays($date, $n), subWorkdays($date, $n) |
moves by $n workdays; 0 returns the day itself at 00:00 |
workdaysBetween($from, $to) |
workdays from $from to $to, both included |
workdaysInMonth($year, $month), workingHoursInMonth($year, $month, $dailyHours = 8) |
the month's workdays and hours |
isCovered($year), coveredYears() |
schedule coverage, see below |
Holiday also offers hungarianName() and englishName().
Holiday::dateIn($year) returns midnight UTC, whereas Calendar results keep the input's
timezone. Compare dates by format('Y-m-d'), not with == on the objects. Holiday::forYear()
and dateIn() are range-lenient helpers: for a year before 2013 they return [] or null,
which means "outside the supported range", not "not a holiday". Use the Calendar methods for
answers that are range-checked.
Coverage and UncoveredYearException
The package bundles the schedules of 2013–2026. 2017 and 2023 had no transfer decree; both are bundled as covered, with no moved days.
Asking about a year without a schedule throws UncoveredYearException (its year property
carries the year), because a bridge day could otherwise be missed without notice. Years before
2013 are never supported: every Calendar query about them throws, holiday(),
isPublicHoliday() and holidays() included, and the exception message says the year is
outside the supported range. Calendar arithmetic that walks into an uncovered year throws as well.
The holiday rules need no schedule, so those three methods answer for any year from 2013 on, covered or not.
$calendar->isCovered(2027); // false $calendar->coveredYears(); // [2013, 2014, ..., 2026]
2027 is not covered yet. The 2027 decree was due by 2026-10-31 but had not been published when this was written (2026-11-05). Until it is, any 2027 date throws. The Artisan command and the CI job described below warn you while it is missing.
Adding a year yourself
In Laravel, add the year to the configuration (config/workdays.php). Take the dates from
the decree published in the Magyar Közlöny; the lists below are empty placeholders, not
real data:
'schedules' => [ 2027 => [ 'source' => 'Title of the 2027 work schedule decree', // fill in 'url' => 'https://njt.jog.gov.hu/...', // fill in 'rest_days' => [], // weekdays that became rest days, e.g. '2027-05-14' 'worked_saturdays' => [], // Saturdays worked in exchange, e.g. '2027-05-22' ], ],
In plain PHP:
use IntrAgile\HungarianWorkdays\Calendar; use IntrAgile\HungarianWorkdays\Schedule; use IntrAgile\HungarianWorkdays\ScheduleRepository; $calendar = new Calendar( ScheduleRepository::bundled()->with(Schedule::fromArray(2027, [ 'source' => 'Title of the 2027 work schedule decree', 'url' => 'https://njt.jog.gov.hu/...', 'rest_days' => [], 'worked_saturdays' => [], ])), );
A configured (or with()) year replaces a bundled one of the same year. Schedule::fromArray()
validates the data and throws InvalidScheduleException for a year before 2013, a malformed
date, a date outside the decree year and the year before it, a worked day that is not a
Saturday, a rest day on a weekend, or a date that is already a public holiday. The Laravel
configuration is checked the same way, and its keys must be years.
assumingNoTransfers()
As a stopgap you can tell the calendar to treat an uncovered year as having no moved rest days and no worked Saturdays:
$calendar = (new Calendar)->assumingNoTransfers();
In Laravel, set 'assume_no_transfers' => true in config/workdays.php. Dates that loaded
schedules list still apply, and the statutory holidays always do.
The risk: Hungarian decrees regularly move a day for several bridge days a year. Under this assumption such a rest day is reported as a workday, and the Saturday worked in exchange as a weekend. Use it only until the decree is published, and not for payroll, deadlines or anything legally binding. Years before 2013 still throw.
Laravel
Facade
use IntrAgile\HungarianWorkdays\Laravel\Facades\Workdays; Workdays::isWorkday(now()); Workdays::addWorkdays(now(), 8);
The facade proxies the Calendar, which is also resolvable from the container.
Carbon macros
Registered on Carbon and CarbonImmutable:
now()->isHungarianWorkday(); now()->isHungarianPublicHoliday(); now()->addHungarianWorkdays(5); now()->subHungarianWorkdays(5); now()->nextHungarianWorkday(); // nextHungarianWorkday(inclusive: true) now()->previousHungarianWorkday();
Date-returning macros give an instance of the receiver's own class at 00:00 in its timezone and never modify the receiver.
The Calendar is a container singleton, built on first use from the configuration at that
moment. The macros look it up in the container captured at boot on every call, so they share
that instance. In long-lived workers such as Octane or queue workers, a change to
workdays.schedules or workdays.assume_no_transfers made after the first use therefore
needs a worker restart.
Validation
use IntrAgile\HungarianWorkdays\Laravel\Rules\HungarianWorkday; $request->validate([ 'delivery_date' => ['required', 'hungarian_workday'], 'due_date' => ['required', new HungarianWorkday], ]);
The value must be a DateTimeInterface or a string PHP can parse as a date. Three failure
messages exist, in English and Hungarian: not a date, not a working day, and no schedule
available for the year (the rule fails instead of throwing).
Customising the message depends on how you use the rule:
- String rule (
'hungarian_workday'): a message you define yourself, inline or asvalidation.hungarian_workdayin your own language files, replaces all three failure messages. - Rule object (
new HungarianWorkday): Laravel looks up custom messages by attribute and rule class, sovalidation.hungarian_workdayin your language files does not apply. Pass the message to the validator as'due_date.hungarian_workday'(or'due_date.'.HungarianWorkday::class, or the bare class name), for example fromFormRequest::messages(). It likewise replaces all three messages for that attribute.
To customise the three messages separately for both forms, publish the workdays-lang files and
edit the hungarian-workdays::validation entries.
Artisan command
php artisan workdays:coverage
Prints the covered years (for example Covered years: 2013–2026). It exits with a failure and a
warning whenever the current year is not covered, and from 31 October on also when the schedule
for the next year is missing, so a scheduler or a deployment check can raise the alarm.
Data sources
All data comes from primary legal sources (the Nemzeti Jogszabálytár and the Magyar Közlöny); third-party calendar sites are never used. Decree titles, URLs and the open points (the pending 24 December promulgation, the 2027 decree) are in DATA-SOURCES.md. Legislation is not subject to copyright in Hungary.
bin/check-coverage.php fails when the current year is not bundled, and from 31 October when
next year is not. The repository's separate schedule-coverage workflow runs it weekly, so a
missing decree surfaces without anyone having to remember, and without turning the tests badge red.
Related packages
- intragile/laravel-hungarian-validation: tax number, IBAN, TAJ, company registration number and postal code validation.
- intragile/laravel-nav-online-szamla: NAV Online Számla client.
Testing
composer test # PHPUnit composer analyse # PHPStan, level max composer format # Pint
Security
See SECURITY.md.
License
MIT, see LICENSE.
Developed and maintained by IntrAgile Kft..