Repository: blacksmithgu/obsidian-dataview
Pull request #361 (2021-07-29 07:14:34)

add support for date milliseconds, 'Z' time zone
Adds support for e.g.:

- `1984-08-15T12:42:59.123`
- `1984-08-15T12:42:59.123Z`
- `1984-08-15T12:42:59Z`

This is sufficient to add support for `new Date().toISOString()`, even if the values from that are super wonky. Closes https://github.com/blacksmithgu/obsidian-dataview/issues/359


Patch:
diff --git a/src/expression/parse.ts b/src/expression/parse.ts
index bf928dd..695fe0f 100644
--- a/src/expression/parse.ts
+++ b/src/expression/parse.ts
@@ -221,17 +221,23 @@ export const EXPRESSION = P.createLanguage<ExpressionLanguage>({
     }).desc("'and' or 'or'"),
 
     // A date which can be YYYY-MM[-DDTHH:mm:ss].
-    // TODO: Add time-zone support.
     // TODO: Will probably want a custom combinator for optional parsing.
     rootDate: q => P.seqMap(P.regexp(/\d{4}/), P.string("-"), P.regexp(/\d{2}/), (year, _, month) => {
         return DateTime.fromObject({ year: Number.parseInt(year), month: Number.parseInt(month) })
-    }).desc("date in format YYYY-MM[-DDTHH-MM-SS]"),
+    }).desc("date in format YYYY-MM[-DDTHH-MM-SS.MS]"),
     date: q => chainOpt<DateTime>(q.rootDate,
         (ym: DateTime) => P.seqMap(P.string("-"), P.regexp(/\d{2}/), (_, day) => ym.set({ day: Number.parseInt(day) })),
         (ymd: DateTime) => P.seqMap(P.string("T"), P.regexp(/\d{2}/), (_, hour) => ymd.set({ hour: Number.parseInt(hour) })),
         (ymdh: DateTime) => P.seqMap(P.string(":"), P.regexp(/\d{2}/), (_, minute) => ymdh.set({ minute: Number.parseInt(minute) })),
         (ymdhm: DateTime) => P.seqMap(P.string(":"), P.regexp(/\d{2}/), (_, second) => ymdhm.set({ second: Number.parseInt(second) })),
-        (dt: DateTime) => P.seqMap(P.string("+").or(P.string("-")), P.regexp(/\d{1,2}(:\d{2})?/), (pm, hr) => dt.setZone("UTC" + pm + hr))
+        (ymdhms: DateTime) => P.alt(
+            P.seqMap(P.string("."), P.regexp(/\d{3}/), (_, millisecond) => ymdhms.set({ millisecond: Number.parseInt(millisecond)})),
+            P.succeed(ymdhms) // pass
+        ),
+        (dt: DateTime) => P.alt(
+            P.seqMap(P.string("+").or(P.string("-")), P.regexp(/\d{1,2}(:\d{2})?/), (pm, hr) => dt.setZone("UTC" + pm + hr)),
+            P.seqMap(P.string("Z"), () => dt.setZone("utc"))
+        )
     ),
 
     // A date, plus various shorthand times of day it could be.


Test patch:
diff --git a/src/test/parse.expression.test.ts b/src/test/parse.expression.test.ts
index 5b45ed1..e2717c1 100644
--- a/src/test/parse.expression.test.ts
+++ b/src/test/parse.expression.test.ts
@@ -120,15 +120,56 @@ test("Parse Year-Month-DayTHour:Minute:Second", () => {
     expect(date.second).toBe(59);
 });
 
-test("Parse Year-Month-DayTHour:Minute:Second-Offset", () => {
-    let date = EXPRESSION.date.tryParse("1984-08-15T12:40:50-07:00");
-    expect(date.zoneName).toBe("UTC-7");
+test("Parse Year-Month-DayTHour:Minute:Second", () => {
+    let date = EXPRESSION.date.tryParse("1984-08-15T12:42:59");
+    expect(date.year).toBe(1984);
+    expect(date.month).toBe(8);
+    expect(date.day).toBe(15);
+    expect(date.hour).toBe(12);
+    expect(date.minute).toBe(42);
+    expect(date.second).toBe(59);
+});
+
+test("Parse Year-Month-DayTHour:Minute:Second.Millisecond", () => {
+    let date = EXPRESSION.date.tryParse("1984-08-15T12:42:59.123");
+    expect(date.year).toBe(1984);
+    expect(date.month).toBe(8);
+    expect(date.day).toBe(15);
+    expect(date.hour).toBe(12);
+    expect(date.minute).toBe(42);
+    expect(date.second).toBe(59);
+    expect(date.millisecond).toBe(123);
+
+    let builtin = EXPRESSION.date.tryParse(new Date("1984-08-15T12:42:59.123").toISOString());
+    // only seconds and milliseconds are inconsistent due to Javascript being bad with
+    // time zones, but the goal here is to ensure values are parsed appropriately at least
+    expect(builtin.second).toBe(59);
+    expect(builtin.millisecond).toBe(123);
+});
+
+describe("Parse Year-Month-DayTHour:Minute:Second(.Millisecond?)Timezone", () => {
+    test("Offset", () => {
+        let date = EXPRESSION.date.tryParse("1984-08-15T12:40:50-07:00");
+        expect(date.zoneName).toBe("UTC-7");
+
+        let date2 = EXPRESSION.date.tryParse("1984-08-15T12:40:50+9");
+        expect(date2.zoneName).toBe("UTC+9");
+
+        let date3 = EXPRESSION.date.tryParse("1985-12-06T19:40:10+06:30");
+        expect(date3.zoneName).toBe("UTC+6:30");
+    })
+
+    test("Z", () => {
+        let date1 = EXPRESSION.date.tryParse("1985-12-06T19:40:10Z");
+        expect(date1.zoneName).toBe("UTC");
 
-    let date2 = EXPRESSION.date.tryParse("1984-08-15T12:40:50+9");
-    expect(date2.zoneName).toBe("UTC+9");
+        let date2 = EXPRESSION.date.tryParse("1985-12-06T19:40:10.123Z");
+        expect(date2.zoneName).toBe("UTC");
 
-    let date3 = EXPRESSION.date.tryParse("1985-12-06T19:40:10+06:30");
-    expect(date3.zoneName).toBe("UTC+6:30");
+        // built-in always returns UTC
+        let date3 = EXPRESSION.date.tryParse(new Date().toISOString());
+        expect(date3.zoneName).toBe("UTC");
+    })
 })
 
 test("Parse Today", () => {