Skip to content

This site describes solve-engine 2.44.0, the version npm installs.

Date literals

Package: DATETIME_PACKAGE. Registered by createEngine(); for a slimmer engine, register it explicitly (see choosing packages).

A date literal is one specific calendar day written out, like the 25th of December 2023. Solve reads several orders, so you can write a date the way you already do, and a setting fixes which order a purely numeric date is read in.

Several orders are recognised. Slashes, hyphens and dots all work, and the month may be spelled out.

25/12/2023 // Monday, December 25, 2023
2023-12-25 // Monday, December 25, 2023
2024-5-3 // Friday, May 3, 2024
25.12.2023 // Monday, December 25, 2023
March 9, 2024 // Saturday, March 9, 2024
25 Dec 2026 // Friday, December 25, 2026
1 Oct 2026 // Thursday, October 1, 2026

A spelled month may be written in full or shortened, Dec or December. oct and dec are also the names of the octal and decimal conversions, and they stay those straight after as, in or to, so 255 as dec is still 255.

Write a literal as one run of characters, with no spaces around its separators. That is what tells a date from the arithmetic it is spelled identically to: 2024-5-3 is a date, and the same digits spaced out are subtraction.

2024 - 5 - 3 // 2,016

Spacing decides it on its own, so a padded chain like 2024 - 05 - 03 is subtraction too.

A meeting, a deadline or a train is a day and a time on that day. Write the time after the date, on its own or after at, or write the time first and the day after on. Each reads as that moment: the same one the ISO form 2026-01-04T14:30 names, so the spellings always agree.

2026-01-04 14:30 // Sunday, January 4, 2026, 2:30:00 PM
23 September 2026 3pm // Wednesday, September 23, 2026, 3:00:00 PM
23 September 2026 at 3pm // Wednesday, September 23, 2026, 3:00:00 PM
3pm on 23 September 2026 // Wednesday, September 23, 2026, 3:00:00 PM
25/12/2026 9:30am // Friday, December 25, 2026, 9:30:00 AM
2026-01-04 14:30:15 // Sunday, January 4, 2026, 2:30:15 PM

The time is written the way a clock time is anywhere else in Solve: 24-hour 14:30, 3pm, 9:30am, 3.30pm, or with seconds, 14:30:15. Midnight is 12am or 00:00, and a moment at midnight shows as the day alone.

Once written, the moment behaves like any other date and time: add a length of time to it, or measure the time between two of them.

23 September 2026 3pm + 90 minutes // Wednesday, September 23, 2026, 4:30:00 PM
hours between 2026-01-04 9am and 2026-01-10 5pm // 152 hours

After on, anything that gives a date will do, a name included, so a note can set the day once and put several times on it:

launch = 23 September 2026
3pm on launch // Wednesday, September 23, 2026, 3:00:00 PM
5:30pm on launch // Wednesday, September 23, 2026, 5:30:00 PM

A time that does not exist on the clock is refused by name rather than read as something else, and a month with no day takes no time, since there is no day to put it on:

2026-01-04 24:00 // ERROR: "24:00" is not a valid time
February 2026 3pm // ERROR: Expected an operator or the end of the line, but found "3pm"

On the days the clocks change, a time in the hour that is skipped or repeated lands where the ISO form puts it. To read the moment in another zone, name the zone the time is in, as in 3pm London on 23 September 2026 in Tokyo (see time zones).

The boundary: a day named in words and then a time, tomorrow 3pm, is not read this way. It belongs with the spoken relative dates, and until then the time-first spelling covers it, since on takes a relative date as readily as a fixed one (3pm on tomorrow).

An all-numeric date can mean two different days. 03/04/2026 is the 3rd of April to most of the world and the 4th of March in the United States, and nothing in the characters says which. The input order is how you settle it.

By default a slash date is read day first (25/12/2023) and a hyphen date month first (12-25-2023), unless it starts with a four-digit year, which makes it ISO. Fix the order for every numeric separator with the date.inputOrder setting:

new ExpressionEngine({ config: { date: { inputOrder: "MDY" } } });
inputOrder12/25/202325/12/20232023/12/252023-12-2525.12.2023
"auto" (default)refused25 December 20236.7425 December 202325 December 2023
"MDY"25 December 2023refused6.7425 December 2023refused
"DMY"refused25 December 20236.7425 December 202325 December 2023
"YMD"refusedrefused25 December 202325 December 2023refused

