From 16039a377f4b1ef0c43cdf46316371438e9e4508 Mon Sep 17 00:00:00 2001 From: niranjan-uma-shankar Date: Tue, 29 Jul 2025 15:21:05 +0530 Subject: [PATCH 1/8] Prepends the label with DST aware GMT offset string This implements a GMT offset string that is DST aware, and is prepended to the label value. --- packages/timezone-data/src/index.ts | 191 ++++++++++++++++++---------- 1 file changed, 124 insertions(+), 67 deletions(-) diff --git a/packages/timezone-data/src/index.ts b/packages/timezone-data/src/index.ts index 31440e640..4f0bb97a6 100644 --- a/packages/timezone-data/src/index.ts +++ b/packages/timezone-data/src/index.ts @@ -1,272 +1,329 @@ const timezoneData: {name: string; label: string}[] = [ { name: 'Pacific/Pago_Pago', - label: '(GMT -11:00) Midway Island, Samoa' + label: 'Midway Island, Samoa' }, { name: 'Pacific/Honolulu', - label: '(GMT -10:00) Hawaii' + label: 'Hawaii' }, { name: 'America/Anchorage', - label: '(GMT -9:00) Alaska' + label: 'Alaska' }, { name: 'America/Tijuana', - label: '(GMT -8:00) Chihuahua, La Paz, Mazatlan' + label: 'Chihuahua, La Paz, Mazatlan' }, { name: 'America/Los_Angeles', - label: '(GMT -8:00) Pacific Time (US & Canada); Tijuana' + label: 'Pacific Time (US & Canada); Tijuana' }, { name: 'America/Phoenix', - label: '(GMT -7:00) Arizona' + label: 'Arizona' }, { name: 'America/Denver', - label: '(GMT -7:00) Mountain Time (US & Canada)' + label: 'Mountain Time (US & Canada)' }, { name: 'America/Costa_Rica', - label: '(GMT -6:00) Central America' + label: 'Central America' }, { name: 'America/Chicago', - label: '(GMT -6:00) Central Time (US & Canada)' + label: 'Central Time (US & Canada)' }, { name: 'America/Mexico_City', - label: '(GMT -6:00) Guadalajara, Mexico City, Monterrey' + label: 'Guadalajara, Mexico City, Monterrey' }, { name: 'America/Regina', - label: '(GMT -6:00) Saskatchewan' + label: 'Saskatchewan' }, { name: 'America/Bogota', - label: '(GMT -5:00) Bogota, Lima, Quito' + label: 'Bogota, Lima, Quito' }, { name: 'America/New_York', - label: '(GMT -5:00) Eastern Time (US & Canada)' + label: 'Eastern Time (US & Canada)' }, { name: 'America/Fort_Wayne', - label: '(GMT -5:00) Indiana (East)' + label: 'Indiana (East)' }, { name: 'America/Caracas', - label: '(GMT -4:00) Caracas, La Paz' + label: 'Caracas, La Paz' }, { name: 'America/Halifax', - label: '(GMT -4:00) Atlantic Time (Canada); Greenland' + label: 'Atlantic Time (Canada); Greenland' }, { name: 'America/Santiago', - label: '(GMT -4:00) Santiago' + label: 'Santiago' }, { name: 'America/St_Johns', - label: '(GMT -3:30) Newfoundland' + label: 'Newfoundland' }, { name: 'America/Argentina/Buenos_Aires', - label: '(GMT -3:00) Buenos Aires, Brasilia, Georgetown' + label: 'Buenos Aires, Brasilia, Georgetown' }, { name: 'America/Noronha', - label: '(GMT -2:00) Fernando de Noronha' + label: 'Fernando de Noronha' }, { name: 'Atlantic/Azores', - label: '(GMT -1:00) Azores' + label: 'Azores' }, { name: 'Atlantic/Cape_Verde', - label: '(GMT -1:00) Cape Verde Is.' + label: 'Cape Verde Is.' }, { name: 'Etc/UTC', - label: '(GMT) UTC' + label: 'UTC' }, { name: 'Africa/Casablanca', - label: '(GMT +0:00) Casablanca, Monrovia' + label: 'Casablanca, Monrovia' }, { name: 'Europe/Dublin', - label: '(GMT +0:00) Dublin, Edinburgh, London' + label: 'Dublin, Edinburgh, London' }, { name: 'Europe/Amsterdam', - label: '(GMT +1:00) Amsterdam, Berlin, Rome, Stockholm, Vienna' + label: ' Amsterdam, Berlin, Rome, Stockholm, Vienna' }, { name: 'Europe/Prague', - label: '(GMT +1:00) Belgrade, Bratislava, Budapest, Prague' + label: 'Belgrade, Bratislava, Budapest, Prague' }, { name: 'Europe/Paris', - label: '(GMT +1:00) Brussels, Copenhagen, Madrid, Paris' + label: 'Brussels, Copenhagen, Madrid, Paris' }, { name: 'Europe/Warsaw', - label: '(GMT +1:00) Sarajevo, Skopje, Warsaw, Zagreb' + label: 'Sarajevo, Skopje, Warsaw, Zagreb' }, { name: 'Africa/Lagos', - label: '(GMT +1:00) West Central Africa' + label: 'West Central Africa' }, { name: 'Europe/Athens', - label: '(GMT +2:00) Athens, Beirut, Bucharest' + label: 'Athens, Beirut, Bucharest' }, { name: 'Africa/Cairo', - label: '(GMT +2:00) Cairo, Egypt' + label: 'Cairo, Egypt' }, { name: 'Africa/Maputo', - label: '(GMT +2:00) Harare' + label: 'Harare' }, { name: 'Europe/Kiev', // Changing name to 'Europe/Kiev', and keeping the UI with Kyiv. Change this once we are passed the moment lib update. - label: '(GMT +2:00) Helsinki, Kyiv, Riga, Sofia, Tallinn, Vilnius' + label: 'Helsinki, Kyiv, Riga, Sofia, Tallinn, Vilnius' }, { name: 'Asia/Jerusalem', - label: '(GMT +2:00) Jerusalem' + label: 'Jerusalem' }, { name: 'Africa/Johannesburg', - label: '(GMT +2:00) Pretoria' + label: 'Pretoria' }, { name: 'Asia/Baghdad', - label: '(GMT +3:00) Baghdad' + label: 'Baghdad' }, { name: 'Asia/Riyadh', - label: '(GMT +3:00) Kuwait, Nairobi, Riyadh' + label: 'Kuwait, Nairobi, Riyadh' }, { name: 'Europe/Istanbul', - label: '(GMT +3:00) Istanbul, Ankara' + label: 'Istanbul, Ankara' }, { name: 'Europe/Moscow', - label: '(GMT +3:00) Moscow, St. Petersburg, Volgograd' + label: 'Moscow, St. Petersburg, Volgograd' }, { name: 'Asia/Tehran', - label: '(GMT +3:30) Tehran' + label: 'Tehran' }, { name: 'Asia/Dubai', - label: '(GMT +4:00) Abu Dhabi, Muscat' + label: 'Abu Dhabi, Muscat' }, { name: 'Asia/Baku', - label: '(GMT +4:00) Baku, Tbilisi, Yerevan' + label: 'Baku, Tbilisi, Yerevan' }, { name: 'Asia/Kabul', - label: '(GMT +4:30) Kabul' + label: 'Kabul' }, { name: 'Asia/Karachi', - label: '(GMT +5:00) Islamabad, Karachi, Tashkent' + label: 'Islamabad, Karachi, Tashkent' }, { name: 'Asia/Yekaterinburg', - label: '(GMT +5:00) Yekaterinburg' + label: 'Yekaterinburg' }, { name: 'Asia/Kolkata', - label: '(GMT +5:30) Chennai, Calcutta, Mumbai, New Delhi' + label: 'Chennai, Kolkata, Mumbai, New Delhi' }, { name: 'Asia/Kathmandu', - label: '(GMT +5:45) Katmandu' + label: 'Katmandu' }, { name: 'Asia/Almaty', - label: '(GMT +6:00) Almaty, Novosibirsk' + label: 'Almaty, Novosibirsk' }, { name: 'Asia/Dhaka', - label: '(GMT +6:00) Astana, Dhaka, Sri Jayawardenepura' + label: 'Astana, Dhaka, Sri Jayawardenepura' }, { name: 'Asia/Rangoon', - label: '(GMT +6:30) Rangoon' + label: 'Rangoon' }, { name: 'Asia/Bangkok', - label: '(GMT +7:00) Bangkok, Hanoi, Jakarta' + label: 'Bangkok, Hanoi, Jakarta' }, { name: 'Asia/Krasnoyarsk', - label: '(GMT +7:00) Krasnoyarsk' + label: 'Krasnoyarsk' }, { name: 'Asia/Hong_Kong', - label: '(GMT +8:00) Beijing, Chongqing, Hong Kong, Urumqi' + label: 'Beijing, Chongqing, Hong Kong, Urumqi' }, { name: 'Asia/Irkutsk', - label: '(GMT +8:00) Irkutsk, Ulaan Bataar' + label: 'Irkutsk, Ulaan Bataar' }, { name: 'Asia/Singapore', - label: '(GMT +8:00) Kuala Lumpur, Perth, Singapore, Taipei' + label: 'Kuala Lumpur, Perth, Singapore, Taipei' }, { name: 'Asia/Tokyo', - label: '(GMT +9:00) Osaka, Sapporo, Tokyo' + label: 'Osaka, Sapporo, Tokyo' }, { name: 'Asia/Seoul', - label: '(GMT +9:00) Seoul' + label: 'Seoul' }, { name: 'Asia/Yakutsk', - label: '(GMT +9:00) Yakutsk' + label: 'Yakutsk' }, { name: 'Australia/Adelaide', - label: '(GMT +9:30) Adelaide' + label: 'Adelaide' }, { name: 'Australia/Darwin', - label: '(GMT +9:30) Darwin' + label: 'Darwin' }, { name: 'Australia/Brisbane', - label: '(GMT +10:00) Brisbane, Guam, Port Moresby' + label: 'Brisbane, Guam, Port Moresby' }, { name: 'Australia/Sydney', - label: '(GMT +10:00) Canberra, Hobart, Melbourne, Sydney, Vladivostok' + label: 'Canberra, Hobart, Melbourne, Sydney, Vladivostok' }, { name: 'Asia/Magadan', - label: '(GMT +11:00) Magadan, Soloman Is., New Caledonia' + label: 'Magadan, Soloman Is., New Caledonia' }, { name: 'Pacific/Auckland', - label: '(GMT +12:00) Auckland, Wellington' + label: 'Auckland, Wellington' }, { name: 'Pacific/Fiji', - label: '(GMT +12:00) Fiji, Kamchatka, Marshall Is.' + label: 'Fiji, Kamchatka, Marshall Is.' }, { name: 'Pacific/Kwajalein', - label: '(GMT +12:00) International Date Line West' + label: 'International Date Line West' } ]; +interface GMTOffsetData { + offsetString: string | null; + offsetMinutes: number; +} + +interface TimezoneDataWithOffset { + name: string; + label: string; + offsetValue: number; +} + +const getGMTOffsetString = (timeZone: string): GMTOffsetData => { + const options: Intl.DateTimeFormatOptions = { + timeZone, + timeZoneName: 'shortOffset' + }; + + const formatter = new Intl.DateTimeFormat('en-GB', options); + const parts = formatter.formatToParts(new Date()); + const offsetPart = parts.find(part => part.type === 'timeZoneName')?.value; + + if (!offsetPart) { + return {offsetString: null, offsetMinutes: 0}; + } + + const match = offsetPart.match(/^GMT([+-])(\d{1,2})(?::(\d{2}))?$/); + + if (!match) { + return {offsetString: offsetPart, offsetMinutes: 0}; + } + + const sign = match[1]; + const hour = parseInt(match[2], 10); + const minute = parseInt(match[3] ?? '0', 10); + const totalMinutes = sign === '+' ? (hour * 60 + minute) : -(hour * 60 + minute); + const offsetString = `GMT ${sign}${hour}:${minute.toString().padStart(2, '0')}`; + + return {offsetString, offsetMinutes: totalMinutes}; +}; + +const labelWithGMTOffset = (label: string, offsetString: string): string => { + return '(' + offsetString + ') ' + label; +}; + +export const timezoneDataWithGMTOffset = (): TimezoneDataWithOffset[] => { + return timezoneData + .map(({name, label}) => { + const {offsetString, offsetMinutes} = getGMTOffsetString(name); + return { + name, + label: offsetString ? labelWithGMTOffset(label, offsetString) : label, + offsetValue: offsetMinutes + }; + }) + .sort((a, b) => a.offsetValue - b.offsetValue); +}; + export default timezoneData; From 568e4edd39a32285d1d02767bb3b7af545e01bb8 Mon Sep 17 00:00:00 2001 From: niranjan-uma-shankar Date: Tue, 29 Jul 2025 21:59:19 +0530 Subject: [PATCH 2/8] Renames function name --- packages/timezone-data/src/index.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/timezone-data/src/index.ts b/packages/timezone-data/src/index.ts index 4f0bb97a6..b808d096f 100644 --- a/packages/timezone-data/src/index.ts +++ b/packages/timezone-data/src/index.ts @@ -280,7 +280,7 @@ interface TimezoneDataWithOffset { offsetValue: number; } -const getGMTOffsetString = (timeZone: string): GMTOffsetData => { +const getGMTOffset = (timeZone: string): GMTOffsetData => { const options: Intl.DateTimeFormatOptions = { timeZone, timeZoneName: 'shortOffset' @@ -316,7 +316,7 @@ const labelWithGMTOffset = (label: string, offsetString: string): string => { export const timezoneDataWithGMTOffset = (): TimezoneDataWithOffset[] => { return timezoneData .map(({name, label}) => { - const {offsetString, offsetMinutes} = getGMTOffsetString(name); + const {offsetString, offsetMinutes} = getGMTOffset(name); return { name, label: offsetString ? labelWithGMTOffset(label, offsetString) : label, From b19edcff330fe89639e298ab0b5b78080a9b3644 Mon Sep 17 00:00:00 2001 From: niranjan-uma-shankar Date: Tue, 29 Jul 2025 21:59:39 +0530 Subject: [PATCH 3/8] Updates target to es2020 to add support for formatToParts(). es2020 is supported by most modern browsers, so should be safe. --- packages/timezone-data/tsconfig.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/timezone-data/tsconfig.json b/packages/timezone-data/tsconfig.json index 1825fce5f..4eaf4e3f7 100644 --- a/packages/timezone-data/tsconfig.json +++ b/packages/timezone-data/tsconfig.json @@ -1,6 +1,6 @@ { "compilerOptions": { - "target": "es2016", + "target": "es2020", "module": "ESNext", "esModuleInterop": true, "forceConsistentCasingInFileNames": true, From caa9a58576c41a8d3d0a1d2c835e7e44b17c1f73 Mon Sep 17 00:00:00 2001 From: niranjan-uma-shankar Date: Wed, 30 Jul 2025 17:40:46 +0530 Subject: [PATCH 4/8] Refactors to simplify code Uses longOffset to simplify the regex match and code. --- packages/timezone-data/src/index.ts | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/packages/timezone-data/src/index.ts b/packages/timezone-data/src/index.ts index b808d096f..ffc9155b0 100644 --- a/packages/timezone-data/src/index.ts +++ b/packages/timezone-data/src/index.ts @@ -280,10 +280,10 @@ interface TimezoneDataWithOffset { offsetValue: number; } -const getGMTOffset = (timeZone: string): GMTOffsetData => { +export const getGMTOffset = (timeZone: string): GMTOffsetData => { const options: Intl.DateTimeFormatOptions = { timeZone, - timeZoneName: 'shortOffset' + timeZoneName: 'longOffset' }; const formatter = new Intl.DateTimeFormat('en-GB', options); @@ -294,15 +294,16 @@ const getGMTOffset = (timeZone: string): GMTOffsetData => { return {offsetString: null, offsetMinutes: 0}; } - const match = offsetPart.match(/^GMT([+-])(\d{1,2})(?::(\d{2}))?$/); + // Expecting formats like "GMT+05:30" or "GMT-08:00" + const match = offsetPart.match(/^GMT([+-])(\d{2}):(\d{2})$/); if (!match) { return {offsetString: offsetPart, offsetMinutes: 0}; } - const sign = match[1]; - const hour = parseInt(match[2], 10); - const minute = parseInt(match[3] ?? '0', 10); + const [, sign, hourStr, minuteStr] = match; + const hour = parseInt(hourStr, 10); + const minute = parseInt(minuteStr, 10); const totalMinutes = sign === '+' ? (hour * 60 + minute) : -(hour * 60 + minute); const offsetString = `GMT ${sign}${hour}:${minute.toString().padStart(2, '0')}`; From b9d16e425321fc029069b0e4dd76b1e06b6db671 Mon Sep 17 00:00:00 2001 From: niranjan-uma-shankar Date: Tue, 12 Aug 2025 12:22:09 +0530 Subject: [PATCH 5/8] Refactor: (1) Improved variable name - changed offsetValue to offsetMinutes (2) added a try/catch block for graceful failure --- packages/timezone-data/src/index.ts | 50 ++++++++++++++++------------- 1 file changed, 27 insertions(+), 23 deletions(-) diff --git a/packages/timezone-data/src/index.ts b/packages/timezone-data/src/index.ts index ffc9155b0..5a445d869 100644 --- a/packages/timezone-data/src/index.ts +++ b/packages/timezone-data/src/index.ts @@ -277,7 +277,7 @@ interface GMTOffsetData { interface TimezoneDataWithOffset { name: string; label: string; - offsetValue: number; + offsetMinutes: number; } export const getGMTOffset = (timeZone: string): GMTOffsetData => { @@ -286,32 +286,36 @@ export const getGMTOffset = (timeZone: string): GMTOffsetData => { timeZoneName: 'longOffset' }; - const formatter = new Intl.DateTimeFormat('en-GB', options); - const parts = formatter.formatToParts(new Date()); - const offsetPart = parts.find(part => part.type === 'timeZoneName')?.value; + try { + const formatter = new Intl.DateTimeFormat('en-GB', options); + const parts = formatter.formatToParts(new Date()); + const offsetPart = parts.find(part => part.type === 'timeZoneName')?.value; - if (!offsetPart) { - return {offsetString: null, offsetMinutes: 0}; - } + if (!offsetPart) { + return {offsetString: null, offsetMinutes: 0}; + } - // Expecting formats like "GMT+05:30" or "GMT-08:00" - const match = offsetPart.match(/^GMT([+-])(\d{2}):(\d{2})$/); - - if (!match) { - return {offsetString: offsetPart, offsetMinutes: 0}; - } + // Expecting formats like "GMT+05:30" or "GMT-08:00" + const match = offsetPart.match(/^GMT([+-])(\d{2}):(\d{2})$/); + + if (!match) { + return {offsetString: offsetPart, offsetMinutes: 0}; + } - const [, sign, hourStr, minuteStr] = match; - const hour = parseInt(hourStr, 10); - const minute = parseInt(minuteStr, 10); - const totalMinutes = sign === '+' ? (hour * 60 + minute) : -(hour * 60 + minute); - const offsetString = `GMT ${sign}${hour}:${minute.toString().padStart(2, '0')}`; - - return {offsetString, offsetMinutes: totalMinutes}; + const [, sign, hourStr, minuteStr] = match; + const hour = parseInt(hourStr, 10); + const minute = parseInt(minuteStr, 10); + const totalMinutes = sign === '+' ? (hour * 60 + minute) : -(hour * 60 + minute); + const offsetString = `GMT ${sign}${hour}:${minute.toString().padStart(2, '0')}`; + + return {offsetString, offsetMinutes: totalMinutes}; + } catch (error) { + return {offsetString: null, offsetMinutes: 0}; + } }; const labelWithGMTOffset = (label: string, offsetString: string): string => { - return '(' + offsetString + ') ' + label; + return `(${offsetString}) ${label}`; }; export const timezoneDataWithGMTOffset = (): TimezoneDataWithOffset[] => { @@ -321,10 +325,10 @@ export const timezoneDataWithGMTOffset = (): TimezoneDataWithOffset[] => { return { name, label: offsetString ? labelWithGMTOffset(label, offsetString) : label, - offsetValue: offsetMinutes + offsetMinutes }; }) - .sort((a, b) => a.offsetValue - b.offsetValue); + .sort((a, b) => a.offsetMinutes - b.offsetMinutes); }; export default timezoneData; From 5d43173876396e631a1e5516d3e1140a6b2f996d Mon Sep 17 00:00:00 2001 From: niranjan-uma-shankar Date: Tue, 12 Aug 2025 12:22:34 +0530 Subject: [PATCH 6/8] Remove example file --- packages/timezone-data/test/hello.test.js | 10 ---------- 1 file changed, 10 deletions(-) delete mode 100644 packages/timezone-data/test/hello.test.js diff --git a/packages/timezone-data/test/hello.test.js b/packages/timezone-data/test/hello.test.js deleted file mode 100644 index 85d69d1e0..000000000 --- a/packages/timezone-data/test/hello.test.js +++ /dev/null @@ -1,10 +0,0 @@ -// Switch these lines once there are useful utils -// const testUtils = require('./utils'); -require('./utils'); - -describe('Hello world', function () { - it('Runs a test', function () { - // TODO: Write me! - 'hello'.should.eql('hello'); - }); -}); From 10a4296b483deda7637b6033df245514361108a3 Mon Sep 17 00:00:00 2001 From: niranjan-uma-shankar Date: Tue, 12 Aug 2025 12:22:52 +0530 Subject: [PATCH 7/8] Adds tests for the timezone-data package --- .../timezone-data/test/timezone-data.test.js | 131 ++++++++++++++++++ 1 file changed, 131 insertions(+) create mode 100644 packages/timezone-data/test/timezone-data.test.js diff --git a/packages/timezone-data/test/timezone-data.test.js b/packages/timezone-data/test/timezone-data.test.js new file mode 100644 index 000000000..105e102f5 --- /dev/null +++ b/packages/timezone-data/test/timezone-data.test.js @@ -0,0 +1,131 @@ +require('should'); +const sinon = require('sinon'); +const {getGMTOffset, timezoneDataWithGMTOffset} = require('../'); +const timezoneData = require('../').default; + +describe('Timezone Data with GMT Offset', function () { + let clock; + + afterEach(function () { + if (clock) { + clock.restore(); + } + }); + + describe('getGMTOffset', function () { + it('should return an object with offsetString and offsetMinutes properties', function () { + const result = getGMTOffset('Etc/UTC'); + result.should.be.an.Object(); + result.should.have.properties(['offsetString', 'offsetMinutes']); + }); + + it('should return the correct offset for Etc/UTC', function () { + const result = getGMTOffset('Etc/UTC'); + result.should.be.an.Object(); + result.offsetMinutes.should.equal(0); + result.offsetString.should.equal('GMT'); + }); + + it('should handle timezone with negative offset', function () { + const result = getGMTOffset('America/Phoenix'); + result.should.be.an.Object(); + result.offsetMinutes.should.equal(-420); + result.offsetString.should.equal('GMT -7:00'); + }); + + it('should handle timezone with half-hour offset like Asia/Kolkata', function () { + const result = getGMTOffset('Asia/Kolkata'); + result.should.be.an.Object(); + result.offsetMinutes.should.equal(330); + result.offsetString.should.equal('GMT +5:30'); + }); + + it('should handle invalid timezone gracefully', function () { + const result = getGMTOffset('Invalid/Timezone'); + result.should.be.an.Object(); + result.should.have.properties(['offsetString', 'offsetMinutes']); + (result.offsetString === null).should.be.true(); + result.offsetMinutes.should.equal(0); + }); + + it('should provide consistent results for non-DST timezone', function () { + clock = sinon.useFakeTimers(new Date('2024-07-01T12:00:00Z')); + const summerResult = getGMTOffset('America/Phoenix'); + clock.restore(); + + clock = sinon.useFakeTimers(new Date('2024-12-01T12:00:00Z')); + const winterResult = getGMTOffset('America/Phoenix'); + + // Arizona doesn't observe DST + winterResult.offsetMinutes.should.equal(summerResult.offsetMinutes); + }); + + it('should detect DST changes for timezone that observes DST', function () { + clock = sinon.useFakeTimers(new Date('2024-12-01T12:00:00Z')); // Winter + const winterResult = getGMTOffset('America/New_York'); + clock.restore(); + + clock = sinon.useFakeTimers(new Date('2024-07-01T12:00:00Z')); // Summer + const summerResult = getGMTOffset('America/New_York'); + + // Summer is 1 hour ahead of winter + (summerResult.offsetMinutes - winterResult.offsetMinutes).should.equal(60); + summerResult.offsetString.should.equal('GMT -4:00'); + summerResult.offsetMinutes.should.equal(-240); + winterResult.offsetString.should.equal('GMT -5:00'); + winterResult.offsetMinutes.should.equal(-300); + }); + }); + + describe('timezoneDataWithGMTOffset', function () { + it('should return an array of timezone data with GMT offsets', function () { + const result = timezoneDataWithGMTOffset(); + + result.should.be.an.Array(); + result.length.should.equal(67); + + result.forEach((item) => { + item.should.have.properties(['name', 'label', 'offsetMinutes']); + }); + }); + + it('should include GMT offset in labels', function () { + const result = timezoneDataWithGMTOffset(); + + const utcTimezone = result.find(tz => tz.name === 'Etc/UTC'); + utcTimezone.should.be.ok(); + utcTimezone.label.should.match(/^\(GMT\) UTC$/); + + const nyTimezone = result.find(tz => tz.name === 'America/New_York'); + nyTimezone.should.be.ok(); + nyTimezone.label.should.match(/^\(GMT -[45]:00\) Eastern Time \(US & Canada\)$/); + }); + + it('should preserve original timezone name', function () { + const result = timezoneDataWithGMTOffset(); + + const namesinResult = result.map(obj => obj.name).sort(); + const namesInTzData = timezoneData.map(obj => obj.name).sort(); + + namesinResult.should.deepEqual(namesInTzData); + }); + + it('should maintain consistent sorting across DST changes', function () { + clock = sinon.useFakeTimers(new Date('2024-12-01T12:00:00Z')); + const winterResult = timezoneDataWithGMTOffset(); + + clock.restore(); + clock = sinon.useFakeTimers(new Date('2024-07-01T12:00:00Z')); + const summerResult = timezoneDataWithGMTOffset(); + + // Both results should be properly sorted + for (let i = 1; i < winterResult.length; i++) { + winterResult[i].offsetMinutes.should.be.greaterThanOrEqual(winterResult[i - 1].offsetMinutes); + } + + for (let i = 1; i < summerResult.length; i++) { + summerResult[i].offsetMinutes.should.be.greaterThanOrEqual(summerResult[i - 1].offsetMinutes); + } + }); + }); +}); From de4d7277381e0fa273078d42ef335d8a4861d2cd Mon Sep 17 00:00:00 2001 From: niranjan-uma-shankar Date: Tue, 12 Aug 2025 12:25:26 +0530 Subject: [PATCH 8/8] Switch out with utls --- packages/timezone-data/test/timezone-data.test.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/timezone-data/test/timezone-data.test.js b/packages/timezone-data/test/timezone-data.test.js index 105e102f5..1894a6eb6 100644 --- a/packages/timezone-data/test/timezone-data.test.js +++ b/packages/timezone-data/test/timezone-data.test.js @@ -1,5 +1,4 @@ -require('should'); -const sinon = require('sinon'); +require('./utils'); const {getGMTOffset, timezoneDataWithGMTOffset} = require('../'); const timezoneData = require('../').default;