Problem
My rows carry dates as strings like "2025-09-02", and I want to work
with them as dates — day of week, grouping by month, readable labels.
Setup
Morel 0.9 added the Date and Time structures. Pin the timezone
first, or a date built without an explicit offset picks up whatever
your machine is set to and your output stops being reproducible:
Sys.set ("timeZone", "UTC");
val orders = [
{ id = 1, quantity = 12, date = "2025-09-02" },
{ id = 2, quantity = 30, date = "2025-09-03" },
{ id = 6, quantity = 20, date = "2025-10-12" },
{ id = 8, quantity = 25, date = "2025-10-18" },
{ id = 11, quantity = 16, date = "2025-11-27" },
{ id = 12, quantity = 22, date = "2025-11-30" }
];
Example
Date.fromString won't read an ISO string, so split the parts and
build the date yourself:
fun monthOf 1 = Jan | monthOf 2 = Feb | monthOf 3 = Mar | monthOf 4 = Apr
| monthOf 5 = May | monthOf 6 = Jun | monthOf 7 = Jul | monthOf 8 = Aug
| monthOf 9 = Sep | monthOf 10 = Oct | monthOf 11 = Nov | monthOf _ = Dec;
fun parseDate s =
case List.map Int.fromString (String.tokens (fn c => c = #"-") s) of
[SOME y, SOME m, SOME d] =>
Date.date { year = y, month = monthOf m, day = d, hour = 0,
minute = 0, second = 0, offset = SOME Time.zeroTime }
| _ => raise Fail ("not an ISO date: " ^ s);
from ord in orders
yield { ord.id, weekday = Date.weekDay (parseDate ord.date) };
val it =
[{id=1,weekday=Tue},{id=2,weekday=Wed},{id=6,weekday=Sun},
{id=8,weekday=Sat},{id=11,weekday=Thu},{id=12,weekday=Sun}]
: {id:int, weekday:weekday} list
What's happening
parseDate : string -> date is the piece you have to write. The
rough edge is that Date.fromString follows Standard ML and expects
"Tue Sep 02 00:00:00 2025", not ISO 8601 — feed it "2025-09-02"
and you get NONE with no explanation. Splitting on #"-" and
calling Date.date is the reliable route, and raise Fail on the
fallthrough means a malformed row fails loudly instead of silently
becoming a wrong date. Note that raise is all you get — Morel 0.9
has no handle, so the failure stops the query rather than being
caught and skipped.
What you get back is worth the ceremony. month and weekday are
real datatypes, not integers or strings, so Date.month d = Oct is a
typed comparison the compiler checks — misspell Otc and it's a
compile error, not an empty result set. That's the difference from a
string-typed date column, where WHERE month = 'Otc' just quietly
returns nothing.
Variations
Group by month. The key is a month value, so the output column has
type month:
from ord in orders
group { month = Date.month (parseDate ord.date) }
compute { orders = count over (), totalQty = sum over ord.quantity };
val it =
[{month=Sep,orders=2,totalQty=42},{month=Oct,orders=2,totalQty=45},
{month=Nov,orders=2,totalQty=38}]
: {month:month, orders:int, totalQty:int} list
Filter on a month and format for display with Date.fmt, which takes
the same % codes as C's strftime:
from ord in orders
where Date.month (parseDate ord.date) = Oct
yield { ord.id, label = Date.fmt "%d %b %Y" (parseDate ord.date) };
val it = [{id=6,label="12 Oct 2025"},{id=8,label="18 Oct 2025"}]
: {id:int, label:string} list
See also
- Recipe 04 — Load a CSV — where date strings usually come from, since every CSV column arrives as
string. - Recipe 10 — Group and aggregate — the
group … computestep this recipe reuses. - Recipe 14 — Clean and normalise strings —
String.tokensand the rest of the splitting toolkit.