dipesh / date
System-level BS (Bikram Sambat) date value object for PHP: parsing, components, language-aware formatting, and cloning. Extracted from dipesh/nepali-date.
Requires
- php: >=8.4
Requires (Dev)
- laravel/pint: ^1.17
- pestphp/pest: ^3.8
- phpstan/phpstan: ^2.3
- phpunit/phpunit: ^11.3
- rector/rector: ^2.6
Suggests
None
Provides
None
Conflicts
None
Replaces
None
This package is not auto-updated.
Last update: 2026-11-09 03:48:50 UTC
README
A system-level BS (Bikram Sambat) date value object for PHP ≥ 8.4: parsing, components, and language-aware formatting. No framework lock-in, no calendar arithmetic — just a clean, immutable-by-default date object.
Extracted from dipesh/nepali-date as a standalone package.
Installation
composer require dipesh/date
Quick start
use Dipesh\Date\Date; use Dipesh\Date\lang\English; use Dipesh\Date\lang\Nepali; $date = new Date('2078/01/15', new English); $date->getDate(); // '2078/01/15' (normalized) $date->getYear(); // 2078 (int) $date->getMonth(); // 1 (int, 1–12) $date->getDay(); // 15 (int) $date->getWeekDay(); // 0 (int, set by subclasses)
Parsing
Date accepts flexible separators and normalizes to YYYY/MM/DD:
new Date('2078-01-01', $lang)->getDate(); // '2078/01/01' new Date('2078.1.1', $lang)->getDate(); // '2078/01/01'
Structural validation only (year ≥ 1, month 1–12, day ≥ 1). Invalid shapes throw:
use Dipesh\Date\Date; Date::parseComponents('2078/01/15'); // [2078, 1, 15] Date::parseComponents('abc'); // throws Exception
Formatting
Supported format characters:
| Char | Renders | Example (English) |
|---|---|---|
Y |
Year | 2078 |
m |
Month, zero-padded | 04 |
M |
Month, short name | (empty) |
F |
Month, full name | Shrawan |
d |
Day, zero-padded | 15 |
w |
Weekday number | 3 |
D |
Weekday, short name | Budh |
l |
Weekday, full name | Budhabar |
g |
Gate (half-moon) label | Gate |
Any other character is preserved literally. Unsupported word characters throw.
Weekday note:
w,D, andlreadgetWeekDay(), which the baseDateleaves at0— it must be computed by a subclass or wrapper (e.g.NepaliDate::withDate()sets it from calendar arithmetic). Formatting a weekday before it is set will error.
$date = new Date('2078/04/15', new English); $date->format(); // '2078/04/15' $date->format('Y-m-d'); // '2078-04-15' $date->format('F d, Y'); // 'Shrawan 15, 2078' (literals kept between format chars) $date->format('Y/m/d', 'np'); // '२०७८/०४/१५' (temporary language override)
The
$langoverride toformat()is temporary — the instance's language is restored afterward.
Language packs
Two immutable packs ship with the package, both implementing Dipesh\Date\Contracts\LanguagePack:
Dipesh\Date\lang\English— Romanized digits, weekday and month namesDipesh\Date\lang\Nepali— Devanagari digits, weekday and month names
Packs are data only. All lookup logic lives on the Dipesh\Date\Language bridge,
which wraps a pack and is what the date object uses internally (Bridge pattern):
use Dipesh\Date\Language; $lang = new Language(new Nepali); // bridge over a pack $lang->getDigit(0); // '०' $lang->getMonth(0); // ['F' => 'बैसाख', 'M' => '']
$date = new Date('2078/01/15', new Nepali); $date->format('Y/m/d'); // '२०७८/०१/१५' $date->format('F'); // 'बैसाख'
Resolve from a code string — both return the bridge:
$date->resolveLanguage('np'); // Language bridge over Nepali $date->resolveLanguage('en'); // Language bridge over English
To add a language, implement Dipesh\Date\Contracts\LanguagePack (a single
toArray() returning gate, digits, weeks, and months) and pass an instance to
the constructor — or skip the class entirely and hand languageUsing() a
callback that returns the data array (see below).
Custom formatters & languages
Both formatUsing() and languageUsing() return a new instance; the
original keeps its defaults.
formatUsing()
Plug in your own formatter — a Dipesh\Date\Contracts\Formatter instance, or a
callback invoked as ($format, $date) returning the formatted string:
$date = new Date('2078/01/15', new English); $custom = $date->formatUsing(fn (string $format) => "<{$format}>"); $custom->format('Y/m/d'); // '<Y/m/d>' $date->format('Y/m/d'); // '2078/01/15' (unchanged)
Formatters extending Dipesh\Date\Formatter are rebound automatically when
the date changes (withDate(), cloning), so a stored prototype always formats
the date it is called on.
languageUsing()
Swap the formatting language — a pack, a bridge, or a callback returning a
language data array (the LanguagePack::toArray() shape):
$date = new Date('2078/01/15', new English); $date->languageUsing(new Nepali)->format('Y/m/d'); // '२०७८/०१/१५' $date->languageUsing(fn () => [ 'gate' => 'G', 'digits' => ['₀', '₁', '₂', '₃', '₄', '₅', '₆', '₇', '₈', '₉'], 'weeks' => array_fill(0, 7, ['l' => 'long', 'D' => 'lo']), 'months' => array_fill(0, 12, ['F' => 'full', 'M' => 'fu']), ])->format('Y/m/d'); // '₂₀₇₈/₀₁/₁₅'
Malformed data arrays throw (Exception) with the offending key or count.
Immutability
withDate() returns a new instance, preserving the language. The original is untouched:
$date = new Date('2078/01/01', new Nepali); $next = $date->withDate('2079/02/03'); $date->getDate(); // '2078/01/01' (unchanged) $next->getDate(); // '2079/02/03'
Cloning also rebinds the formatter to the clone, so the two never share state.
Comparison
Date uses the HasComparisonDate trait (Dipesh\Date\Concerns\), which orders dates
component-wise — year first, then month, then day. No calendar arithmetic is involved.
isAnniversary() compares month and day only, ignoring the year.
The reference date is required (a Date object or a date string); there is no
default-to-today behaviour, since this package has no clock.
$date = new Date('2078/01/15', new English); $date->isEqual('2078/01/15'); // true $date->isEqual('2078-01-15'); // true (flexible separators) $date->isGreaterThan('2077/12/31'); // true $date->isLessThan('2079/01/01'); // true $date->isGreaterThan('2078/01/15'); // false (equal is not greater) $date->isAnniversary('2080/01/15'); // true (same month and day, any year) $date->isAnniversary('2080/01/16'); // false
The same methods are available to any class that uses the trait and
implements Dipesh\Date\Contracts\Date.
Architecture
| Piece | Role |
|---|---|
Date |
The date value object — parsing, components, formatting |
Contracts\Date |
Date interface: getters, withDate, format, parseComponents |
Contracts\Formatter |
Formatter interface: format, formatNumber, formatMonth, formatWeekDay |
Contracts\Language |
Language lookup interface: digits, weeks, months, gate |
Contracts\LanguagePack |
Language data contract (Bridge implementor): toArray |
Language |
Language bridge — wraps a pack, owns the localized lookups |
Formatter |
Abstract formatter base — takes a Date on construct, forDate rebinding |
FormatDate |
Default BS formatter (extends Formatter) |
CallbackFormatter |
Adapts a formatUsing() callback to the Formatter contract |
lang\English, lang\Nepali |
Immutable language packs (data only) |
Concerns\HasComparisonDate |
Comparison trait: isEqual, isGreaterThan, isLessThan, isAnniversary |
This package deliberately contains no calendar arithmetic (no AD ↔ BS conversion, no day math, no DataSet). Those concerns live in dipesh/nepali-date, which builds on this value object.
Testing
composer install composer test # pest --parallel composer check # lint (pint + phpstan) + tests
License
MIT — see LICENSE.