A camel dancing in the desert at dusk, ringed by a swirl of clocks, hourglasses and numerals

Time::Keeper::Natural: What time is 'next Tuesday at 3 pm'?

Nobody has ever said "let's meet at 2026-10-20T15:00:00+01:00". People say "next Tuesday at 3 pm", or "end of the month", or "in a couple of hours", and then a programmer somewhere writes a regular expression, and then another one for the case they forgot, and then a third one for French.

Time::Keeper::Natural reads the words people use for a time into Time::Keeper values. It is written in XS, and it gets its words from CLDR, the same data your phone uses to write dates, so it reads what Time::Keeper writes in every one of CLDR's 1,121 locales.

Every example below has the clock at today, Tuesday 20 October 2026, 09:00 in London.

use Time::Keeper::Natural;

my $n = Time::Keeper::Natural->new(zone => 'Europe/London', locale => 'en-GB');

$n->parse('tomorrow');                 # 2026-10-21T00:00:00+01:00
$n->parse('tomorrow at 9 am');         # 2026-10-21T09:00:00+01:00
$n->parse('next Tuesday at 3 pm');     # 2026-10-27T15:00:00+00:00
$n->parse('end of last month');        # 2026-09-30T23:59:59.999999999+01:00
$n->parse('in two weeks');             # 2026-11-03T09:00:00+00:00
$n->parse('3 days after tomorrow');    # 2026-10-24T00:00:00+01:00
$n->parse_duration('1.5 hours');       # PT1H30M
my ($from, $to) = $n->parse_interval('15–17 Oct 2026');

Note "next Tuesday" and "in two weeks" land at +00:00. The clocks go back on Sunday, and it knows.

It speaks your language (and your grammar)

$n->in_locale('fr')->parse('il y a 3 heures');           # 2026-10-20T06:00:00+01:00
$n->in_locale('ja')->parse('来月末');                     # 2026-11-30T23:59:59.999999999+00:00
$n->in_locale('de')->parse('Ende nächsten Monats');      # 2026-11-30T23:59:59.999999999+00:00
$n->in_locale('ru')->parse('в третью пятницу ноября');   # 2026-11-20T00:00:00+00:00

German puts "next month" in the genitive after "Ende". Russian puts Friday in the accusative and has no "of" at all. Japanese puts the month first and "end" last. CLDR knows none of this, so each language has a small word table that knows just enough to send the text back to the words CLDR does know. They are first drafts, and every one says in its header what its language still cannot do.

It will not guess

In Hindi, "कल" means both yesterday and tomorrow. Context decides, and a parser has none, so it says so:

$n->in_locale('hi')->parse('कल');
# cannot read 'कल' at 'कल': 'कल' means yesterday and tomorrow in hi; pass prefer

$n->in_locale('hi')->new(prefer => 'future')->parse('कल');   # 2026-10-21T00:00:00+01:00

And a word it does not know is not a time:

$n->try_parse('sometime next week');   # undef

When it does fill something in, it tells you. match gives back what it read and what it assumed: the year, the time of day, the zone.

Finding times in a sentence

New in 0.04, find reads a whole message and hands back every time in it, each with its place in the text:

my $text = 'Can we meet next Tuesday at 3 pm, or on the 30th?';
for my $m ($n->find($text)) {
    say substr($text, $m->start, $m->end - $m->start), ' => ', $m->value;
}
# next Tuesday at 3 pm => 2026-10-27T15:00:00+00:00
# on the 30th => 2026-10-30T00:00:00+00:00

It is strict on purpose. "I bought 3 apples" is not a time, and "See you in May" only counts if you ask for names, because May is also a person and a verb. It reads a 200 character sentence about 20,000 times a second, so you can point it at your inbox without feeling bad.

Getting it

cpanm Time::Keeper::Natural

All writing