Search by

dipesh / date

dipeshpokhrel

System-level BS (Bikram Sambat) date value object for PHP: parsing, components, language-aware formatting, and cloning. Extracted from dipesh/nepali-date.

v1.0.0 2026-11-08 05:26 UTC

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, and l read getWeekDay(), which the base Date leaves at 0 — 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 $lang override to format() 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 names
  • Dipesh\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.