NAME
Time::Keeper::Natural - the words people use for a time, read in every locale
VERSION
Version 0.02
SYNOPSIS
use Time::Keeper::Natural;
# the clock at Thursday 15 October 2026, 09:00 in London
my $n = Time::Keeper::Natural->new(zone => 'Europe/London', locale => 'en-GB');
$n->parse('tomorrow'); # 2026-10-16T00:00:00+01:00
$n->parse('in 3 days'); # 2026-10-18T09:00:00+01:00
$n->parse('next Tuesday at 3 pm'); # 2026-10-20T15:00:00+01:00
$n->parse('end of last month'); # 2026-09-30T23:59:59.999999999+01:00
$n->parse('in two weeks'); # 2026-10-29T09:00:00+00:00
$n->parse_duration('1 hour, 30 minutes'); # PT1H30M
my ($from, $to) = $n->parse_interval('15 – 17 Oct 2026');
$n->in_locale('fr')->parse('il y a 3 heures'); # 2026-10-15T06:00:00+01:00
$n->in_locale('ja')->parse('来月末'); # 2026-11-30T23:59:59.999999999+00:00
$n->try_parse('sometime'); # undef
my $m = $n->match('15 October');
$m->assumed; # ('time', 'year', 'zone')
Time::Keeper::Natural->parse('tomorrow', zone => 'Asia/Tokyo');
It reads relative phrases ("RELATIVE TIME"), durations ("DURATIONS"), dates and times in the locale's own formats, alone or on a day ("ABSOLUTE DATES AND TIMES"), intervals ("INTERVALS"), and, where a locale has a word table, "end of", "start of", "at" and "on" ("WORDS"). A method that cannot read a text dies saying what it could not read.
DESCRIPTION
A natural phrase for a time, "next Tuesday at 3 pm", "in two weeks", "end of last month", read into a Time::Keeper value, a Time::Keeper::Date or a Time::Keeper::Duration, relative to a moment called the anchor, in the locale's own words.
What it promises comes in two tiers, and coverage says which tier a locale is in.
What it reads in every locale
Whatever Time::Keeper writes in a locale, this module reads back in that locale: the relative time format_relative writes ("3 hours ago", "yesterday", "il y a 3 heures"), the durations Duration->format writes ("1 hour, 30 minutes", "1:05:09"), the intervals format_interval writes ("15–17 Oct 2026"), and the locale's own date and time formats and skeletons, in all 1,121 locales of CLDR, with digits in any script. The test suite proves it by writing each of these and reading it back.
What it reads where it has the words
CLDR has no words for "end of", "start of", "at", "on" or "the". Those come from a small word table the distribution ships per locale, English first. coverage answers words for a locale with a table and data for one without, which reads everything in the first tier and none of this.
WHAT IT DOES NOT READ
- Free text.
-
A text is read whole or not at all: "let's meet next Tuesday" dies. Finding a date somewhere inside a sentence is another job, with its own guesses.
- Recurrence and vague words.
-
"every Tuesday", "sometime next week", "soon".
- Idioms CLDR does not have.
-
"a fortnight", "tonight", "EOD", "Tuesday the 20th", "Tuesday of next week"; and a spelling the locale's rules do not write ("twentyone").
- Holidays and business days.
-
"the day after Thanksgiving", "three working days from now".
- What a language's grammar keeps out of reach.
-
Where "end of" wants a case CLDR does not have (German, Greek and Russian genitives, Hindi's oblique case), the edges read with month names, years and dates only; each table says what it cannot do in its own header.
Three choices a reader will look for, each made once: a bare hour in a locale whose clock has twelve hours dies unless bare_hour says which; a period word alone is the period's first moment ("next month" is the 1st); and "next Tuesday" is the first Tuesday after today, never today.
The word tables other than English's are first drafts, marked unreviewed in their files, and await a native reader.
OPTIONS
Every method takes the options as name and value pairs, either on new or, in the class form, after the text. An unknown or repeated option dies by name.
- now
-
A Time::Keeper value, the anchor that "tomorrow" and "in 3 days" are counted from. Without it the anchor is
Time::Keeper->now, which reads$Time::Keeper::CLOCK, so a test pins it withlocal. - zone
-
The zone of the result, any zone Time::Keeper takes; an anchor given as
nowis read in it. Without it the result is in the anchor's zone, which is UTC when the anchor is the clock. - locale
-
The locale whose words are read and the result carries, any code Time::Keeper knows; an anchor given as
nowis read in it, which also decides its week's first day. Without it the anchor's locale is used, English when the anchor is the clock. - bare_hour
-
What a bare hour with no half of the day means in a locale whose clock has twelve hours:
am,pm, ornext, the first reading after the anchor on that day. Without it such an hour dies. In a locale whose clock has twenty-four hours a bare hour is the hour. - prefer
-
futureorpast: which side of the anchor to take where the text does not say. It does four things, all in that one sense: a date with no year moves to the later or the earlier year; a bare weekday is the coming or the last one; a phrase CLDR writes for two days (Hindi's "कल", yesterday and tomorrow) is the later or the earlier of them; and a day of the month with no month ("the 15th") is the next or the last such day. Without it the anchor's year and month are taken, a bare weekday is the coming one, and a phrase with two meanings dies. - keep_zone
-
True to leave a value read with a zone in its text ("3 pm EST") in that zone, rather than bring it into the parser's ("ZONES").
METHODS
new
my $n = Time::Keeper::Natural->new(zone => 'Europe/London', locale => 'fr');
my $m = $n->new(bare_hour => 'pm');
A parser with the options given. Called on a parser, a new one with that parser's options and these on top; an option given as undef is dropped. A parser is immutable.
parse
my $t = $n->parse('next Tuesday at 3 pm');
my $t = Time::Keeper::Natural->parse('tomorrow', zone => 'Asia/Tokyo');
The Time::Keeper value the whole text names, or a death saying what it could not read ("ERRORS"). A phrase that names a day and no time is the day's first moment, as today is. The value is in the parser's zone and locale.
try_parse
As parse, but undef where parse would die, with $@ left as it was.
parse_date
The Time::Keeper::Date of the moment the text names: "next Tuesday" is a date, and "in 3 hours" is the date of that moment.
try_parse_date
As parse_date, with undef for a death.
parse_duration
The Time::Keeper::Duration the text names, "2 weeks", "1 hour, 30 minutes", "1:05:09", "PT90M" ("DURATIONS"); a text that names a moment rather than a length dies.
try_parse_duration
As parse_duration, with undef for a death.
match
my $m = $n->match('15 October');
$m->value; # 2026-10-15T00:00:00+01:00
$m->assumed; # ('time', 'year', 'zone')
What parse would read, as a Time::Keeper::Natural::Match: the value, the precision the text named, and what was assumed to make it a value. A text parse would die on gives undef, never a death, whatever the reason.
match_date
As match, the value a Time::Keeper::Date and the kind date.
match_duration
As match for what parse_duration reads, the kind duration and the precision none.
parse_interval
my ($start, $end) = $n->parse_interval('Monday to Friday');
The two Time::Keeper values an interval names, its start and its end ("INTERVALS"); a text that names one time dies saying so.
try_parse_interval
As parse_interval, with an empty list for a death.
match_interval
As match for what parse_interval reads: the kind interval, the start as value and the end as to.
coverage
Time::Keeper::Natural->coverage('en-AU'); # words, through en
Time::Keeper::Natural->coverage('fr'); # data, until French has a table
words for a locale with a word table in its chain, data for one without, which reads everything in the first tier and none of "WORDS". An unknown locale dies.
locales
The codes of the locales with a word table of their own.
in_locale
my $fr = $n->in_locale('fr');
A new parser reading that locale's words, the other options kept.
in_zone
A new parser whose results are in that zone, the other options kept.
now
The anchor: the now given, in the zone given if there is one, else the clock's reading in the zone and locale given. Read afresh on every call, so a clock that moves is followed.
zone
The zone results are in: the option, else the anchor's.
locale
The locale whose words are read: the option as its canonical code, else the anchor's.
bare_hour
The option, or undef.
prefer
The option, or undef.
keep_zone
The option, or undef.
RELATIVE TIME
# the clock pinned to Thursday 2026-10-15T09:00:00 Europe/London
$n->parse('tomorrow'); # 2026-10-16T00:00:00+01:00
$n->parse('now'); # 2026-10-15T09:00:00+01:00
$n->parse('in 3 days'); # 2026-10-18T09:00:00+01:00
$n->parse('3 hours ago'); # 2026-10-15T06:00:00+01:00
$n->parse('in 2 weeks and 3 days'); # 2026-11-01T09:00:00+00:00
$n->parse('next Tuesday'); # 2026-10-20T00:00:00+01:00
$n->parse('last month'); # 2026-09-01T00:00:00+01:00
$n->parse('this week'); # the week's first day, by the locale
$n->in_locale('fr')->parse('après-demain');
$n->in_locale('de')->parse('vor 2 Wochen');
The words are the locale's own, as "relative_names" in Time::Keeper::Locale gives them and "format_relative" in Time::Keeper writes them, so whatever format_relative writes in a locale reads back in it. Their meaning:
- A period word is the start of its period.
-
"today" is the day's first moment, "this week" the first moment of the locale's week (Sunday in
en-US, Monday inen-GB), "next month" the first moment of next month, "last year" 1 January of last year, "this quarter" the quarter's first day. "now" is the anchor itself. - A weekday is a day.
-
"next Tuesday" is the first Tuesday strictly after the anchor, "last Tuesday" the first strictly before, "this Tuesday" the Tuesday of the anchor's locale week, which may be past; "in 2 Tuesdays" the second Tuesday after the anchor. Each is the day's first moment.
- A count of units is added to the anchor.
-
"in 3 days" and "3 days ago" keep the anchor's time of day; "in 3 hours" is three hours of elapsed time. Years, quarters, months, weeks and days are counted on the calendar, as
format_relativecounts them, so a day across a clock change is still a day; hours, minutes and seconds are counted on the clock. - A chain adds its parts in one direction.
-
"in 2 weeks and 3 days", "1 hour, 30 minutes and 10 seconds ago", "dans 2 semaines et 3 jours", "in 2 Wochen und 3 Tagen": the first or the last part carries the direction ("in", "ago"), the others are bare counts, joined by the locale's list words or by nothing. Months are added before days, as
plusadds them. A chain that points both ways dies; one with no direction is a duration, whichparse_durationreads.
A count never dies for a wrong plural: "in 1 days" is one day. A day that lands in a clock change that skipped it moves forward to the first moment that existed. Where CLDR's data gives one string to both directions (its Arabic short month pattern for a few months ago carries the word for "in"), the direction whose marker the text carries wins, so the text means what a reader would take it to mean rather than what the faulty pattern was for.
Some languages write one word for two days, and CLDR keeps it for both: Hindi's "कल" is yesterday and tomorrow, and a speaker tells them apart by the verb's tense, which the parser does not see. Such a phrase dies naming both readings, "'कल' means yesterday and tomorrow in hi; pass prefer", unless prefer says which: future takes the later, past the earlier, and match reports the direction as assumed. The same holds wherever the phrase stands, before a time ("कल 15:00") or after one. The locales whose data has such phrases are few: Arabic, Danish, Hindi and a handful more.
ABSOLUTE DATES AND TIMES
# the clock pinned to Thursday 2026-10-15T09:00:00 Europe/London, en-GB
$n->parse('2026-10-20T15:00:00Z'); # 2026-10-20T16:00:00+01:00, in the parser's zone
$n->parse('15 October 2026'); # 2026-10-15T00:00:00+01:00
$n->parse('15 October'); # the same, the year assumed
$n->parse('15/10/2026'); # the locale's short date
$n->parse('October 2026'); # 2026-10-01T00:00:00+01:00
$n->parse('15:30'); # 2026-10-15T15:30:00+01:00, today
$n->parse('3 pm'); # 2026-10-15T15:00:00+01:00
$n->parse('3 in the afternoon'); # the same
$n->parse('noon'); # 2026-10-15T12:00:00+01:00
$n->parse('next Tuesday 3 pm'); # 2026-10-20T15:00:00+01:00
$n->parse('tomorrow 15:30'); # 2026-10-16T15:30:00+01:00
$n->parse('15 October 9 am'); # 2026-10-15T09:00:00+01:00
$n->in_locale('en-US')->parse('Oct 20'); # 2026-10-20T00:00:00+01:00
$n->parse('Oct 20'); # dies: en-GB writes 20 Oct
A date or a time is read through the locale's own formats, so the order of day and month is the locale's and never a guess. The forms tried, in turn: what "parse" in Time::Keeper reads (ISO 8601, the RFC dates, @epoch), in the parser's zone when the text names none; the locale's four date-and-time, four date and four time formats; every skeleton of its available_formats, with and without zero padding and, for a time, with and without its spaces ("3pm"); a bare hour; and noon or midnight. A date with no year takes the anchor's year (prefer moves one that is past or to come to the other side; today stands); a date with no time is the day's first moment; a time with no date is on the anchor's date. A weekday that does not fit the date dies saying what day it was.
A bare hour is the hour in a locale whose clock has twenty-four; in one whose clock has twelve it dies unless bare_hour says am, pm or next (the first reading after the anchor that day, else the later one). "3 in the afternoon" places the hour by the locale's day-period rules.
A day and a time combine either way round: a relative day or an absolute date, and a time, joined by a space ("next Tuesday 3 pm", "3 pm tomorrow", "15 October 9 am", "明日15:30"). The time replaces the day's; a time on a phrase that already names one ("in 3 hours 5 pm") dies.
$n->parse('the 15th'); # 2026-10-15T00:00:00+01:00
$n->parse('15th October'); # 2026-10-15T00:00:00+01:00
$n->parse('the 1st of next month'); # 2026-11-01T00:00:00+00:00
$n->parse('the 2nd at 3 pm'); # 2026-10-02T15:00:00+01:00
$n->in_locale('fr')->parse('le 1er octobre');
A day of the month may be written as an ordinal in digits, as the locale's CLDR rules write one ("15th", "1er", "1.º", "2:a"; about forty locales have them, "NUMBER WORDS"): in the locale's own date order, before a month and the word table's "of", or alone, when it is that day of the anchor's month and match assumes the month (under prefer, the next or the last such day). A time may follow it. The ordinal is read only where the text does not read without it, so a phrase that already meant something keeps its meaning (Vietnamese "thứ 2" is Monday), and a month without the day dies saying so ("2026-11 has no day 31").
ZONES
$n->parse('3 pm EST'); # 2026-10-15T21:00:00+01:00
$n->parse('15:30 Europe/Paris'); # 2026-10-15T14:30:00+01:00
$n->parse('9:00 GMT+2'); # 2026-10-15T08:00:00+01:00
$n->parse('tomorrow 9:00 America/New_York'); # 2026-10-16T14:00:00+01:00
$n->new(keep_zone => 1)->parse('3 pm EST'); # 2026-10-15T15:00:00-05:00
$n->in_locale('fr')->parse('15:30 heure d’été d’Europe centrale');
A zone may follow what is read: an IANA name, UTC or GMT; one of the locale's own zone names, specific, generic or a city ("Eastern Standard Time", "Pacific Time", "Paris Time"), or its GMT format ("GMT+2", "UTC+2"); an offset ("+05:30"); or an abbreviation. The text is read in that zone and the value brought into the parser's zone, unless keep_zone, and match no longer assumes the zone.
A standard or daylight name keeps its offset out of season: "3 pm EST" in October is 15:00 at -05:00, though New York is then on daylight time. An abbreviation the locale's CLDR names do not have ("JST", "CEST" in English) is read where every zone that uses it, in the tz database, gives it one offset, and dies where they differ ("IST" is India, Ireland and Israel), unless the parser's zone is one that uses it. A time that falls in the named zone's gap moves forward, as one in the parser's zone does.
WORDS
$n->parse('end of last month'); # 2026-09-30T23:59:59.999999999+01:00
$n->parse('start of next week'); # 2026-10-19T00:00:00+01:00
$n->parse('the end of the year'); # 2026-12-31T23:59:59.999999999+00:00
$n->parse('end of October'); # 2026-10-31T23:59:59.999999999+00:00
$n->parse('end of today'); # 2026-10-15T23:59:59.999999999+01:00
$n->parse('tomorrow at 3 pm'); # 2026-10-16T15:00:00+01:00
$n->parse('at 3 pm'); # 2026-10-15T15:00:00+01:00
$n->parse('on Tuesday at 9 am'); # 2026-10-20T09:00:00+01:00
$n->parse('Tuesday'); # 2026-10-20T00:00:00+01:00
CLDR has no words for the start or the end of a period, for the "at" between a day and a time, for "on" before a day, or for "the". A locale gets them from a small table the distribution ships, words/<code>.txt beside the module (Time::Keeper::Natural::Words), English first; a locale whose chain has no table reads everything else and none of these, and coverage says which.
Word tables ship for: ar, bn, de, el, en, es, fr, hi, it, ja, ko, nl, pt, ru, th, zh.
Every table but English's is unreviewed, written as the first draft of each language and marked so in its file; each carries the phrases its test lines prove and says what its grammar keeps out of reach. A language that puts "end" after the period ("来月末", "다음 달 말", "অক্টোবরের শেষ") is written as a frame, and an article a contraction swallowed ("fin du mois dernier") is put back before giving up. A language whose "end of" wants a case CLDR does not have (German's genitive, Russian's) reads its edges with month names, years and dates only. The table's keys, the whole of them: start_of, end_of, at, on, skip (words dropped where a part begins), and (a chain joiner, where the locale's list pattern has none), of and last (for "the last Friday of October") and range (two points as an interval, see "INTERVALS").
"start of" and "end of" take any period the parser reads: a period word ("last month", "next week", "today"), a weekday ("next Tuesday"), a date ("15 October"), a month ("October", "October 2026"), a year ("2026"), or a bare unit name ("the month", "the year") for the current one. The result is the period's first moment or its last nanosecond, by the locale's week. A phrase that names a moment ("end of in 3 hours") dies: a moment has no end.
A bare weekday name is the coming one, today included ("Thursday" on a Thursday is today), or the past one under prefer => 'past'. This is the one place a name is given a direction, because "Tuesday at 3 pm" in a message means the coming Tuesday; match reports it as assumed.
NUMBER WORDS
$n->parse('in two weeks'); # 2026-10-29T09:00:00+00:00
$n->parse('twenty one days ago'); # 2026-09-24T09:00:00+01:00
$n->parse('in a week'); # the article is one
$n->in_locale('fr')->parse('dans deux semaines');
$n->in_locale('de')->parse('vor einundzwanzig Tagen');
$n->parse_duration('two hours and thirty minutes');
$n->parse('the third Friday of next month'); # 2026-11-20T00:00:00+00:00
$n->parse('the last Friday of October'); # 2026-10-30T00:00:00+00:00
Where a count is read as digits it is also read as a word, 0 to 99, in the locales whose CLDR data has spellout rules, about ninety of them (Time::Keeper::Natural::Numbers); every form those rules write is read, with hyphens or spaces, and no other. English's "a" and "an" are one. The day ordinals in digits come the same way, from the locales' %digits-ordinal rules, English's "st", "nd", "rd" and "th" chosen by CLDR's ordinal plural rules when the distribution is built.
"the third Friday of next month" counts a weekday in a month: an ordinal word, 1 to 5, or the word table's "last", then a weekday name, the table's "of", and anything that names a month ("next month", "October", "October 2026"). A month with no fifth Friday dies saying so, and so does a week or a year in place of the month. This needs the table's of and last keys, so it reads where a locale has a word table.
DURATIONS
$n->parse_duration('1 hour, 30 minutes'); # PT1H30M
$n->parse_duration('1 hr 30 min'); # PT1H30M
$n->parse_duration('1:05:09'); # PT1H5M9S
$n->parse_duration('2 weeks'); # P14D, a duration's days
$n->parse_duration('1 year, 2 months and 3 days'); # P1Y2M3D
$n->parse_duration('PT90M'); # PT1H30M
$n->in_locale('de')->parse_duration('1 Std., 30 Min.');
$n->in_locale('ar')->parse_duration('ساعتان و30 دقيقة');
Whatever Duration->format writes in a locale and a style, parse_duration reads back in it. Three forms are tried in turn: the ISO 8601 form Duration->parse reads; the locale's digital clock ("1:05:09", "1.05.09" in Finnish), where two parts are hours and minutes, as the digital style writes them, never minutes and seconds; and the locale's unit words in any width, joined by its list words, by commas or by nothing, in any order ("30 minutes and 1 hour"). A unit given twice dies. Where one word serves two units in a width (English's narrow "m" is months and minutes), the chain decides: a part is read as the nearest unit below the one before it, and a lone "30m" is minutes.
Parts are unsigned, as Duration->format writes them: a text with a sign dies naming it. Milliseconds and microseconds become nanoseconds. parse of a duration dies saying parse_duration reads it, and parse_duration of a moment dies saying it is a time.
INTERVALS
$n->parse_interval('15 – 17 Oct 2026'); # the 15th 00:00, the 17th 23:59:59.999999999
$n->parse_interval('9:00 – 10:30'); # 09:00, 10:30 today
$n->parse_interval('2026-10-15T09:00/PT2H'); # 09:00, 11:00
$n->parse_interval('Monday to Friday'); # next Monday 00:00, that Friday's end
$n->parse_interval('from 9 am to 5 pm'); # 09:00, 17:00 today
$n->parse_interval('10 pm to 2 am'); # 22:00 today, 02:00 tomorrow
$n->in_locale('ja')->parse_interval('2026年10月15日~17日');
Three forms are tried in turn. The ISO 8601 form with a /, as Time::Keeper->parse_interval reads it, ends included. What format_interval writes, in every locale: the text is read against each of the locale's interval formats, split where the field that differs begins again, and what the two ends share, written once, is lent to the end without it ("15 – 17 Oct 2026" is October 2026 for both). And, where the locale has the words, two phrases round a range phrase of its word table ("from {0} to {1}", "between {0} and {1}") or round its dash, each read as parse reads it and the second against the first: "Monday to Friday" is the Friday after that Monday, and "15 to 17 October" the 15th of the second's month. A hyphen is read for the locale's dash. Where the locale writes the end first round its dash, as Balochi does, so is it read.
An end that names a day or a longer period runs to that period's last nanosecond, as end_of gives it ("Monday to Friday" ends as Friday does); an end that names a time is that instant ("9 to 5" ends at 5). An end named without a date that falls before the start is the next one: "10 pm to 2 am" ends tomorrow. A bare hour at the end is the next one after the start whatever bare_hour says ("9 am to 5" ends at 17:00); the start's follows bare_hour. An interval whose end is still before its start dies, and so does a text that names one time.
The precision is the start's, and the Match assumes what either end assumed.
my $m = $n->match('next Tuesday at 3 pm');
$m->value; # 2026-10-20T15:00:00+01:00
$m->precision; # hour
$m->assumed; # ('zone')
$n->match('Tuesday')->assumed; # ('weekday next', 'time', 'zone')
$n->match('nonsense'); # undef
parse gives a value and leaves the caller unable to tell what the text said from what was filled in. match gives both; see Time::Keeper::Natural::Match for the list of what can be assumed, which is fixed.
ERRORS
Every method that reads a text either returns what it names or dies. A death from reading has one shape:
Time::Keeper::Natural->parse: cannot read 'next week zzz' at 'zzz': no such word in en
The text, the first token that could not be read, and the reason. The reasons are a fixed set, and the parser refuses to give any other: the text is empty, no such word in <locale>, one direction a phrase, a duration, not a time; parse_duration reads it, a time, not a duration, <unit> is given twice, a sign; a duration is unsigned, a bare hour; say am, pm or pass bare_hour, <date> was a <weekday>, a time on a time, a moment, not a period, nothing left after '<word>', an ordinal weekday is counted in a month, not a <unit>, no month has a <nth> <day>, <yyyy-mm> has no <nth> <day>, and coverage of <locale> is data; '<phrase>' needs its words, given when a locale with no word table is handed English's "end of" or "at": the text may be right, and the locale has no words for it; and '<phrase>' means <reading> and <reading> in <locale>; pass prefer, for a phrase CLDR writes for two days ("RELATIVE TIME"); '<abbreviation>' names more than one zone in <locale> ("ZONES"); and from parse_interval, the interval ends before it starts and a single time, not an interval. An option that is wrong dies from new, naming the option. The try_ forms give undef (try_parse_interval an empty list) for any of these and leave $@ alone.
TESTING
local $Time::Keeper::CLOCK = Time::Keeper->parse('2026-10-15T09:00:00Z');
Pins the clock the anchor is read from, for one block; see "TESTING" in Time::Keeper. Every test in this distribution does so.
SEE ALSO
Time::Keeper, Time::Keeper::Locale, Time::Keeper::Duration, Time::Keeper::Natural::Match, Time::Keeper::Natural::Words, Time::Keeper::Natural::Numbers, Time::Keeper::Natural::Lexicon.
Date::Parse and Time::ParseDate read English, by rules of thumb. chrono (JavaScript) and dateparser (Python) find dates inside free text.
CLDR's date fields and spellout rules, which this module reads backwards: https://cldr.unicode.org/.
AUTHOR
LNATION <email@lnation.org>
BUGS
Please report any bugs or feature requests to bug-time-keeper-natural at rt.cpan.org, or through the web interface at https://rt.cpan.org/NoAuth/ReportBug.html?Queue=Time-Keeper-Natural.
LICENSE AND COPYRIGHT
This software is Copyright (c) 2026 by LNATION.
This is free software, licensed under:
The Artistic License 2.0 (GPL Compatible)
The number words and the locale data are from the Unicode Common Locale Data Repository, Copyright (c) 1991-2026 Unicode, Inc., under the Unicode License v3.