1import * as DateTime from "effect/DateTime";
2import * as Option from "effect/Option";
3import * as Schema from "effect/Schema";
4
5const isZonedIsoDateTime = Schema.is(
6 Schema.String.check(
7 Schema.isPattern(
8 /^(?:\d{4}|[+-]\d{6})-(?:0[1-9]|1[0-2])-(?:0[1-9]|[12]\d|3[01])T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?|24:00(?::00(?:\.0+)?)?)(?:Z|[+-](?:[01]\d|2[0-3]):[0-5]\d)$/,
9 ),
10 Schema.isTrimmed(),
11 ),
12);
13
14function parseTimestamp(value: string): number {
15 if (!isZonedIsoDateTime(value)) return Number.NaN;
16
17 // Engines can normalize invalid calendar dates instead of rejecting them.
18 const datePart = value.slice(0, value.indexOf("T"));
19 const date = DateTime.make(`${datePart}T00:00:00.000Z`);
20 if (Option.isNone(date)) return Number.NaN;
21 const parts = DateTime.toPartsUtc(date.value);
22 if (parts.month !== Number(datePart.slice(-5, -3)) || parts.day !== Number(datePart.slice(-2))) {
23 return Number.NaN;
24 }
25 return Date.parse(value);
26}
27
28/** Compare date-time strings by absolute time, with stable handling for malformed stored values. */
29export function compareDateTimeStrings(left: string, right: string): number {
30 const leftTimestamp = parseTimestamp(left);
31 const rightTimestamp = parseTimestamp(right);
32 const leftIsValid = !Number.isNaN(leftTimestamp);
33 const rightIsValid = !Number.isNaN(rightTimestamp);
34
35 if (leftIsValid !== rightIsValid) return leftIsValid ? 1 : -1;
36 if (leftIsValid) return leftTimestamp - rightTimestamp;
37 return left < right ? -1 : left > right ? 1 : 0;
38}