diff --git a/src/coalesce-vue/src/model.ts b/src/coalesce-vue/src/model.ts index 217ee3e87..731ef8a54 100644 --- a/src/coalesce-vue/src/model.ts +++ b/src/coalesce-vue/src/model.ts @@ -201,7 +201,7 @@ export function parseValue( if (value instanceof Date) { date = value; } else if (type === "string") { - date = parseJSONDate(value); + date = parseJSONDate(value, meta.dateKind); } // isNaN is what date-fn's `isValid` calls internally, diff --git a/src/coalesce-vue/src/util.ts b/src/coalesce-vue/src/util.ts index 050a7fc3e..50d816097 100644 --- a/src/coalesce-vue/src/util.ts +++ b/src/coalesce-vue/src/util.ts @@ -44,8 +44,10 @@ export function isNullOrWhitespace(value: string | null | undefined) { } const iso8601DateRegex = - /^(\d{4})-(\d{2})-(\d{2})[T ](\d{2}):(\d{2}):(\d{2})(?:\.(\d{0,7}))?(?:Z|(.)(\d{2}):?(\d{2})?)?/; -export function parseJSONDate(argument: string) { + /^(\d{4})-(\d{2})-(\d{2})(?:[T ](\d{2}):(\d{2}):(\d{2})(?:\.(\d{0,7}))?(?:Z|(.)(\d{2}):?(\d{2})?)?)?/; +const iso8601TimeRegex = + /^(\d{2}):(\d{2}):(\d{2})(?:\.(\d{0,7}))?(?:Z|(.)(\d{2}):?(\d{2})?)?$/; +export function parseJSONDate(argument: string, kind: DateKind = "datetime") { // DO NOT USE `new Date()` here. // Safari incorrectly interprets times without a timezone offset // (i.e. DateTime objects in c#) as UTC instead of local time. @@ -61,10 +63,36 @@ export function parseJSONDate(argument: string) { // This method is only slightly slower than parseJSON, // but is still 3-4x faster than parseISO while also having much, much less code. + if (kind == "time") { + // Time-only formats might come from the server as truly only a time (C# TimeOnly type). + // "ISO 8601: Time without UTC offset information" https://github.com/dotnet/runtime/issues/53539 + + var timeParts = argument.match(iso8601TimeRegex) || []; + if (timeParts.length >= 3) { + // If ES Temporal was standardize we could maybe switch Coalesce to use it, + // but for now we just settle for using a Date and ignoring the time part. + + // NOTE: We set the date to Jan 1 in to avoid Daylight Savings switch days, + // which if such a date were to be the date component of the `Date`, would fail + // to represent the time correctly. + return new Date( + new Date().getFullYear(), + 0, + 1, + +timeParts[1], // h + +timeParts[2], // m + +timeParts[3], // s + +((timeParts[4] || "0") + "00").substring(0, 3) // ms (maybe never used?) + ); + } + } + var parts = argument.match(iso8601DateRegex) || []; - const part9 = parts[9]; + const part9 = parts[9]; // TZ offset + if (part9 !== undefined) { + // Date+Time, with offset specifier return new Date( Date.UTC( +parts[1], @@ -76,7 +104,8 @@ export function parseJSONDate(argument: string) { +((parts[7] || "0") + "00").substring(0, 3) ) ); - } else { + } else if (parts[4] !== undefined) { + // Date+Time, without offset specifier return new Date( +parts[1], +parts[2] - 1, @@ -86,6 +115,9 @@ export function parseJSONDate(argument: string) { +parts[6], +((parts[7] || "0") + "00").substring(0, 3) ); + } else { + // Date only, without a time portion: + return new Date(+parts[1], +parts[2] - 1, +parts[3]); } } diff --git a/src/coalesce-vue/test/model.toModel.spec.ts b/src/coalesce-vue/test/model.toModel.spec.ts index be9a56f50..ab9a0a8fd 100644 --- a/src/coalesce-vue/test/model.toModel.spec.ts +++ b/src/coalesce-vue/test/model.toModel.spec.ts @@ -81,6 +81,23 @@ const dtoToModelMappings = [ dto: "2020-06-10T14:00:00", model: new Date(2020, 5, 10, 14, 0, 0, 0), }, + { + meta: { ...studentProps.birthDate, dateKind: "date" }, + dto: "2020-06-10", + model: new Date(2020, 5, 10, 0, 0, 0, 0), + }, + { + meta: { ...studentProps.birthDate, dateKind: "time" }, + dto: "12:34:56", + // Time-only parses as Jan 1 of current year to avoid un-representable times on DST changeover days. + model: new Date(new Date().getFullYear(), 0, 1, 12, 34, 56, 0), + }, + { + // Time-only, represented in a datetime format. + meta: { ...studentProps.birthDate, dateKind: "time" }, + dto: "2020-06-10T12:34:56", + model: new Date(2020, 5, 10, 12, 34, 56, 0), + }, ...unparsable( studentProps.birthDate, "abc",