PHPackages                             vesselind/working-days-calculator - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [Utility &amp; Helpers](/categories/utility)
4. /
5. vesselind/working-days-calculator

ActiveLibrary[Utility &amp; Helpers](/categories/utility)

vesselind/working-days-calculator
=================================

PHP library for calculating Bulgarian working days and legal deadlines

1.0.1(2w ago)00MITPHPPHP ^8.2

Since May 21Pushed 2w agoCompare

[ Source](https://github.com/vesselind/working-days-calculator)[ Packagist](https://packagist.org/packages/vesselind/working-days-calculator)[ RSS](/packages/vesselind-working-days-calculator/feed)WikiDiscussions main Synced 1w ago

READMEChangelog (1)Dependencies (2)Versions (2)Used By (0)

Bulgarian Working Days Calculator
=================================

[](#bulgarian-working-days-calculator)

[![PHP ^8.2](https://camo.githubusercontent.com/b5d4f7901c58ad1ddfff679966f426cc25a9354bab763846b9a7276c2feab4e0/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f5048502d253545382e322d626c7565)](https://www.php.net/)[![License: MIT](https://camo.githubusercontent.com/5caa455d8debc46fb23abbadb45a733a937f3910a73fc875c2f7820468e1bb54/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f4c6963656e73652d4d49542d677265656e)](LICENSE)

A standalone, framework-agnostic PHP library for calculating Bulgarian working days, working hours, public holidays, and legal deadlines per the Bulgarian Labor Code (art. 154) and Civil Procedural Code (art. 60).

Installation
------------

[](#installation)

### Via Packagist

[](#via-packagist)

```
composer require vesselind/working-days-calculator
```

### Via local path repository

[](#via-local-path-repository)

```
# In your project's composer.json, add the path repository:
composer config repositories.working-days-calculator path /path/to/WorkingDaysCalculator

# Then require the package:
composer require vesselind/working-days-calculator
```

Quick Start (≤ 5 lines — SC-004)
--------------------------------

[](#quick-start--5-lines--sc-004)

```
$result = (new \Vesselind\WorkingDaysCalculator\Calculator\WorkingDaysCalculator())->workingDaysInMonth(2026, 5);
echo $result->workingDays; // 18
```

Usage Examples
--------------

[](#usage-examples)

### Working Days in a Month

[](#working-days-in-a-month)

```
use Vesselind\WorkingDaysCalculator\Calculator\WorkingDaysCalculator;

$calculator = new WorkingDaysCalculator();
$result = $calculator->workingDaysInMonth(2026, 5); // May 2026
echo $result->workingDays; // 18

$result->totalCalendarDays;  // 31 (May has 31 days)
$result->weekendDays;        // int — Saturdays + Sundays
$result->publicHolidayDays;  // int — all non-weekend holidays in May
$result->holidays;           // Holiday[] — each holiday with date, name, type
```

### Working Days in a Full Year

[](#working-days-in-a-full-year)

```
$result = $calculator->workingDaysInYear(2025);
echo $result->workingDays; // e.g., 248
```

### Working Days for a Custom Date Range

[](#working-days-for-a-custom-date-range)

```
use Vesselind\WorkingDaysCalculator\Model\WorkPeriod;
use Carbon\CarbonImmutable;

$period = new WorkPeriod(
    startDate: new CarbonImmutable('2025-12-15'),
    endDate:   new CarbonImmutable('2026-01-15'),
);

$result = $calculator->workingDaysInPeriod($period);
echo $result->workingDays;
```

### Working Hours

[](#working-hours)

```
// Hours in a month
$hours = $calculator->workingHoursInMonth(2025, 1);   // January 2025
echo $hours->totalWorkingHours;  // workingDays × 8

// Hours in a year
$hours = $calculator->workingHoursInYear(2025);
echo $hours->totalWorkingHours;

// Hours in a custom period
$hours = $calculator->workingHoursInPeriod($period);
echo $hours->workingDays;
echo $hours->hoursPerDay;   // 8 (default)
```

### Listing Holidays for a Period

[](#listing-holidays-for-a-period)

```
use Vesselind\WorkingDaysCalculator\Model\HolidayType;

$holidays = $calculator->holidaysInPeriod($period); // Holiday[]

foreach ($holidays as $holiday) {
    echo $holiday->date->toDateString(); // e.g., 2025-01-01
    echo $holiday->name;                 // Нова година
    echo $holiday->type->value;          // 'fixed' | 'compensation' | 'orthodox_easter' | 'government_announced'
}

// Filter: only Easter holidays
$easterHolidays = array_filter(
    $holidays,
    fn($h) => $h->type === HolidayType::OrthodoxEaster
);
```

### Injecting Government-Announced Days Off

[](#injecting-government-announced-days-off)

```
use Carbon\CarbonImmutable;

$calculator = new WorkingDaysCalculator(
    governmentDaysOff: [
        new CarbonImmutable('2026-01-02'), // Bridge day announced by decree
    ]
);

$result = $calculator->workingDaysInMonth(2026, 1);
// January 2026 working days decreased by 1 for the injected day

// Safe to inject a date that is already a public holiday:
// it is deduplicated automatically — no double-counting.
```

### Orthodox Easter Date

[](#orthodox-easter-date)

```
use Vesselind\WorkingDaysCalculator\Calculator\EasterCalculator;
use Carbon\CarbonImmutable;

$easter = new EasterCalculator();
$easterSunday = $easter->forYear(2025);   // CarbonImmutable: 2025-04-20

$goodFriday   = $easterSunday->subDays(2);  // 2025-04-18
$holySaturday = $easterSunday->subDays(1);  // 2025-04-19
$easterMonday = $easterSunday->addDays(1);  // 2025-04-21
```

### Legal Deadline Calculation (CPC Art. 60)

[](#legal-deadline-calculation-cpc-art-60)

```
use Vesselind\WorkingDaysCalculator\Calculator\DeadlineCalculator;
use Vesselind\WorkingDaysCalculator\Model\TermUnit;
use Carbon\CarbonImmutable;

$deadlineCalc = new DeadlineCalculator(new WorkingDaysCalculator());
$startDate    = new CarbonImmutable('2025-01-01');

// 1-month term
$deadline = $deadlineCalc->calculate($startDate, TermUnit::Month, 1);
echo $deadline->rawExpiry->toDateString();       // 2025-02-01
echo $deadline->resolvedExpiry->toDateString();  // next working day if 2025-02-01 is non-working

// 30-day term (counts from day after start date)
$deadline = $deadlineCalc->calculate($startDate, TermUnit::Day, 30);
echo $deadline->resolvedExpiry->toDateString();

// 1-year term from leap day
$deadline = $deadlineCalc->calculate(
    new CarbonImmutable('2024-02-29'), // leap year
    TermUnit::Year,
    1
);
echo $deadline->rawExpiry->toDateString();  // 2025-02-28 (Feb has no 29th in 2025)
```

### Handling Invalid Input

[](#handling-invalid-input)

```
use Vesselind\WorkingDaysCalculator\Exception\InvalidDateRangeException;
use Vesselind\WorkingDaysCalculator\Model\WorkPeriod;
use Carbon\CarbonImmutable;

try {
    $period = new WorkPeriod(
        startDate: new CarbonImmutable('2025-05-31'),
        endDate:   new CarbonImmutable('2025-05-01'), // end < start
    );
} catch (InvalidDateRangeException $e) {
    echo $e->getMessage();
    // "End date 2025-05-01 must be on or after start date 2025-05-31"
}
```

### Legal Deadline Calculation (ЗЗД Art. 72 — Law of Obligations)

[](#legal-deadline-calculation-ззд-art-72--law-of-obligations)

```
use Vesselind\WorkingDaysCalculator\Calculator\ZzdDeadlineCalculator;
use Vesselind\WorkingDaysCalculator\Calculator\WorkingDaysCalculator;
use Vesselind\WorkingDaysCalculator\Model\MonthAnchor;
use Vesselind\WorkingDaysCalculator\Model\TermUnit;
use Carbon\CarbonImmutable;

$calc = new ZzdDeadlineCalculator(new WorkingDaysCalculator());

// Forward term — 1 month (start day is NOT counted per art. 72 §2)
$deadline = $calc->calculate(new CarbonImmutable('2025-01-31'), TermUnit::Month, 1);
echo $deadline->rawExpiry->toDateString();       // 2025-02-28 (Feb has no 31st)
echo $deadline->resolvedExpiry->toDateString();  // first following working day if non-working

// Forward term — 30 days
$deadline = $calc->calculate(new CarbonImmutable('2025-01-01'), TermUnit::Day, 30);
echo $deadline->rawExpiry->toDateString();       // 2025-01-31

// Forward term — 2 weeks (expires on same weekday)
$deadline = $calc->calculate(new CarbonImmutable('2025-01-01'), TermUnit::Week, 2);
echo $deadline->rawExpiry->toDateString();       // 2025-01-15 (same weekday, 2 weeks later)

// Backward term — art. 72 §3: 3 days BEFORE a known date
// Neither the target day nor the expiry day is counted
$deadline = $calc->calculateBackward(new CarbonImmutable('2026-01-10'), 3);
echo $deadline->rawExpiry->toDateString();       // 2026-01-06 (Jan 7, 8, 9 = 3 days strictly between)
echo $deadline->resolvedExpiry->toDateString();  // advanced if non-working

// Month anchors — art. 72 §5
$date = new CarbonImmutable('2025-02-10');
echo $calc->resolveMonthAnchor($date, MonthAnchor::Beginning)->toDateString(); // 2025-02-01
echo $calc->resolveMonthAnchor($date, MonthAnchor::Middle)->toDateString();    // 2025-02-15
echo $calc->resolveMonthAnchor($date, MonthAnchor::End)->toDateString();       // 2025-02-28
```

> **ЗЗД art. 72 rules applied:**
>
> - §1 Month terms clamp to last day of month when the target month is shorter.
> - §1 Week terms expire on the same weekday N weeks later.
> - §2 Day terms: the triggering event day is **not** counted.
> - §2 Non-working last day → first following working day (*присъствен ден*).
> - §3 Backward day terms: **neither** the target day **nor** the expiry day is counted.
> - §5 Named anchors: *начало* = 1st, *среда* = 15th, *край* = last day of month.

---

Requirements
------------

[](#requirements)

RequirementValuePHP version^8.2Carbon^3.0 (`nesbot/carbon`)FrameworkNone (zero runtime framework dependencies)InstallationComposerNamespace`Vesselind\WorkingDaysCalculator\`Features
--------

[](#features)

- **Bulgarian public holidays** — Labor Code art. 154 (11 fixed holidays)
- **Compensation days** — art. 154 §2 weekend-shift rule
- **Orthodox Easter** — Meeus/Julian algorithm (2000–2100)
- **Government-announced days** — art. 154 §3 (constructor injection, no global state)
- **Working hours** — configurable hours-per-day multiplier
- **Legal deadlines (CPC)** — CPC art. 60 (Day/Week/Month/Year terms with automatic advancement)
- **Legal deadlines (ЗЗД)** — Law of Obligations art. 72: forward terms, backward day terms (§3, both endpoints excluded), named month anchors (§5 — beginning/middle/end of month)
- **Immutable value objects** — all results are `readonly` DTOs
- **Strict types** — `declare(strict_types=1)` in every file

License
-------

[](#license)

MIT

###  Health Score

39

—

LowBetter than 84% of packages

Maintenance96

Actively maintained with recent releases

Popularity0

Limited adoption so far

Community6

Small or concentrated contributor base

Maturity46

Maturing project, gaining track record

 Bus Factor1

Top contributor holds 100% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Every ~0 days

Total

2

Last Release

19d ago

### Community

Maintainers

![](https://www.gravatar.com/avatar/b9795dd6ac4f24e5f5d80fbc7ced06995bb6394172cdccc97284ac5776d64ef9?d=identicon)[vesselind](/maintainers/vesselind)

---

Top Contributors

[![vesselind](https://avatars.githubusercontent.com/u/5898398?v=4)](https://github.com/vesselind "vesselind (5 commits)")

###  Code Quality

TestsPHPUnit

### Embed Badge

![Health badge](/badges/vesselind-working-days-calculator/health.svg)

```
[![Health](https://phpackages.com/badges/vesselind-working-days-calculator/health.svg)](https://phpackages.com/packages/vesselind-working-days-calculator)
```

###  Alternatives

[illuminate/support

The Illuminate Support package.

582110.9M39.4k](/packages/illuminate-support)[craftcms/feed-me

Import content from XML, RSS, CSV or JSON feeds into entries, categories, Craft Commerce products, and more.

294943.4k27](/packages/craftcms-feed-me)[solspace/craft-freeform

The most flexible and user-friendly form building plugin!

53675.5k15](/packages/solspace-craft-freeform)[erlandmuchasaj/laravel-gzip

Gzip your responses.

40140.4k2](/packages/erlandmuchasaj-laravel-gzip)[japanese-date/japanese-date

日本の暦、祝日を取り扱うライブラリ

169.9k](/packages/japanese-date-japanese-date)[solspace/craft-calendar

The most powerful event management and calendaring plugin!

1831.6k1](/packages/solspace-craft-calendar)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
