Chronolizer is an Effect 4 library that converts natural-language date ranges to compact date filters and back. It supports ranges without a start or end, autocomplete, spelling correction, and eight languages.
year to date → { gte: "now/y", lte: "now" }
January of last year → { gte: "now-1y/y", lt: "now-1y/y+1M" }
since January 2025 → { gte: "2025-01-01" }
Note
Chronolizer currently uses effect@4.0.0-rc.112. Effect 4 is not stable yet, so its API can change.
Chronolizer requires Effect 4 and uses ES modules. Install both packages:
pnpm add chronolizer effect@4.0.0-rc.112English is available from the main package.
import { EnglishLanguageLayer, formatFilter, parseNatural } from "chronolizer";
import { Effect } from "effect";
const program = parseNatural("January of last year", { locale: "en" }).pipe(
Effect.map(({ range }) => formatFilter(range)),
Effect.tap((filter) => Effect.log("Date filter", filter)),
Effect.provide(EnglishLanguageLayer),
);
Effect.runPromise(program);
// { gte: "now-1y/y", lt: "now-1y/y+1M" }parseNatural parses the complete input. It does not extract a date range from a longer sentence.
Import each non-English language separately. This keeps unused language code out of your build.
import { formatFilter, parseNatural } from "chronolizer";
import { GermanLanguageLayer } from "chronolizer/locales/de";
import { Effect } from "effect";
const program = parseNatural("seit Jahresbeginn", { locale: "de" }).pipe(
Effect.map(({ range }) => formatFilter(range)),
Effect.tap((filter) => Effect.log("Datumsfilter", filter)),
Effect.provide(GermanLanguageLayer),
);
Effect.runPromise(program);
// { gte: "now/y", lte: "now" }Use languagePluginsLayer to combine languages:
import { EnglishLanguage, languagePluginsLayer, parseNatural } from "chronolizer";
import { GermanLanguage } from "chronolizer/locales/de";
import { PolishLanguage } from "chronolizer/locales/pl";
import { Effect } from "effect";
const Languages = languagePluginsLayer([EnglishLanguage, GermanLanguage, PolishLanguage]);
const program = parseNatural("letzten Monat", { locale: "de" }).pipe(Effect.provide(Languages));
Effect.runPromise(program);formatNatural returns one standard phrase for a range. It preserves the meaning, but it does not reproduce the original wording. Absolute days use the language's numeric Intl.DateTimeFormat form.
import { formatNatural, parseFilter } from "chronolizer";
import { GermanLanguageLayer } from "chronolizer/locales/de";
import { Effect } from "effect";
const program = parseFilter({ gte: "2025-01-01", lt: "2025-02-01" }).pipe(
Effect.flatMap((range) => formatNatural(range, { locale: "de" })),
Effect.tap((text) => Effect.log("Date range", text)),
Effect.provide(GermanLanguageLayer),
);
Effect.runPromise(program);
// "Januar 2025"suggestNatural returns valid standard phrases and their date ranges.
import { EnglishLanguageLayer, suggestNatural } from "chronolizer";
import { Effect } from "effect";
const program = suggestNatural("last m", { locale: "en", limit: 5 }).pipe(
Effect.tap((suggestions) => Effect.log("Suggestions", suggestions)),
Effect.provide(EnglishLanguageLayer),
);
Effect.runPromise(program);
// [{ text: "last month", range: ... }, ...]The default limit is 10. The maximum limit is 100. A nonpositive or invalid limit returns no suggestions.
Set typoMode to "tolerant" to correct close spelling errors:
const program = parseNatural("januray of last yaer", {
locale: "en",
typoMode: "tolerant",
});The result contains its quality, applied corrections, and any alternatives with a different meaning. Strict mode is the default and does not correct the input.
Set allowFuture to false to reject periods after the current period:
const program = parseNatural("next 3 months", {
locale: "en",
allowFuture: false,
});This option rejects forms such as next month, next 3 weeks, and in 3 years. It keeps complete current calendar periods such as today, this week, and this month. It does not change them to ranges that end at the current time.
The option does not compare fixed dates, such as January 2099, with the current date. Parsing does not read the clock.
The same option is available in suggestNatural.
Provide an Effect time zone when you calculate ranges based on the current date:
import { parseFilter, resolve } from "chronolizer";
import { DateTime, Effect } from "effect";
const program = parseFilter({ gte: "now/y", lte: "now" }).pipe(
Effect.flatMap(resolve),
DateTime.withCurrentZoneNamed("Europe/Berlin"),
);
Effect.runPromise(program);resolve uses the Effect clock and DateTime.CurrentTimeZone. It uses only the time zone that you provide.
Use the exported Effect Schema before you parse unknown data:
import { DateFilter, parseFilter } from "chronolizer";
import { Effect, Schema } from "effect";
const program = Schema.decodeUnknownEffect(DateFilter)(externalInput).pipe(
Effect.flatMap(parseFilter),
);
Effect.runPromise(program);You can parse:
- days, weeks, months, quarters, and years;
- ranges with a length, such as
last 3 months; - single periods in the past or future, such as
30 months ago; - ranges from a period start to now, such as
year to date; - fixed months, quarters, years, and date ranges;
- named days with or without a year, such as
January 12and12th of January; - ranges without one end, such as
since January 2025andfrom January 12; - written counts from one through ninety-nine, such as
two weeks agoandtwenty-one weeks ago; - combined phrases, such as
the day before January 12andyesterday two weeks ago; - period starts, period ends, and weekends;
- abbreviated month names and common equivalent phrases.
Examples:
| Input | Filter |
|---|---|
today |
{ gte: "now/d", lt: "now/d+1d" } |
last 3 months |
{ gte: "now-3M", lte: "now" } |
30 months ago |
{ gte: "now-30M/M", lt: "now-30M/M+1M" } |
January 2025 |
{ gte: "2025-01-01", lt: "2025-02-01" } |
January 12 |
{ gte: "now/y+11d", lt: "now/y+12d" } |
Q1 2025 |
{ gte: "2025-01-01", lt: "2025-04-01" } |
01 January 2025 - 31 January 2025 |
{ gte: "2025-01-01", lt: "2025-02-01" } |
through January 2025 |
{ lt: "2025-02-01" } |
| Language | Code | Import |
|---|---|---|
| English | en |
chronolizer or chronolizer/locales/en |
| German | de |
chronolizer/locales/de |
| Spanish | es |
chronolizer/locales/es |
| French | fr |
chronolizer/locales/fr |
| Dutch | nl |
chronolizer/locales/nl |
| Turkish | tr |
chronolizer/locales/tr |
| Czech | cs |
chronolizer/locales/cs |
| Polish | pl |
chronolizer/locales/pl |
A date filter has at least one bound. It can have one lower bound and one upper bound.
| Key | Meaning |
|---|---|
gt |
After the value, exclusive |
gte |
On or after the value |
lt |
Before the value, exclusive |
lte |
On or before the value |
An expression starts with now or an ISO date. Operations run from left to right.
Calendar shifts are not combined or canceled. For example, 2025-01-31||+1M+1M
resolves to March 28, not March 31. Each month shift clamps the day to the last
valid day of that month. Time-zone gaps can also affect intermediate dates.
expression := "now" operation* | YYYY-MM-DD | YYYY-MM-DD "||" operation+
operation := ("+" | "-") positiveInteger unit | "/" unit
unit := "d" | "w" | "M" | "q" | "y"
/unit moves a value to the start of its calendar unit. Add || before operations on a fixed date, for example 2025-01-01||+1M.
A complete calendar period includes its start but excludes the start of the next period. Weeks start on Monday.
A named day without a year uses the current calendar year. Write the year for February 29 because parsing does not read the clock.
| Export | Purpose |
|---|---|
parseNatural |
Parse complete natural-language input to a date range |
formatNatural |
Write a supported range as one standard phrase |
suggestNatural |
Return autocomplete suggestions and date ranges |
parseFilter |
Parse a date filter to a date range |
formatFilter |
Format a date range as a date filter |
resolve |
Calculate dates with an Effect clock and time zone |
DateFilter |
Validate external date-filter data with Effect Schema |
DateRangeFromFilter |
Convert a filter in both directions with one Effect Schema |
Chronolizer is available under the MIT License.