smwks / laravel-edtf
Fuzzy and partial historical dates (EDTF) for Eloquent and Filament.
Requires
- php: ^8.3
- illuminate/database: ^13.0
- illuminate/support: ^13.0
Requires (Dev)
- filament/forms: ^5.0
- filament/schemas: ^5.0
- filament/support: ^5.0
- livewire/livewire: ^4.4
- orchestra/testbench: ^11.2
- pestphp/pest: ^5.1
Suggests
- filament/forms: Required to use Smwks\LaravelEdtf\Filament\EdtfDatePicker.
This package is auto-updated.
Last update: 2026-08-31 22:10:59 UTC
README
Fuzzy and partial historical dates (EDTF) for Eloquent and Filament.
Real source material is often imprecise — "died sometime in 1937", "born around
1926". A plain date column can't hold that, so this package stores the fuzzy
value alongside a normalized exact date: every query, sort, and age calculation
that already reads the plain column keeps working, while the imprecision is
preserved instead of guessed away.
Installation
composer require smwks/laravel-edtf
The service provider is auto-discovered. filament/forms is only needed if you
use EdtfDatePicker; the rest of the package has no Filament dependency.
Usage
Migration
The edtf() Blueprint macro adds both columns at once — {name} (a nullable
date) and {name}_edtf (a nullable json):
Schema::create('people', function (Blueprint $table) { $table->id(); $table->edtf('born_on'); });
Model
Cast the _edtf column and you get an Edtf value object back:
use Smwks\LaravelEdtf\Casts\AsEdtf; protected function casts(): array { return [ 'born_on' => 'date', 'born_on_edtf' => AsEdtf::class, ]; }
$person->born_on_edtf = '1926~'; $person->born_on_edtf->raw(); // '1926~' $person->born_on_edtf->precision(); // Precision::Year $person->born_on_edtf->isApproximate(); // true $person->born_on_edtf->min(); // 1926-01-01 $person->born_on_edtf->max(); // 1926-12-31 $person->born_on_edtf->bestGuess(); // 1926-01-01
The column stores {"edtf": "1926~", "min": "1926-01-01", "max": "1926-12-31"},
so range queries ("born in the 1920s") filter on real dates rather than parsing
EDTF strings per row.
Displaying a human-readable date
There is no built-in humanizer. Edtf deliberately exposes the parts and lets
you format to taste — the important thing is to format against the precision,
so a year- or month-precision value never prints a filler day or month:
use Smwks\LaravelEdtf\Edtf; use Smwks\LaravelEdtf\Precision; function humanizeEdtf(Edtf $edtf): string { $date = $edtf->bestGuess(); $text = match ($edtf->precision()) { Precision::Day => $date->format('F j, Y'), // March 3, 2021 Precision::Month => $date->format('F Y'), // March 2021 Precision::Year => $date->format('Y'), // 2021 }; if ($edtf->isApproximate()) { $text = "c. {$text}"; } if ($edtf->isUncertain()) { $text .= '?'; } return $text; }
bestGuess() is anchored to the start of the precision (2021-03 →
2021-03-01, 2021 → 2021-01-01), so it is safe to format() once the
match has picked the right mask. For unspecified digits (192X) it collapses
to the low end; show the span with min()/max() instead:
$edtf->min()->format('Y').'–'.$edtf->max()->format('Y'); // 2010–2019
Filament
EdtfDatePicker replaces a plain DatePicker. One control picks the precision
and takes what's known, and on save it writes both columns from that single
input — {name} from bestGuess() and {name}_edtf from the full value:
use Smwks\LaravelEdtf\Filament\EdtfDatePicker; EdtfDatePicker::make('born_on')
The field's sub-inputs are stock Select/TextInput components — it can
hydrate and display any value the Parser accepts, but it can only save
plain, fully-specified values (1926, 1926-02, 1937-11-25). Qualifiers
(1926~) and unspecified digits (192X) are preserved on read but are not
yet re-editable through this field; assign them directly on the model when
you need them. A qualifier-aware UI is a planned follow-up.
EDTF scope
A deliberately bounded subset of the Library of Congress EDTF specification — enough for real historical person records, not the whole spec:
- Level 0 —
YYYY,YYYY-MM,YYYY-MM-DD. - Level 1 — uncertain (
1926?), approximate (1926~), both (1926%), and unspecified digits (192X). - Level 2 — group qualification (
2004?-06-11), individual component qualification (?2004-06-~11), and unspecified digits anywhere in a component (1XXX-12).
Out of scope, and rejected with a clear InvalidEdtfException rather than
silently guessed at: exponential years (Y-17E7), significant digits
(1950S2), sets ([1667,1668] / {1667,1668}), intervals with qualified
components (2004-06-~01/2004-06-~20), and sub-year groupings such as seasons
(2001-34). Calendar-impossible dates (2005-02-29) are rejected too.
Testing
composer install vendor/bin/pest
License
MIT. See LICENSE.