diff --git a/src/tests/totext.i18n.extra.test.ts b/src/tests/totext.i18n.extra.test.ts index 6414515..3357e3b 100644 --- a/src/tests/totext.i18n.extra.test.ts +++ b/src/tests/totext.i18n.extra.test.ts @@ -9,10 +9,10 @@ function zdt(y: number, m: number, d: number, h: number, tz = 'UTC') { const expected = { de: [ 'jede/n/s Tag', - 'jede/n/s Tag um 10 AM, 12 PM und 5 PM UTC', - 'jede/n/s Woche am Sonntag um 10 AM, 12 PM und 5 PM UTC', - 'jede/n/s Tag um 5:30 PM CST', - 'jede/n/s Tag um 6:15:45 AM UTC', + 'jede/n/s Tag um 10 Uhr, 12 Uhr und 17 Uhr UTC', + 'jede/n/s Woche am Sonntag um 10 Uhr, 12 Uhr und 17 Uhr UTC', + 'jede/n/s Tag um 17:30 GMT-6', + 'jede/n/s Tag um 6:15:45 UTC', 'jede/n/s Werktag', 'jede/n/s 2 Minuten', 'jede/n/s Woche am Sonntag bis 10. November 2012', @@ -23,10 +23,10 @@ const expected = { ], es: [ 'cada día', - 'cada día a las 10 AM, 12 PM y 5 PM UTC', - 'cada semana en domingo a las 10 AM, 12 PM y 5 PM UTC', - 'cada día a las 5:30 PM CST', - 'cada día a las 6:15:45 AM UTC', + 'cada día a las 10, 12 y 17 UTC', + 'cada semana en domingo a las 10, 12 y 17 UTC', + 'cada día a las 17:30 GMT-6', + 'cada día a las 6:15:45 UTC', 'cada día de la semana', 'cada 2 minutos', 'cada semana en domingo hasta 10 de noviembre de 2012', @@ -39,7 +39,7 @@ const expected = { 'हर दिन', 'हर दिन पर 10 AM, 12 PM और 5 PM UTC', 'हर सप्ताह को रविवार पर 10 AM, 12 PM और 5 PM UTC', - 'हर दिन पर 5:30 PM CST', + 'हर दिन पर 5:30 pm GMT-6', 'हर दिन पर 6:15:45 AM UTC', 'हर सप्ताह का दिन', 'हर 2 मिनट', @@ -51,10 +51,10 @@ const expected = { ], yue: [ '每 日', - '每 日 在 10 AM, 12 PM 和 5 PM UTC', - '每 週 在 星期日 在 10 AM, 12 PM 和 5 PM UTC', - '每 日 在 5:30 PM CST', - '每 日 在 6:15:45 AM UTC', + '每 日 在 上午10時, 下午12時 和 下午5時 utc', + '每 週 在 星期日 在 上午10時, 下午12時 和 下午5時 utc', + '每 日 在 下午5:30 GMT-6', + '每 日 在 上午6:15:45 utc', '每 平日', '每 2 分鐘', '每 週 在 星期日 直到 2012年11月10日', @@ -65,10 +65,10 @@ const expected = { ], ar: [ 'كل يوم', - 'كل يوم عند 10 AM, 12 PM و 5 PM UTC', - 'كل أسبوع في الأحد عند 10 AM, 12 PM و 5 PM UTC', - 'كل يوم عند 5:30 PM CST', - 'كل يوم عند 6:15:45 AM UTC', + 'كل يوم عند 10 ص, 12 م و 5 م utc', + 'كل أسبوع في الأحد عند 10 ص, 12 م و 5 م utc', + 'كل يوم عند 5:30 م غرينتش-6', + 'كل يوم عند 6:15:45 ص utc', 'كل يوم من أيام الأسبوع', 'كل 2 دقائق', 'كل أسبوع في الأحد حتى 10 نوفمبر 2012', @@ -79,10 +79,10 @@ const expected = { ], he: [ 'כל יום', - 'כל יום בשעה 10 AM, 12 PM ו 5 PM UTC', - 'כל שבוע ב יום ראשון בשעה 10 AM, 12 PM ו 5 PM UTC', - 'כל יום בשעה 5:30 PM CST', - 'כל יום בשעה 6:15:45 AM UTC', + 'כל יום בשעה 10, 12 ו 17 utc', + 'כל שבוע ב יום ראשון בשעה 10, 12 ו 17 utc', + 'כל יום בשעה 17:30 GMT-6‎', + 'כל יום בשעה 6:15:45 utc', 'כל יום חול', 'כל 2 דקות', 'כל שבוע ב יום ראשון עד 10 בנובמבר 2012', @@ -91,12 +91,12 @@ const expected = { 'כל חודש ב יום שני ו יום רביעי ב שני פעם', 'כל חודש במשך 1 פעם עם 1 תאריך נוסף למעט 2 תאריכים', ], - zh: [ + 'zh-Hans': [ '每 日', - '每 日 在 10 AM, 12 PM 和 5 PM UTC', - '每 周 在 星期日 在 10 AM, 12 PM 和 5 PM UTC', - '每 日 在 5:30 PM CST', - '每 日 在 6:15:45 AM UTC', + '每 日 在 10时, 12时 和 17时 utc', + '每 周 在 星期日 在 10时, 12时 和 17时 UTC', + '每 日 在 17:30 GMT-6', + '每 日 在 6:15:45 utc', '每 工作日', '每 2 分钟', '每 周 在 星期日 直到 2012年11月10日', @@ -107,10 +107,10 @@ const expected = { ], fr: [ 'chaque jour', - 'chaque jour à 10 AM, 12 PM et 5 PM UTC', - 'chaque semaine le dimanche à 10 AM, 12 PM et 5 PM UTC', - 'chaque jour à 5:30 PM CST', - 'chaque jour à 6:15:45 AM UTC', + 'chaque jour à 10 h, 12 h et 17 h UTC', + 'chaque semaine le dimanche à 10 h, 12 h et 17 h UTC', + 'chaque jour à 17:30 UTC−6', + 'chaque jour à 6:15:45 UTC', 'chaque jour de semaine', 'chaque 2 minutes', "chaque semaine le dimanche jusqu'au 10 novembre 2012", diff --git a/src/tests/totext.i18n.test.ts b/src/tests/totext.i18n.test.ts index aa3efa8..54ed889 100644 --- a/src/tests/totext.i18n.test.ts +++ b/src/tests/totext.i18n.test.ts @@ -40,8 +40,8 @@ const cases = [ const expected = { de: [ 'jede/n/s Tag', - 'jede/n/s Tag um 10 AM, 12 PM und 5 PM EDT', - 'jede/n/s Woche am Sonntag um 10 AM, 12 PM und 5 PM EDT', + 'jede/n/s Tag um 10 Uhr, 12 Uhr und 17 Uhr GMT-4', + 'jede/n/s Woche am Sonntag um 10 Uhr, 12 Uhr und 17 Uhr GMT-4', 'jede/n/s Woche am Sonntag', 'jede/n/s Stunde', 'jede/n/s 4 Stunden', @@ -54,14 +54,14 @@ const expected = { 'jede/n/s Jahr', 'jede/n/s Jahr am 1. Freitag', 'jede/n/s Jahr am 13. Freitag', - 'jede/n/s Tag um 5:30 PM EDT', - 'jede/n/s Woche am Montag und Mittwoch um 10 AM und 4 PM EDT', - 'jede/n/s Woche am Dienstag und Donnerstag um 9:30 AM und 3:30 PM EDT', + 'jede/n/s Tag um 17:30 GMT-4', + 'jede/n/s Woche am Montag und Mittwoch um 10 Uhr und 16 Uhr GMT-4', + 'jede/n/s Woche am Dienstag und Donnerstag um 9:30 und 15:30 GMT-4', ], es: [ 'cada día', - 'cada día a las 10 AM, 12 PM y 5 PM EDT', - 'cada semana en domingo a las 10 AM, 12 PM y 5 PM EDT', + 'cada día a las 10, 12 y 17 GMT-4', + 'cada semana en domingo a las 10, 12 y 17 GMT-4', 'cada semana en domingo', 'cada hora', 'cada 4 horas', @@ -74,14 +74,14 @@ const expected = { 'cada año', 'cada año en 1º viernes', 'cada año en 13º viernes', - 'cada día a las 5:30 PM EDT', - 'cada semana en lunes y miércoles a las 10 AM y 4 PM EDT', - 'cada semana en martes y jueves a las 9:30 AM y 3:30 PM EDT', + 'cada día a las 17:30 GMT-4', + 'cada semana en lunes y miércoles a las 10 y 16 GMT-4', + 'cada semana en martes y jueves a las 9:30 y 15:30 GMT-4', ], hi: [ 'हर दिन', - 'हर दिन पर 10 AM, 12 PM और 5 PM EDT', - 'हर सप्ताह को रविवार पर 10 AM, 12 PM और 5 PM EDT', + 'हर दिन पर 10 AM, 12 PM और 5 PM GMT-4', + 'हर सप्ताह को रविवार पर 10 AM, 12 PM और 5 PM GMT-4', 'हर सप्ताह को रविवार', 'हर घंटा', 'हर 4 घंटे', @@ -94,14 +94,14 @@ const expected = { 'हर साल', 'हर साल को 1वां शुक्रवार', 'हर साल को 13वां शुक्रवार', - 'हर दिन पर 5:30 PM EDT', - 'हर सप्ताह को सोमवार और बुधवार पर 10 AM और 4 PM EDT', - 'हर सप्ताह को मंगलवार और गुरुवार पर 9:30 AM और 3:30 PM EDT', + 'हर दिन पर 5:30 pm GMT-4', + 'हर सप्ताह को सोमवार और बुधवार पर 10 am और 4 pm GMT-4', + 'हर सप्ताह को मंगलवार और गुरुवार पर 9:30 am और 3:30 pm GMT-4', ], yue: [ '每 日', - '每 日 在 10 AM, 12 PM 和 5 PM EDT', - '每 週 在 星期日 在 10 AM, 12 PM 和 5 PM EDT', + '每 日 在 上午10時, 下午12時 和 下午5時 GMT-4', + '每 週 在 星期日 在 上午10時, 下午12時 和 下午5時 GMT-4', '每 週 在 星期日', '每 小時', '每 4 小時', @@ -114,14 +114,14 @@ const expected = { '每 年', '每 年 在 第1 星期五', '每 年 在 第13 星期五', - '每 日 在 5:30 PM EDT', - '每 週 在 星期一 和 星期三 在 10 AM 和 4 PM EDT', - '每 週 在 星期二 和 星期四 在 9:30 AM 和 3:30 PM EDT', + '每 日 在 下午5:30 GMT-4', + '每 週 在 星期一 和 星期三 在 上午10時 和 下午4時 GMT-4', + '每 週 在 星期二 和 星期四 在 上午9:30 和 下午3:30 GMT-4', ], ar: [ 'كل يوم', - 'كل يوم عند 10 AM, 12 PM و 5 PM EDT', - 'كل أسبوع في الأحد عند 10 AM, 12 PM و 5 PM EDT', + 'كل يوم عند 10 ص, 12 م و 5 م غرينتش-4', + 'كل أسبوع في الأحد عند 10 ص, 12 م و 5 م غرينتش-4', 'كل أسبوع في الأحد', 'كل ساعة', 'كل 4 ساعات', @@ -134,14 +134,14 @@ const expected = { 'كل سنة', 'كل سنة في الأول الجمعة', 'كل سنة في الثالث عشر الجمعة', - 'كل يوم عند 5:30 PM EDT', - 'كل أسبوع في الاثنين و الأربعاء عند 10 AM و 4 PM EDT', - 'كل أسبوع في الثلاثاء و الخميس عند 9:30 AM و 3:30 PM EDT', + 'كل يوم عند 5:30 م غرينتش-4', + 'كل أسبوع في الاثنين و الأربعاء عند 10 ص و 4 م غرينتش-4', + 'كل أسبوع في الثلاثاء و الخميس عند 9:30 ص و 3:30 م غرينتش-4', ], he: [ 'כל יום', - 'כל יום בשעה 10 AM, 12 PM ו 5 PM EDT', - 'כל שבוע ב יום ראשון בשעה 10 AM, 12 PM ו 5 PM EDT', + 'כל יום בשעה 10, 12 ו 17 GMT-4‎', + 'כל שבוע ב יום ראשון בשעה 10, 12 ו 17 GMT-4‎', 'כל שבוע ב יום ראשון', 'כל שעה', 'כל 4 שעות', @@ -154,14 +154,14 @@ const expected = { 'כל שנה', 'כל שנה ב ראשון יום שישי', 'כל שנה ב שלושה עשר יום שישי', - 'כל יום בשעה 5:30 PM EDT', - 'כל שבוע ב יום שני ו יום רביעי בשעה 10 AM ו 4 PM EDT', - 'כל שבוע ב יום שלישי ו יום חמישי בשעה 9:30 AM ו 3:30 PM EDT', + 'כל יום בשעה 17:30 GMT-4‎', + 'כל שבוע ב יום שני ו יום רביעי בשעה 10 ו 16 GMT-4‎', + 'כל שבוע ב יום שלישי ו יום חמישי בשעה 9:30 ו 15:30 GMT-4‎', ], - zh: [ + 'zh-Hans': [ '每 日', - '每 日 在 10 AM, 12 PM 和 5 PM EDT', - '每 周 在 星期日 在 10 AM, 12 PM 和 5 PM EDT', + '每 日 在 10时, 12时 和 17时 GMT-4', + '每 周 在 星期日 在 10时, 12时 和 17时 GMT-4', '每 周 在 星期日', '每 小时', '每 4 小时', @@ -174,14 +174,14 @@ const expected = { '每 年', '每 年 在 第1 星期五', '每 年 在 第13 星期五', - '每 日 在 5:30 PM EDT', - '每 周 在 星期一 和 星期三 在 10 AM 和 4 PM EDT', - '每 周 在 星期二 和 星期四 在 9:30 AM 和 3:30 PM EDT', + '每 日 在 17:30 GMT-4', + '每 周 在 星期一 和 星期三 在 10时 和 16时 GMT-4', + '每 周 在 星期二 和 星期四 在 9:30 和 15:30 GMT-4', ], fr: [ 'chaque jour', - 'chaque jour à 10 AM, 12 PM et 5 PM EDT', - 'chaque semaine le dimanche à 10 AM, 12 PM et 5 PM EDT', + 'chaque jour à 10 h, 12 h et 17 h UTC−4', + 'chaque semaine le dimanche à 10 h, 12 h et 17 h UTC−4', 'chaque semaine le dimanche', 'chaque heure', 'chaque 4 heures', @@ -194,9 +194,9 @@ const expected = { 'chaque année', 'chaque année le 1er vendredi', 'chaque année le 13e vendredi', - 'chaque jour à 5:30 PM EDT', - 'chaque semaine le lundi et mercredi à 10 AM et 4 PM EDT', - 'chaque semaine le mardi et jeudi à 9:30 AM et 3:30 PM EDT', + 'chaque jour à 17:30 UTC−4', + 'chaque semaine le lundi et mercredi à 10 h et 16 h UTC−4', + 'chaque semaine le mardi et jeudi à 9:30 et 15:30 UTC−4', ], }; diff --git a/src/tests/totext.test.ts b/src/tests/totext.test.ts index 716ae66..9f17ceb 100644 --- a/src/tests/totext.test.ts +++ b/src/tests/totext.test.ts @@ -170,4 +170,15 @@ describe('RRuleTemporal.toText', () => { }); expect(toText(rule)).toBe('every week on Thursday at 10:30 AM UTC'); }); + + test('should display the requested wall-clock time for DST spring-forward (nonexistent local time)', () => { + const rule = new RRuleTemporal({ + dtstart: Temporal.ZonedDateTime.from('2025-03-09T02:30:00[America/New_York]'), + freq: 'YEARLY', + byHour: [2], + byMinute: [30], + }); + const text = toText(rule, 'en'); + expect(text).toContain('2:30'); // Should not display 3:30 + }); }); diff --git a/src/totext.ts b/src/totext.ts index 453291d..b960df1 100644 --- a/src/totext.ts +++ b/src/totext.ts @@ -638,30 +638,46 @@ function formatByDayToken(tok: string | number, locale: LocaleData): string { return `${ordinal(ord, locale)} ${name}`; } -function formatTime(hour: number, minute = 0, second = 0): string { - const hr12 = ((hour + 11) % 12) + 1; - const ampm = hour < 12 ? 'AM' : 'PM'; - const mm = String(minute).padStart(2, '0'); - const ss = String(second).padStart(2, '0'); - if (second) { - return `${hr12}:${mm}:${ss} ${ampm}`; - } - if (minute) { - return `${hr12}:${mm} ${ampm}`; +function formatTime(zdt: Temporal.ZonedDateTime, locale: string, hour: number, minute: number, second: number): string { + const options: Intl.DateTimeFormatOptions = { + hour: 'numeric', + timeZone: zdt.timeZoneId, + }; + + if (second) options.second = '2-digit'; + if (second || minute) options.minute = '2-digit'; + + const ruleTime = Temporal.PlainTime.from({hour, minute, second}); + + let result; + try { + result = ruleTime.toLocaleString(locale, options); + } catch { + result = ruleTime.toLocaleString('en', options); } - return `${hr12} ${ampm}`; + + return result; } function weekdayTokenFromZdt(zdt: Temporal.ZonedDateTime): string { return allowedWeekdays[zdt.dayOfWeek - 1]!; } -function tzAbbreviation(zdt: Temporal.ZonedDateTime): string { - const parts = new Intl.DateTimeFormat('en-US', { +function tzAbbreviation(zdt: Temporal.ZonedDateTime, locale: string): string { + const options: Intl.DateTimeFormatOptions = { timeZone: zdt.timeZoneId, timeZoneName: 'short', hour: 'numeric', - }).formatToParts(new Date(zdt.epochMilliseconds)); + }; + + let formatter + try { + formatter = new Intl.DateTimeFormat(locale, options); + } catch { + formatter = Intl.DateTimeFormat('en', options); + } + + const parts = formatter.formatToParts(new Date(zdt.epochMilliseconds)); const tzPart = parts.find((p) => p.type === 'timeZoneName'); return tzPart?.value || zdt.timeZoneId; } @@ -794,9 +810,11 @@ export function toText(input: RRuleTemporal | string, locale?: string, options: if (textByHour) { const minutes = textByMinute ?? [0]; const seconds = textBySecond ?? [0]; - const times = textByHour.flatMap((h) => minutes.flatMap((m) => seconds.map((s) => formatTime(h, m, s)))); + const times = textByHour.flatMap((h) => + minutes.flatMap((m) => seconds.map((s) => formatTime(opts.dtstart, dateLocale, h, m, s))), + ); parts.push(data.words.at, list(times, undefined, data.words.and)); - parts.push(tzAbbreviation(opts.dtstart)); + parts.push(tzAbbreviation(opts.dtstart, dateLocale)); } if (!textByHour && textByMinute) {