From ccff0b3ccfc8122cc3a16efd839175a1d5b85efc Mon Sep 17 00:00:00 2001 From: Martin Schoeler Date: Thu, 24 Jul 2025 20:57:34 -0300 Subject: [PATCH] feat: add timezones to timestamp rendering (#36529) --- packages/message-parser/src/grammar.pegjs | 10 ++- packages/message-parser/src/utils.ts | 40 ++++++++- .../message-parser/tests/timestamp.test.ts | 83 +++++++++++++++---- 3 files changed, 112 insertions(+), 21 deletions(-) diff --git a/packages/message-parser/src/grammar.pegjs b/packages/message-parser/src/grammar.pegjs index 0173a2e089e..713d989e42b 100644 --- a/packages/message-parser/src/grammar.pegjs +++ b/packages/message-parser/src/grammar.pegjs @@ -34,6 +34,7 @@ unorderedList, timestamp, timestampFromHours, + timestampFromIsoTime, } = require('./utils'); let skipBold = false; @@ -83,17 +84,18 @@ TimestampType = "t" / "T" / "d" / "D" / "f" / "F" / "R" Unixtime = d:Digit |10| { return d.join(''); } -TimestampHoursMinutesSeconds = hours:Digit |2| ":" minutes:Digit|2| ":" seconds:Digit |2| "Z"? { return timestampFromHours(hours.join(''), minutes.join(''), seconds.join('')); } +TimestampHoursMinutesSeconds = hours:Digit |2| ":" minutes:Digit|2| ":" seconds:Digit |2| tz:Timezone? { return timestampFromHours(hours.join(''), minutes.join(''), seconds.join(''), tz); } -TimestampHoursMinutes = hours:Digit |2| ":" minutes:Digit|2| "Z"? { return timestampFromHours(hours.join(''), minutes.join('')); } +TimestampHoursMinutes = hours:Digit |2| ":" minutes:Digit|2| tz:Timezone? { return timestampFromHours(hours.join(''), minutes.join(''),undefined, tz); } Timestamp = TimestampHoursMinutesSeconds / TimestampHoursMinutes +Timezone = offset:('+'/'-') tzHour: Digit |2| ':' tzMinute: Digit |2| { return `${offset}${tzHour.join('')}:${tzMinute.join('')}` } -ISO8601Date = year:Digit |4| "-" month:Digit |2| "-" day:Digit |2| "T" hours:Digit |2| ":" minutes:Digit|2| ":" seconds:Digit |2| "." milliseconds:Digit |3| "Z"? { return new Date(year.join('') + '-' + month.join('') + '-' + day.join('') + 'T' + hours.join('') + ':' + minutes.join('') + ':' + seconds.join('') + '.' + milliseconds.join('') + 'Z').getTime().toString(); } +ISO8601Date = year:Digit |4| "-" month:Digit |2| "-" day:Digit |2| "T" hours:Digit |2| ":" minutes:Digit|2| ":" seconds:Digit |2| "." milliseconds:Digit |3| tz:Timezone? { return timestampFromIsoTime({year: year.join(''), month: month.join(''), day: day.join(''), hours: hours.join(''), minutes: minutes.join(''), seconds: seconds.join(''), milliseconds: milliseconds.join(''), timezone: tz}) } -ISO8601DateWithoutMilliseconds = year:Digit |4| "-" month:Digit |2| "-" day:Digit |2| "T" hours:Digit |2| ":" minutes:Digit|2| ":" seconds:Digit |2| "Z"? { return new Date(year.join('') + '-' + month.join('') + '-' + day.join('') + 'T' + hours.join('') + ':' + minutes.join('') + ':' + seconds.join('') + 'Z').getTime().toString(); } +ISO8601DateWithoutMilliseconds = year:Digit |4| "-" month:Digit |2| "-" day:Digit |2| "T" hours:Digit |2| ":" minutes:Digit|2| ":" seconds:Digit |2| tz:Timezone? { return timestampFromIsoTime({year: year.join(''), month: month.join(''), day: day.join(''), hours: hours.join(''), minutes: minutes.join(''), seconds: seconds.join(''), timezone: tz}) } TimestampRules = "" { return timestamp(date, format); } / "" { return timestamp(date); } diff --git a/packages/message-parser/src/utils.ts b/packages/message-parser/src/utils.ts index 3bf9b8f689c..75ab4e9d6cc 100644 --- a/packages/message-parser/src/utils.ts +++ b/packages/message-parser/src/utils.ts @@ -255,14 +255,48 @@ export const timestampFromHours = ( hours: string, minutes = '00', seconds = '00', + timezone = '', ) => { const date = new Date(); const yearMonthDay = date.toISOString().split('T')[0]; - return new Date(`${yearMonthDay}T${hours}:${minutes}:${seconds}Z`) - .getTime() - .toString(); + const timestamp = + (new Date( + `${yearMonthDay}T${hours}:${minutes}:${seconds}${timezone}`, + ).getTime() / + 1000) | + 0; + + return timestamp.toString(); +}; + +export const timestampFromIsoTime = ({ + year, + month, + day, + hours, + minutes, + seconds, + milliseconds, + timezone, +}: { + year: string[]; + month: string[]; + day: string[]; + hours: string[]; + minutes: string[]; + seconds: string[]; + milliseconds?: string[]; + timezone?: string; +}) => { + const date = + (new Date( + `${year}-${month}-${day}T${hours}:${minutes}:${seconds}.${milliseconds || '000'}${timezone ? `${timezone}` : ''}`, + ).getTime() / + 1000) | + 0; + return date.toString(); }; export const extractFirstResult = ( diff --git a/packages/message-parser/tests/timestamp.test.ts b/packages/message-parser/tests/timestamp.test.ts index 1073125aacc..c1c9a20a8c9 100644 --- a/packages/message-parser/tests/timestamp.test.ts +++ b/packages/message-parser/tests/timestamp.test.ts @@ -35,34 +35,89 @@ test.each([ test.each([ [ - '', - [paragraph([timestamp('1753178400000', 'R')])], + '', + [ + paragraph([ + timestamp( + (Date.parse('2025-07-22T10:00:00.000+00:00') / 1000).toString(), + 'R', + ), + ]), + ], ], [ - '', - [paragraph([timestamp('1753178400000', 'R')])], + '', + [ + paragraph([ + timestamp( + (Date.parse('2025-07-22T10:00:00.000+00:00') / 1000).toString(), + 'R', + ), + ]), + ], ], [ - '', - [paragraph([timestamp('1753178400000', 'R')])], + '', + [ + paragraph([ + timestamp( + (Date.parse('2025-07-22T10:00:00.000+00:00') / 1000).toString(), + 'R', + ), + ]), + ], ], - ['', [paragraph([timestamp('1753178400000', 'R')])]], [ - '', - [paragraph([timestamp(timestampFromHours('10', '00', '00'), 'R')])], + '', + [ + paragraph([ + timestamp( + (Date.parse('2025-07-22T10:00:00+00:00') / 1000).toString(), + 'R', + ), + ]), + ], ], [ - '', - [paragraph([timestamp(timestampFromHours('10', '00', '00'), 'R')])], + '', + [ + paragraph([ + timestamp(timestampFromHours('10', '00', '00', '+00:00'), 'R'), + ]), + ], ], [ - '', - [paragraph([timestamp(timestampFromHours('10', '00', '00'), 't')])], + '', + [ + paragraph([ + timestamp(timestampFromHours('10', '00', '00', '+00:00'), 'R'), + ]), + ], + ], + [ + '', + [ + paragraph([ + timestamp(timestampFromHours('10', '00', '05', '+00:00'), 't'), + ]), + ], ], [ - '', + '', [paragraph([timestamp(timestampFromHours('10', '00', '00'), 't')])], ], + + [ + '', + [ + paragraph([ + timestamp( + ((Date.parse('2025-07-24T20:19:58.154+00:00') / 1000) | 0).toString(), + 'R', + ), + ]), + ], + ], ])('parses %p', (input, output) => { expect(parse(input)).toMatchObject(output); });