“Refused” means the line reports what is wrong with the date rather than answering something else; the next section covers what it says and how to turn it off. 2023/12/25 is the one cell that stays arithmetic: a four-digit group at the START of a slash run is ordinary division, because 1000/10/5 is 20 and people write that constantly.

Only ambiguous literals are affected. A spelled-out month (March 9, 2024) is never ambiguous, and neither is a hyphen date that starts with a four-digit year: 2023-12-25 has no reading but year, month, day, so it is read as ISO whichever order is set, timestamp or not.

Reading the order from the reader’s machine

Section titled “Reading the order from the reader’s machine”

A host that runs on the reader’s own machine can ask the machine instead of choosing for everybody. "locale" reads the day, month and year order the operating system writes dates in, so a British notepad reads day first and an American one month first, with no setting to explain to anybody:

const engine = createEngine({ config: { date: { inputOrder: "locale" } } });

A host that is not the reader (a server rendering someone else’s document) names the reader instead, with a BCP-47 language tag:

createEngine({ config: { date: { inputOrder: "locale", inputLocale: "en-US" } } });

inputLocale is read only when inputOrder is "locale". Setting it beside any other order changes nothing at all, which is deliberate: a field that quietly switched inference on would turn a predictable mistake into a wrongly read document rather than into no change.

Whatever you choose, getDateReading() reports what the engine settled on, and where it came from. It is what a settings panel shows and what a bug report should carry:

createEngine({ config: { date: { inputOrder: "locale" } } }).getDateReading();
// { order: "DMY", orderSource: "host-locale", locale: "en-GB" }

orderSource is one of "config" (you named an order), "locale" or "host-locale" (it was read from a tag), "separator" (the "auto" default), or "fallback", which means inference was asked for and could not be made, so the engine is reading dates exactly as "auto" does. A "fallback" is worth surfacing: it is the case where a reader should be offered the choice by hand.

A run of digits that is shaped like a date but that the chosen order cannot read is not answered as arithmetic. It says what is wrong.

There are two things that can be wrong, and they are separate because their fixes are. The order might be wrong, in which case the reading that would work is named:

12/25/2023 // ERROR: "12/25/2023" is not a date read day first: there is no month 25. Read month first it is 25 December 2023. Set date.inputOrder to "MDY" to read numeric dates month first.

Or the date might not exist in any order, in which case no order is suggested, because none would help:

31/04/2026 // ERROR: "31/04/2026" is not a real date: April 2026 has 30 days.
2026-02-29 // ERROR: "2026-02-29" is not a real date: February 2026 has 28 days.
29 February 2026 // ERROR: "29 February 2026" is not a real date: February 2026 has 28 days.

Both are ordinary error results on the line, not exceptions, and they carry through the rest of the expression, so 31/02/2026 + 1 day reports the date rather than answering a day count built on one that does not exist.

Before this behaviour existed, each of those lines answered a number instead: 12/25/2023 was 0.00, 31/04/2026 was 0.00, 2026-02-29 was 1,995, and 29 February 2026 was a fourteen-digit number, 29 multiplied by the instant of the 1st of February. If a document relied on one of the first three, date.onAmbiguous puts those numbers back. The fourth stays refused, since its old number multiplied a date, and a date is refused in multiplication everywhere: a moment has no size to multiply.

new ExpressionEngine({ config: { date: { onAmbiguous: "arithmetic" } } });

The boundary, deliberately: only a run that could not be arithmetic is refused. A four-digit group at the START of a slash run is left alone (1000/10/5 is 20, 1024/8/2 is 64), and so is a run whose groups are all one or two digits (12/13/14 is 0.07), because a two-digit year is too weak a signal to hang a refusal on.

1000/10/5 // 20
1024/8/2 // 64
12/13/14 // 0.07

The one refusal onAmbiguous cannot switch off is the dot form, and it is the one place this release takes an answer away. The dot form used to ignore date.inputOrder altogether and always read day first, so a month-first engine answered 25.12.2026 as 25 December while refusing 12.25.2026 outright. Both were wrong for that reader, and in opposite directions.

on a month-first enginebeforenow
25.12.2026Friday, December 25, 2026refused: not a date read month first
12.25.2026a parse errorFriday, December 25, 2026

There is no arithmetic reading to fall back to here, because two dots are not division, which is why the setting cannot restore the old answer.