|
| 1 | +-- ISO 8601-2 Extended Date/Time Format Level 1 (partial) |
| 2 | +-- See https://www.loc.gov/standards/datetime/ |
| 3 | +-- Not yet supported, but unlikely to be needed in a bibliographic context: |
| 4 | +-- Year prefix by Y (before -9999 or after 9999) |
| 5 | +-- Unspecified digit(s) from the right (e.g. 202X, 2024-XX, etc.) |
| 6 | + |
| 7 | +-- Examples: |
| 8 | +-- "2020-01" (January 2020) |
| 9 | +-- "2020-23" (Autumn 2020) |
| 10 | +-- "2020-11-01" (November 1, 2020) |
| 11 | +-- "2020-12-01T12:13:55+01:23" (December 1, 2020, 12:13:55 PM, UTC+1:23) |
| 12 | +-- "2020-12-01T12:13:55Z" (December 1, 2020, 12:13:55 PM, UTC) |
| 13 | +-- "2020-12-01T12:13:55" (December 1, 2020, 12:13:55 PM, local considered as UTC here) |
| 14 | +-- "2033?" (2033, approximate / circa) |
| 15 | +-- "2033-01~" (January 2033, approximate month |
| 16 | +-- "2033-01-12%" (January 12, 2033, approximate day) |
| 17 | +-- "2033-01-01/2033-01-12" (January 1-12, 2033) |
| 18 | +-- "/2033-01-12" (up to January 12, 2033) |
| 19 | +-- "2033-01-31/" (from January 31, 2033) |
| 20 | + |
| 21 | +local lpeg = require("lpeg") |
| 22 | +local R, S, P, C, Ct, Cg = lpeg.R, lpeg.S, lpeg.P, lpeg.C, lpeg.Ct, lpeg.Cg |
| 23 | + |
| 24 | +local digit = R("09") |
| 25 | +local dash = P("-") |
| 26 | +local colon = P(":") |
| 27 | +local slash = P("/") |
| 28 | +local yapprox = P("?") / function () |
| 29 | + return "true" |
| 30 | +end |
| 31 | +local mapprox = P("~") / function () |
| 32 | + return "true" |
| 33 | +end |
| 34 | +local dapprox = P("%") / function () |
| 35 | + return "true" |
| 36 | +end |
| 37 | +-- time |
| 38 | +local D2 = digit * digit / tonumber |
| 39 | +local offset = P("Z") |
| 40 | + + C(S("+-")) |
| 41 | + * C(D2) |
| 42 | + * colon |
| 43 | + * C(D2) |
| 44 | + / function (s, h, m) |
| 45 | + local sign = s == "+" and 1 or -1 |
| 46 | + return { hour = h * sign, minute = m * sign } |
| 47 | + end |
| 48 | +local timespec = P("T") |
| 49 | + * Cg(D2, "hour") |
| 50 | + * colon |
| 51 | + * Cg(D2, "minute") |
| 52 | + * colon |
| 53 | + * Cg(D2, "second") |
| 54 | + * Cg(offset ^ -1, "offset") |
| 55 | +-- year from -9999 to 9999 |
| 56 | +local D4 = digit * digit * digit * digit / tonumber |
| 57 | +local year = D4 + P(dash) * D4 |
| 58 | +-- month 01-12 |
| 59 | +local month = (P("0") * R("19") + P("1") * R("02")) / tonumber |
| 60 | +-- season 21-24 (Spring, Summer, Autumn, Winter) |
| 61 | +local season = P("2") * R("14") / function (s) |
| 62 | + return tonumber(s) - 20 |
| 63 | +end |
| 64 | +-- day 01-31 (unverified) |
| 65 | +local day = D2 / tonumber |
| 66 | +-- date |
| 67 | +local datespec = Cg(year, "year") * Cg(yapprox, "approximate") |
| 68 | + + Cg(year, "year") * (dash * Cg(month, "month") * Cg(mapprox, "approximate")) |
| 69 | + + Cg(year, "year") * (dash * Cg(month, "month") * (dash * Cg(day, "day") * Cg(dapprox, "approximate"))) |
| 70 | + + Cg(year, "year") * (dash * Cg(season, "season")) |
| 71 | + + Cg(year, "year") * (dash * Cg(month, "month") * (dash * Cg(day, "day") * timespec ^ -1) ^ -1) ^ -1 |
| 72 | +local date = Ct(datespec) |
| 73 | + / function (t) |
| 74 | + return { |
| 75 | + year = t.year, |
| 76 | + season = t.season, |
| 77 | + month = t.month, |
| 78 | + approximate = t.approximate, |
| 79 | + day = t.day, |
| 80 | + -- N.B Local time does not make sense in a blibliographic context |
| 81 | + -- so we ignore the offset and consider all times to be UTC even without a Z |
| 82 | + hour = t.hour, |
| 83 | + minute = t.minute, |
| 84 | + second = t.second, |
| 85 | + } |
| 86 | + end |
| 87 | +local startdate = Ct(date * slash * date ^ -1) |
| 88 | + / function (t) |
| 89 | + local approx = t[1].approximate or t[2] and t[2].approximate |
| 90 | + return { |
| 91 | + approximate = approx, |
| 92 | + range = true, |
| 93 | + startdate = t[1], |
| 94 | + enddate = t[2], |
| 95 | + } |
| 96 | + end |
| 97 | +local enddateonly = slash |
| 98 | + * date |
| 99 | + / function (t) |
| 100 | + return { |
| 101 | + aproximate = t.approximate, |
| 102 | + range = true, |
| 103 | + enddate = t, |
| 104 | + } |
| 105 | + end |
| 106 | +local dateinterval = startdate + enddateonly |
| 107 | +local END = P(-1) |
| 108 | +local isodatetimspec = (dateinterval + date) * END |
| 109 | + |
| 110 | +--- Parse an ISO 8601 date/time string. |
| 111 | +-- For a single date, the fields are year, month, day, season, hour, minute, |
| 112 | +-- second, and approximate (true/false). |
| 113 | +-- For date ranges, the start and/or end dates are returned in a table, with |
| 114 | +-- the range field set to true for convenience. |
| 115 | +-- @tparam string dt The date/time string |
| 116 | +-- @treturn table A table with the parsed date/time, or nil if the string could not be parsed |
| 117 | +local function isodatetime (dt) |
| 118 | + return lpeg.match(isodatetimspec, dt) |
| 119 | +end |
| 120 | + |
| 121 | +return isodatetime |
0 commit comments