Sooner or later every business system needs to know whether a date is a working day. Payroll needs the number of working days and hours in a month. A courier promise says "delivered in 2 working days". An office auto-reply, a reminder, a deadline "within 14 working days" and even a smart-home alarm all depend on the same question. In Bulgaria the answer is not "Monday to Friday minus a fixed list": Easter moves with the Orthodox calendar, holidays that fall on a weekend move to the next working day, and the government sometimes declares extra days off. This guide collects the rules, gives tested PHP code and shows where hand-written code usually breaks.
#The official holidays
Article 154(1) of the Labour Code lists the official holidays:
- 1 January, New Year;
- 3 March, the Day of the Liberation of Bulgaria, the national holiday;
- 1 May, Labour Day and the Day of International Workers' Solidarity;
- 6 May, St George's Day, the Day of Valour and the Bulgarian Army;
- 24 May, the Day of Bulgarian Education and Culture and of the Slavic Script;
- 6 September, Unification Day;
- 22 September, the Independence Day of Bulgaria;
- 24, 25 and 26 December, Christmas Eve and Christmas;
- Good Friday, Holy Saturday, Easter Sunday and Easter Monday, by the Orthodox calendar.
The same article also names 1 November, the Day of the Bulgarian Enlighteners, but only as a non-attendance day for all educational institutions. For everyone else it is a working day. That one exception already breaks a single "days off" list shared by an office and a family calendar.
#Orthodox Easter moves every year
Four of the holidays depend on Orthodox Easter, which is calculated on the Julian calendar and usually falls later than Western Easter. In 2026 it is on 12 April, in 2027 on 2 May. PHP's built-in easter_date() gives the Western date, so it cannot be used as it is. The Meeus algorithm for the Julian calendar, shifted by 13 days to the Gregorian calendar, is short and exact for 1900 to 2099:
function orthodox_easter(int $year): DateTimeImmutable
{
$a = $year % 4;
$b = $year % 7;
$c = $year % 19;
$d = (19 * $c + 15) % 30;
$e = (2 * $a + 4 * $b - $d + 34) % 7;
$month = intdiv($d + $e + 114, 31);
$day = ($d + $e + 114) % 31 + 1;
// Julian calendar date + 13 days = Gregorian date (valid 1900 to 2099).
return (new DateTimeImmutable(sprintf('%d-%02d-%02d', $year, $month, $day)))->modify('+13 days');
}
#Holidays on a weekend move to the next working day
Article 154(2) adds the rule most code forgets: when a holiday other than the Easter holidays falls on a Saturday or a Sunday, the first working day after it is a day off. Two weekend holidays give two days off. Some examples:
- 24 May 2026 is a Sunday, so Monday 25 May 2026 is a day off.
- 6 September 2026 is a Sunday, so Monday 7 September is a day off.
- 26 December 2026 is a Saturday, so Monday 28 December is a day off.
- 25 and 26 December 2027 are a Saturday and a Sunday, so Monday 27 and Tuesday 28 December 2027 are both days off.
- 1 May 2027 falls on Holy Saturday, and the next Monday is already Easter Monday, so the extra day off is Tuesday 4 May 2027. The same happened in 2021.
The code below adds the fixed holidays and the Easter days first, then moves each weekend holiday to the next weekday that is not already off. Processing them in date order handles the double Christmas and the Easter collision:
function bulgarian_days_off(int $year): array
{
$fixed = ['01-01', '03-03', '05-01', '05-06', '05-24', '09-06', '09-22', '12-24', '12-25', '12-26'];
$off = [];
foreach ($fixed as $md) {
$off["$year-$md"] = true;
}
$easter = orthodox_easter($year);
foreach (['-2 days', '-1 day', '+0 days', '+1 day'] as $shift) {
$off[$easter->modify($shift)->format('Y-m-d')] = true; // Good Friday to Easter Monday
}
// Art. 154(2): a holiday other than Easter on a Saturday or Sunday gives the next free weekday off.
foreach ($fixed as $md) {
$date = new DateTimeImmutable("$year-$md");
if ((int) $date->format('N') < 6) {
continue;
}
do {
$date = $date->modify('+1 day');
} while ((int) $date->format('N') >= 6 || isset($off[$date->format('Y-m-d')]));
$off[$date->format('Y-m-d')] = true;
}
ksort($off);
return array_keys($off);
}
function is_working_day(DateTimeImmutable $date, array $extraDaysOff = []): bool
{
$day = $date->format('Y-m-d');
return (int) $date->format('N') < 6
&& ! in_array($day, bulgarian_days_off((int) $date->format('Y')), true)
&& ! in_array($day, $extraDaysOff, true);
}
We checked it against the published calendars: it gives 26 May and 8 September 2025, 25 May, 7 September and 28 December 2026, and 4 May, 27 and 28 December 2027 as moved days off. With it, December 2026 has 20 working days, and 14 working days after Friday 18 December 2026 is Wednesday 13 January 2027.
#Days the government declares
The Council of Ministers can declare additional non-working days. For the changeover to the euro it declared 31 December 2025 and 2 January 2026. No formula predicts these, which is why the function above takes a list of extra days off. Keep that list in configuration, not in code, so whoever runs the system can add a day the week it is announced. The same list is the place for your own company holidays.
#Month hours and deadlines
Two calculations come up in almost every Bulgarian business system:
- The month's working hours: working days times the length of the working day, usually 8 hours. December 2026 gives 20 × 8 = 160 hours. Payroll uses it for hourly rates and overtime.
- A deadline in working days: step forward one day at a time and count only working days, as in the example above. Never add calendar days and "adjust for weekends" afterwards; that misses holidays in the middle of the period.
Store the inputs you used, such as the list of extra days off, next to the result. When someone asks in March why a deadline was set in January, you can answer.
#Where hand-written code usually breaks
- Western Easter from
easter_date()instead of Orthodox Easter. - No moved days off, or only one when two weekend holidays meet.
- Moving the Easter holidays, which the Labour Code excludes.
- 1 November treated as a day off for everyone, or ignored for schools.
- A hard-coded list for one year that nobody updates in January.
- Time zones: a server in UTC decides "today" two or three hours before Sofia does.
#The same rules in Home Assistant
We wrote these rules once more for smart homes and small offices that run Home Assistant: Sedmitsa is an integration that knows the Labour Code holidays with Orthodox Easter, the moved days off and the declared days for the euro changeover. Each calendar you add gives sensors for a working day today and tomorrow, the kind of day, the next day off with the length of the whole break, the next working day, and the working days and hours of the month. A calendar entity shows every holiday. Choose a grade group and you also get school-day sensors and the 2026/2027 school calendar from the Ministry of Education's order. Three actions answer scripts: check a date, count working days, and add working days to a date. Your own days off and a Saturday you work are settings, not code.
It runs locally, without a cloud service or an account. Its 132 automated tests run inside Home Assistant Core 2026.10.0, and the holiday rules are checked against the published Bulgarian calendars for 2021 to 2027. Sedmitsa is listed as Coming soon.
#Frequently asked questions
If a holiday falls on a Saturday, is the Friday before it a day off?
No. Under Article 154(2) of the Labour Code the day off moves forward, to the first working day after the holiday, usually the Monday.
Do the Easter holidays also move?
No. Good Friday, Holy Saturday, Easter Sunday and Easter Monday are excluded from the moving rule. Holy Saturday and Easter Sunday never give an extra day off.
Is 1 November a day off?
Not for workers in general. The Labour Code makes it a non-attendance day for educational institutions only, so a school calendar treats it differently from an office calendar.
How do I handle a day the government declares later?
Keep a list of extra days off in configuration and add the day as soon as the decision is published. No algorithm can predict these.
Comments
Comments
Be the first to leave a comment.
Leave a comment