From 1644bc43dbbd93abc8863ffd7db0d686284c9907 Mon Sep 17 00:00:00 2001 From: Sylvain Date: Fri, 3 Feb 2023 17:25:24 +0100 Subject: [PATCH] (bug) invalid date display in negative timezones --- CHANGELOG.md | 2 + app/frontend/src/javascript/lib/format.ts | 27 +++++------- app/frontend/src/javascript/models/fablab.ts | 5 ++- .../src/javascript/typings/date-iso.d.ts | 4 +- app/views/application/index.html.erb | 1 + test/frontend/__setup__/globals.js | 1 + test/frontend/lib/format.test.ts | 43 +++++++++++++++++-- 7 files changed, 60 insertions(+), 23 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0866a053c..9ab19b8e6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,7 @@ # Changelog Fab-manager +- Fix a bug: invalid date display in negative timezones + ## v5.6.10 2023 February 02 - Optimized memory consumption in statistics fetcher service diff --git a/app/frontend/src/javascript/lib/format.ts b/app/frontend/src/javascript/lib/format.ts index 27af1cae1..eb2ba0b42 100644 --- a/app/frontend/src/javascript/lib/format.ts +++ b/app/frontend/src/javascript/lib/format.ts @@ -1,6 +1,6 @@ -import moment, { unitOfTime } from 'moment'; +import moment, { unitOfTime } from 'moment-timezone'; import { IFablab } from '../models/fablab'; -import { TDateISO, TDateISODate, TDateISOShortTime } from '../typings/date-iso'; +import { IANATimeZone, TDateISO, TDateISODate, TDateISOShortTime } from '../typings/date-iso'; declare let Fablab: IFablab; @@ -46,30 +46,23 @@ export default class FormatLib { } else { tempDate = moment(date).toDate(); } - return Intl.DateTimeFormat(Fablab.intl_locale).format(tempDate); + return Intl.DateTimeFormat(Fablab.intl_locale, { timeZone: Fablab.timezone }).format(tempDate); }; /** * Parse the provided datetime or date string (as ISO8601 format) and return the equivalent Date object */ - private static parseISOdate = (date: TDateISO|TDateISODate, res: Date = new Date()): Date => { - const isoDateMatch = (date as string)?.match(/^(\d\d\d\d)-(\d\d)-(\d\d)/); - res.setDate(parseInt(isoDateMatch[3], 10)); - res.setMonth(parseInt(isoDateMatch[2], 10) - 1); - res.setFullYear(parseInt(isoDateMatch[1], 10)); - - return res; + private static parseISOdate = (date: TDateISO|TDateISODate): Date => { + const isoDateMatch = (date as string)?.match(/^(\d\d\d\d-\d\d-\d\d)/); + return new Date(`${isoDateMatch[1]}T12:00:00${Fablab.timezone_offset}`); }; /** * Parse the provided datetime or time string (as ISO8601 format) and return the equivalent Date object */ - private static parseISOtime = (date: TDateISO|TDateISOShortTime, res: Date = new Date()): Date => { - const isoTimeMatch = (date as string)?.match(/(^|T)(\d\d):(\d\d)/); - res.setHours(parseInt(isoTimeMatch[2], 10)); - res.setMinutes(parseInt(isoTimeMatch[3], 10)); - - return res; + private static parseISOtime = (date: TDateISO|TDateISOShortTime): Date => { + const isoTimeMatch = (date as string)?.match(/(^|T)(\d\d:\d\d)/); + return new Date(`1970-01-01T${isoTimeMatch[2]}:00${Fablab.timezone_offset}`); }; /** @@ -89,7 +82,7 @@ export default class FormatLib { } else { tempDate = moment(date).toDate(); } - return Intl.DateTimeFormat(Fablab.intl_locale, { hour: 'numeric', minute: 'numeric' }).format(tempDate); + return Intl.DateTimeFormat(Fablab.intl_locale, { hour: 'numeric', minute: 'numeric', timeZone: Fablab.timezone }).format(tempDate); }; /** diff --git a/app/frontend/src/javascript/models/fablab.ts b/app/frontend/src/javascript/models/fablab.ts index 1c74d1b16..d4a07452a 100644 --- a/app/frontend/src/javascript/models/fablab.ts +++ b/app/frontend/src/javascript/models/fablab.ts @@ -1,3 +1,5 @@ +import { IANATimeZone, TTimezoneISO } from '../typings/date-iso'; + export interface IFablab { plansModule: boolean, spacesModule: boolean, @@ -13,7 +15,8 @@ export interface IFablab { fullcalendar_locale: string, intl_locale: string, intl_currency: string, - timezone: string, + timezone: IANATimeZone, + timezone_offset: TTimezoneISO, weekStartingDay: string, d3DateFormat: string, uibDateFormat: string, diff --git a/app/frontend/src/javascript/typings/date-iso.d.ts b/app/frontend/src/javascript/typings/date-iso.d.ts index e29eeb4ac..45ae915d4 100644 --- a/app/frontend/src/javascript/typings/date-iso.d.ts +++ b/app/frontend/src/javascript/typings/date-iso.d.ts @@ -26,7 +26,7 @@ type TDateISOTime = `${TDateISOShortTime}:${TSeconds}`|`${TDateISOShortTime}:${T /** * Represent a timezone like `+0100` */ -type TTimezoneISO = `+${THours}${TMinutes}`|`-${THours}${TMinutes}`|'Z' +type TTimezoneISO = `+${THours}${TMinutes}`|`-${THours}${TMinutes}`|`+${THours}:${TMinutes}`|`-${THours}:${TMinutes}`|'Z' /** * Represent a string like `2021-01-08T14:42:34.678Z` (format: ISO 8601). @@ -36,3 +36,5 @@ type TTimezoneISO = `+${THours}${TMinutes}`|`-${THours}${TMinutes}`|'Z' * "Expression produces a union type that is too complex to represent. ts(2590) */ export type TDateISO = `${TDateISODate}T${TDateISOTime}${TTimezoneISO}`; + +export type IANATimeZone = `${string}/${string}` | 'UTC'; diff --git a/app/views/application/index.html.erb b/app/views/application/index.html.erb index 12c5422fd..b15de5432 100644 --- a/app/views/application/index.html.erb +++ b/app/views/application/index.html.erb @@ -54,6 +54,7 @@ Fablab.intl_locale = "<%= Rails.application.secrets.intl_locale %>"; Fablab.intl_currency = "<%= Rails.application.secrets.intl_currency %>"; Fablab.timezone = "<%= Time.zone.tzinfo.name %>"; + Fablab.timezone_offset = "<%= Time.zone.formatted_offset %>"; Fablab.translations = { app: { shared: { diff --git a/test/frontend/__setup__/globals.js b/test/frontend/__setup__/globals.js index 68446f13a..2156efc21 100644 --- a/test/frontend/__setup__/globals.js +++ b/test/frontend/__setup__/globals.js @@ -24,6 +24,7 @@ global.Fablab.fullcalendar_locale = 'fr'; global.Fablab.intl_locale = 'fr-FR'; global.Fablab.intl_currency = 'EUR'; global.Fablab.timezone = 'Europe/Paris'; +global.Fablab.timezone_offset = '+01:00'; global.Fablab.translations = { app: { shared: { diff --git a/test/frontend/lib/format.test.ts b/test/frontend/lib/format.test.ts index 289e73d01..f22be214d 100644 --- a/test/frontend/lib/format.test.ts +++ b/test/frontend/lib/format.test.ts @@ -3,38 +3,73 @@ import { IFablab } from 'models/fablab'; declare const Fablab: IFablab; describe('FormatLib', () => { - test('format a date', () => { + test('format a Date object in french format', () => { Fablab.intl_locale = 'fr-FR'; - const str = FormatLib.date(new Date('2023-01-12T12:00:00+0100')); + Fablab.timezone = 'Europe/Paris'; + Fablab.timezone_offset = '+01:00'; + const str = FormatLib.date(new Date('2023-01-12T23:59:00+0100')); expect(str).toBe('12/01/2023'); }); + test('format a Date object in canadian format', () => { + Fablab.intl_locale = 'fr-CA'; + Fablab.timezone = 'America/Toronto'; + Fablab.timezone_offset = '-05:00'; + const str = FormatLib.date(new Date('2023-01-12T23:59:00-0500')); + expect(str).toBe('2023-01-12'); + }); test('format an iso8601 short date in french format', () => { Fablab.intl_locale = 'fr-FR'; + Fablab.timezone = 'Europe/Paris'; + Fablab.timezone_offset = '+01:00'; const str = FormatLib.date('2023-01-12'); expect(str).toBe('12/01/2023'); }); test('format an iso8601 short date in canadian format', () => { Fablab.intl_locale = 'fr-CA'; + Fablab.timezone = 'America/Toronto'; + Fablab.timezone_offset = '-05:00'; const str = FormatLib.date('2023-02-27'); expect(str).toBe('2023-02-27'); }); - test('format an iso8601 date', () => { + test('format an iso8601 date in french format', () => { + Fablab.intl_locale = 'fr-FR'; + Fablab.timezone = 'Europe/Paris'; + Fablab.timezone_offset = '+01:00'; + const str = FormatLib.date('2023-01-12T23:59:14+0100'); + expect(str).toBe('12/01/2023'); + }); + test('format an iso8601 date in canadian format', () => { Fablab.intl_locale = 'fr-CA'; + Fablab.timezone = 'America/Toronto'; + Fablab.timezone_offset = '-05:00'; const str = FormatLib.date('2023-01-12T23:59:14-0500'); expect(str).toBe('2023-01-12'); }); - test('format a time', () => { + test('format a time from a Date object', () => { Fablab.intl_locale = 'fr-FR'; + Fablab.timezone = 'Europe/Paris'; + Fablab.timezone_offset = '+01:00'; const str = FormatLib.time(new Date('2023-01-12T23:59:14+0100')); expect(str).toBe('23:59'); }); + test('format a time from a Date object in canadian format', () => { + Fablab.intl_locale = 'fr-CA'; + Fablab.timezone = 'America/Toronto'; + Fablab.timezone_offset = '-05:00'; + const str = FormatLib.time(new Date('2023-01-12T23:59:14-0500')); + expect(str).toBe('23 h 59'); + }); test('format an iso8601 short time', () => { Fablab.intl_locale = 'fr-FR'; + Fablab.timezone = 'Europe/Paris'; + Fablab.timezone_offset = '+01:00'; const str = FormatLib.time('23:59'); expect(str).toBe('23:59'); }); test('format an iso8601 time', () => { Fablab.intl_locale = 'fr-CA'; + Fablab.timezone = 'America/Toronto'; + Fablab.timezone_offset = '-05:00'; const str = FormatLib.time('2023-01-12T23:59:14-0500'); expect(str).toBe('23 h 59'); });