1 import { listTimeZones } from '@protontech/timezone-support';
3 import type { SimpleMap } from '@proton/shared/lib/interfaces';
6 convertUTCDateTimeToZone,
7 convertZonedDateTimeToUTC,
8 getAbbreviatedTimezoneName,
9 getReadableCityTimezone,
13 } from '../../lib/date/timezone';
14 import { MANUAL_TIMEZONE_LINKS, unsupportedTimezoneLinks } from '../../lib/date/timezoneDatabase';
16 describe('convert utc', () => {
17 const obj = (year: number, month: number, day: number, hours = 0, minutes = 0, seconds = 0) => ({
26 it('should convert a zoned time (Australia/Sydney) to utc', () => {
27 expect(convertZonedDateTimeToUTC(obj(2019, 1, 1), 'Australia/Sydney')).toEqual(obj(2018, 12, 31, 13));
30 it('should convert a zoned time (Europe/Zurich summer) to utc', () => {
31 expect(convertZonedDateTimeToUTC(obj(2019, 6, 15, 1), 'Europe/Zurich')).toEqual(obj(2019, 6, 14, 23));
34 it('should convert a zoned time (Europe/Zurich winter) to utc', () => {
35 expect(convertZonedDateTimeToUTC(obj(2019, 12, 15, 1), 'Europe/Zurich')).toEqual(obj(2019, 12, 15, 0));
38 it('should convert a zoned time to utc with winter-to-summer (Europe/Zurich 2017) dst shift', () => {
39 expect(convertZonedDateTimeToUTC(obj(2017, 3, 26, 1), 'Europe/Zurich')).toEqual(obj(2017, 3, 26, 0));
40 expect(convertZonedDateTimeToUTC(obj(2017, 3, 26, 2), 'Europe/Zurich')).toEqual(obj(2017, 3, 26, 1));
41 expect(convertZonedDateTimeToUTC(obj(2017, 3, 26, 3), 'Europe/Zurich')).toEqual(obj(2017, 3, 26, 1));
42 expect(convertZonedDateTimeToUTC(obj(2017, 3, 26, 4), 'Europe/Zurich')).toEqual(obj(2017, 3, 26, 2));
45 it('should convert a zoned time to utc with summer-to-winter (Europe/Zurich 2019) dst shift', () => {
46 expect(convertZonedDateTimeToUTC(obj(2019, 10, 27, 1), 'Europe/Zurich')).toEqual(obj(2019, 10, 26, 23));
47 expect(convertZonedDateTimeToUTC(obj(2019, 10, 27, 2), 'Europe/Zurich')).toEqual(obj(2019, 10, 27, 1));
48 expect(convertZonedDateTimeToUTC(obj(2019, 10, 27, 3), 'Europe/Zurich')).toEqual(obj(2019, 10, 27, 2));
49 expect(convertZonedDateTimeToUTC(obj(2019, 10, 27, 4), 'Europe/Zurich')).toEqual(obj(2019, 10, 27, 3));
52 it('should convert from utc time to a timezone (Australia/Sydney)', () => {
53 expect(convertUTCDateTimeToZone(obj(2018, 12, 31, 13), 'Australia/Sydney')).toEqual(obj(2019, 1, 1));
56 it('should convert back from a utc to a timezone (Europe/Zurich summer)', () => {
57 expect(convertUTCDateTimeToZone(obj(2019, 6, 14, 23), 'Europe/Zurich')).toEqual(obj(2019, 6, 15, 1));
60 it('should convert back from a utc time to a timezone (Europe/Zurich winter)', () => {
61 expect(convertUTCDateTimeToZone(obj(2019, 12, 15, 0), 'Europe/Zurich')).toEqual(obj(2019, 12, 15, 1));
64 it('should convert back from a utc to a timezone (Europe/Zurich 2017) with summer-to-winter dst shifts', () => {
65 expect(convertUTCDateTimeToZone(obj(2017, 3, 26, 0), 'Europe/Zurich')).toEqual(obj(2017, 3, 26, 1));
66 expect(convertUTCDateTimeToZone(obj(2017, 3, 26, 1), 'Europe/Zurich')).toEqual(obj(2017, 3, 26, 3));
67 expect(convertUTCDateTimeToZone(obj(2017, 3, 26, 2), 'Europe/Zurich')).toEqual(obj(2017, 3, 26, 4));
70 it('should convert back from a utc to a timezone (Europe/Zurich 2017) with winter-to-summer dst shifts', () => {
71 expect(convertUTCDateTimeToZone(obj(2019, 10, 27, 0), 'Europe/Zurich')).toEqual(obj(2019, 10, 27, 2));
72 expect(convertUTCDateTimeToZone(obj(2019, 10, 27, 1), 'Europe/Zurich')).toEqual(obj(2019, 10, 27, 2));
73 expect(convertUTCDateTimeToZone(obj(2019, 10, 27, 2), 'Europe/Zurich')).toEqual(obj(2019, 10, 27, 3));
77 describe('getSupportedTimezone', () => {
78 it('should remove extra slashes', () => {
79 expect(getSupportedTimezone('/Europe/London/')).toEqual('Europe/London');
80 expect(getSupportedTimezone('/Africa/Freetown/')).toEqual('Africa/Abidjan');
83 it('should support the bluejeans format', () => {
84 expect(getSupportedTimezone('/tzurl.org/2020b/America/Chicago')).toEqual('America/Chicago');
85 expect(getSupportedTimezone('/tzurl.org/2020b/Asia/Istanbul')).toEqual('Europe/Istanbul');
88 it('should support the mozilla format', () => {
89 expect(getSupportedTimezone('/mozilla.org/20050126_1/America/Argentina/Ushuaia')).toEqual(
90 'America/Argentina/Ushuaia'
92 expect(getSupportedTimezone('/mozilla.org/20050126_1/America/Argentina/ComodRivadavia')).toEqual(
93 'America/Argentina/Catamarca'
97 it('should transform globally defined alias timezones according to the longest match', () => {
98 // GB (Europe/London), Eire (Europe/Dublin) and GB-Eire (Europe/London) are all alias timezones.
99 // getSupportedTimezone should transform according to the longest match, GB-Eire -> Europe/London
100 expect(getSupportedTimezone('/mozilla.org/20050126_1/GB-Eire')).toEqual('Europe/London');
101 expect(getSupportedTimezone('/tzurl.org/2021a/Eire')).toEqual('Europe/Dublin');
102 expect(getSupportedTimezone('/GB/')).toEqual('Europe/London');
105 it('should be robust for capturing globally defined timezones (for which no specification exists)', () => {
106 expect(getSupportedTimezone('/IANA-db:Asia/Seoul--custom.format')).toEqual('Asia/Seoul');
109 it('should filter non-supported canonical timezones', () => {
110 const canonical = listTimeZones();
111 const results = canonical.map((tzid) => getSupportedTimezone(tzid));
112 const expected = canonical.map((tzid) => unsupportedTimezoneLinks[tzid] || tzid);
113 expect(results).toEqual(expected);
116 it('should return the supported canonical timezone for alias timezones', () => {
121 'America/Indianapolis',
130 'America/Puerto_Rico',
137 const results = alias.map((tzid) => getSupportedTimezone(tzid));
138 expect(results).toEqual(canonical);
141 it('should convert manual timezones', () => {
142 const manual = Object.keys(MANUAL_TIMEZONE_LINKS);
143 const results = manual.map((tzid) => getSupportedTimezone(tzid));
144 const expected = Object.values(MANUAL_TIMEZONE_LINKS);
145 expect(results).toEqual(expected);
148 it('should convert time zones we do not support yet', () => {
149 const expectedMap: SimpleMap<string> = {
150 'Pacific/Kanton': 'Pacific/Fakaofo',
151 'Europe/Kyiv': 'Europe/Kiev',
152 'America/Nuuk': 'Atlantic/Stanley',
153 'America/Ciudad_Juarez': 'America/Ojinaga',
156 Object.keys(expectedMap).forEach((tzid) => {
157 expect(getSupportedTimezone(tzid)).toBe(expectedMap[tzid]);
161 it('should convert deprecated time zones', () => {
162 const expectedMap: SimpleMap<string> = {
163 'Australia/Currie': 'Australia/Hobart',
166 Object.keys(expectedMap).forEach((tzid) => {
167 expect(getSupportedTimezone(tzid)).toBe(expectedMap[tzid]);
171 it('should be robust', () => {
172 const tzids = ['Chamorro (UTC+10)', '(GMT-01:00) Azores', 'Mountain Time (U.S. & Canada)'];
173 const expected = ['Pacific/Saipan', 'Atlantic/Azores', 'America/Denver'];
174 const results = tzids.map(getSupportedTimezone);
175 expect(results).toEqual(expected);
178 it('should return undefined for unknown timezones', () => {
179 const unknown = ['Chamorro Standard Time', '(UTC+01:00) Bruxelles, Copenhague, Madrid, Paris'];
180 const results = unknown.map((tzid) => getSupportedTimezone(tzid));
181 const expected = unknown.map(() => undefined);
182 expect(results).toEqual(expected);
186 describe('guessTimeZone', () => {
187 const timeZoneList = listTimeZones();
189 describe('should guess right when the browser detects "Europe/Zurich"', () => {
191 spyOn<any>(Intl, 'DateTimeFormat').and.returnValue({
192 resolvedOptions: () => ({ timeZone: 'Europe/Zurich' }),
196 it('guesses "Europe/Zurich"', () => {
197 expect(guessTimezone(timeZoneList)).toBe('Europe/Zurich');
201 describe('should guess right when the browser detects "Europe/Kyiv"', () => {
203 spyOn<any>(Intl, 'DateTimeFormat').and.returnValue({
204 resolvedOptions: () => ({ timeZone: 'Europe/Kyiv' }),
208 it('guesses "Europe/Kiev"', () => {
209 expect(guessTimezone(timeZoneList)).toBe('Europe/Kiev');
213 describe('should guess right when the browser detects "Pacific/Kanton"', () => {
215 spyOn<any>(Intl, 'DateTimeFormat').and.returnValue({
216 resolvedOptions: () => ({ timeZone: 'Pacific/Kanton' }),
220 it('guesses "Pacific/Fakaofo"', () => {
221 expect(guessTimezone(timeZoneList)).toBe('Pacific/Fakaofo');
225 describe('should guess right when the browser detects unknown time zone with GMT+1', () => {
227 const baseTime = Date.UTC(2023, 2, 7, 9);
229 spyOn<any>(Intl, 'DateTimeFormat').and.returnValue({
230 resolvedOptions: () => ({ timeZone: 'unknown' }),
232 spyOn<any>(window, 'Date').and.returnValue({
233 getTime: () => baseTime,
234 getTimezoneOffset: () => -60,
235 getUTCFullYear: () => 2023,
236 getUTCMonth: () => 2,
239 getUTCHours: () => 9,
240 getUTCMinutes: () => 0,
241 getUTCSeconds: () => 0,
242 getUTCMilliseconds: () => 0,
246 it('guesses "Africa/Algiers", first in the list with that GMT offset', () => {
247 expect(guessTimezone(timeZoneList)).toBe('Africa/Algiers');
252 describe('getReadableCityTimezone', () => {
253 it('should return city timezone without underscores', () => {
254 const cityTimezone = 'Los_Angeles';
256 expect(getReadableCityTimezone(cityTimezone)).toEqual('Los Angeles');
259 it('should return city timezone without change', () => {
260 const cityTimezone = 'Paris';
262 expect(getReadableCityTimezone(cityTimezone)).toEqual('Paris');
266 describe('getAbbreviatedTimezoneName', () => {
267 it('should return the expected offset timezone', () => {
268 const timezone = 'Europe/Paris';
269 const result = getAbbreviatedTimezoneName('offset', timezone) || '';
271 expect(['GMT+1', 'GMT+2']).toContain(result);
274 it('should return the expected city timezone', () => {
275 const timezone = 'Europe/Paris';
277 expect(getAbbreviatedTimezoneName('city', timezone)).toEqual('Paris');
280 it('should return the expected city timezone on a readable format', () => {
281 const timezone = 'America/Los_Angeles';
283 expect(getAbbreviatedTimezoneName('city', timezone)).toEqual('Los Angeles');
286 it('should return the expected city timezone for a longer timezone', () => {
287 const timezone = 'America/North_Dakota/New_Salem';
289 expect(getAbbreviatedTimezoneName('city', timezone)).toEqual('New Salem');
293 describe('getTimezoneAndOffset', () => {
294 it('should return the expected timezone string, containing offset and name', () => {
295 const timezone = 'Europe/Paris';
296 const result = getTimezoneAndOffset(timezone);
298 expect(['GMT+1 • Europe/Paris', 'GMT+2 • Europe/Paris']).toContain(result);