Skip to content

Commit b17750b

Browse files
authored
Merge pull request #193 from cloud-atlas-ai/fix-microsoft-timezone-parsing
Support Windows Timezones
2 parents 9d9becc + 85bbb7c commit b17750b

File tree

7 files changed

+1102
-9
lines changed

7 files changed

+1102
-9
lines changed

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,8 @@
1010
"version": "node version-bump.mjs && git add manifest.json versions.json",
1111
"test": "jest",
1212
"test:watch": "jest --watch",
13-
"test:coverage": "jest --coverage"
13+
"test:coverage": "jest --coverage",
14+
"update-timezones": "node scripts/update-timezones.js"
1415
},
1516
"keywords": [],
1617
"author": "",

scripts/update-timezones.js

Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
#!/usr/bin/env node
2+
3+
/**
4+
* Script to update Windows to IANA timezone mappings from Unicode CLDR data
5+
*
6+
* Usage: node scripts/update-timezones.js
7+
*
8+
* This script fetches the latest windowsZones.xml from Unicode CLDR project
9+
* and updates the timezone mapping in src/icalUtils.ts
10+
*/
11+
12+
const https = require('https');
13+
const fs = require('fs');
14+
const path = require('path');
15+
16+
const CLDR_URL = 'https://raw.githubusercontent.com/unicode-org/cldr/main/common/supplemental/windowsZones.xml';
17+
const OUTPUT_TS_FILE = path.join(__dirname, '..', 'src', 'generated', 'windowsTimezones.ts');
18+
const OUTPUT_JSON_FILE = path.join(__dirname, '..', 'src', 'generated', 'windows-to-iana.json');
19+
const OUTPUT_YAML_FILE = path.join(__dirname, '..', 'src', 'generated', 'windows-to-iana.yaml');
20+
21+
console.log('Fetching Windows timezone mappings from Unicode CLDR...');
22+
23+
https.get(CLDR_URL, (res) => {
24+
let data = '';
25+
26+
res.on('data', (chunk) => {
27+
data += chunk;
28+
});
29+
30+
res.on('end', () => {
31+
try {
32+
const mappings = parseWindowsZones(data);
33+
34+
// Ensure output directory exists
35+
const generatedDir = path.dirname(OUTPUT_TS_FILE);
36+
if (!fs.existsSync(generatedDir)) {
37+
fs.mkdirSync(generatedDir, { recursive: true });
38+
}
39+
40+
generateTypeScriptFile(mappings);
41+
generateJsonFile(mappings);
42+
generateYamlFile(mappings);
43+
44+
console.log(`✅ Successfully updated ${Object.keys(mappings).length} timezone mappings`);
45+
console.log(`📁 TypeScript: ${OUTPUT_TS_FILE}`);
46+
console.log(`📁 JSON: ${OUTPUT_JSON_FILE}`);
47+
console.log(`📁 YAML: ${OUTPUT_YAML_FILE}`);
48+
} catch (error) {
49+
console.error('❌ Error parsing timezone data:', error.message);
50+
process.exit(1);
51+
}
52+
});
53+
}).on('error', (err) => {
54+
console.error('❌ Error fetching timezone data:', err.message);
55+
process.exit(1);
56+
});
57+
58+
function parseWindowsZones(xmlData) {
59+
const mappings = {};
60+
61+
// Extract mapZone elements with territory="001" (primary mappings)
62+
const mapZoneRegex = /<mapZone[^>]*other="([^"]*)"[^>]*territory="001"[^>]*type="([^"]*)"/g;
63+
64+
let match;
65+
while ((match = mapZoneRegex.exec(xmlData)) !== null) {
66+
const windowsZone = match[1];
67+
const ianaZone = match[2];
68+
69+
// Take the first IANA zone if multiple are specified
70+
const primaryIanaZone = ianaZone.split(' ')[0];
71+
72+
mappings[windowsZone] = primaryIanaZone;
73+
}
74+
75+
return mappings;
76+
}
77+
78+
function generateTypeScriptFile(mappings) {
79+
const timestamp = new Date().toISOString();
80+
81+
const content = `/**
82+
* Windows to IANA timezone mappings
83+
*
84+
* Generated from Unicode CLDR windowsZones.xml
85+
* Last updated: ${timestamp}
86+
* Source: https://github.com/unicode-org/cldr/blob/main/common/supplemental/windowsZones.xml
87+
*
88+
* To update this file, run: node scripts/update-timezones.js
89+
*/
90+
91+
export const WINDOWS_TO_IANA_TIMEZONES: Record<string, string> = ${JSON.stringify(mappings, null, 2)};
92+
93+
/**
94+
* Get IANA timezone identifier for a Windows timezone name
95+
* @param windowsTimezone Windows timezone name (e.g., "Eastern Standard Time")
96+
* @returns IANA timezone identifier (e.g., "America/New_York") or undefined if not found
97+
*/
98+
export function getIanaTimezone(windowsTimezone: string): string | undefined {
99+
return WINDOWS_TO_IANA_TIMEZONES[windowsTimezone];
100+
}
101+
102+
/**
103+
* Get all supported Windows timezone names
104+
* @returns Array of Windows timezone names
105+
*/
106+
export function getSupportedWindowsTimezones(): string[] {
107+
return Object.keys(WINDOWS_TO_IANA_TIMEZONES);
108+
}
109+
`;
110+
111+
fs.writeFileSync(OUTPUT_TS_FILE, content, 'utf8');
112+
}
113+
114+
function generateJsonFile(mappings) {
115+
const timestamp = new Date().toISOString();
116+
117+
const jsonData = {
118+
metadata: {
119+
description: "Windows to IANA timezone mappings",
120+
source: "Unicode CLDR windowsZones.xml",
121+
sourceUrl: "https://github.com/unicode-org/cldr/blob/main/common/supplemental/windowsZones.xml",
122+
lastUpdated: timestamp,
123+
updateCommand: "node scripts/update-timezones.js",
124+
totalMappings: Object.keys(mappings).length
125+
},
126+
mappings: mappings
127+
};
128+
129+
fs.writeFileSync(OUTPUT_JSON_FILE, JSON.stringify(jsonData, null, 2), 'utf8');
130+
}
131+
132+
function generateYamlFile(mappings) {
133+
const timestamp = new Date().toISOString();
134+
135+
let yamlContent = `# Windows to IANA timezone mappings
136+
#
137+
# Generated from Unicode CLDR windowsZones.xml
138+
# Last updated: ${timestamp}
139+
# Source: https://github.com/unicode-org/cldr/blob/main/common/supplemental/windowsZones.xml
140+
#
141+
# To update this file, run: node scripts/update-timezones.js
142+
143+
metadata:
144+
description: "Windows to IANA timezone mappings"
145+
source: "Unicode CLDR windowsZones.xml"
146+
sourceUrl: "https://github.com/unicode-org/cldr/blob/main/common/supplemental/windowsZones.xml"
147+
lastUpdated: "${timestamp}"
148+
updateCommand: "node scripts/update-timezones.js"
149+
totalMappings: ${Object.keys(mappings).length}
150+
151+
mappings:
152+
`;
153+
154+
// Convert mappings to YAML format
155+
for (const [windowsZone, ianaZone] of Object.entries(mappings)) {
156+
yamlContent += ` "${windowsZone}": "${ianaZone}"\n`;
157+
}
158+
159+
fs.writeFileSync(OUTPUT_YAML_FILE, yamlContent, 'utf8');
160+
}

src/generated/windows-to-iana.json

Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
{
2+
"metadata": {
3+
"description": "Windows to IANA timezone mappings",
4+
"source": "Unicode CLDR windowsZones.xml",
5+
"sourceUrl": "https://github.com/unicode-org/cldr/blob/main/common/supplemental/windowsZones.xml",
6+
"lastUpdated": "2025-09-23T01:28:13.315Z",
7+
"updateCommand": "node scripts/update-timezones.js",
8+
"totalMappings": 139
9+
},
10+
"mappings": {
11+
"Dateline Standard Time": "Etc/GMT+12",
12+
"UTC-11": "Etc/GMT+11",
13+
"Aleutian Standard Time": "America/Adak",
14+
"Hawaiian Standard Time": "Pacific/Honolulu",
15+
"Marquesas Standard Time": "Pacific/Marquesas",
16+
"Alaskan Standard Time": "America/Anchorage",
17+
"UTC-09": "Etc/GMT+9",
18+
"Pacific Standard Time (Mexico)": "America/Tijuana",
19+
"UTC-08": "Etc/GMT+8",
20+
"Pacific Standard Time": "America/Los_Angeles",
21+
"US Mountain Standard Time": "America/Phoenix",
22+
"Mountain Standard Time (Mexico)": "America/Mazatlan",
23+
"Mountain Standard Time": "America/Denver",
24+
"Yukon Standard Time": "America/Whitehorse",
25+
"Central America Standard Time": "America/Guatemala",
26+
"Central Standard Time": "America/Chicago",
27+
"Easter Island Standard Time": "Pacific/Easter",
28+
"Central Standard Time (Mexico)": "America/Mexico_City",
29+
"Canada Central Standard Time": "America/Regina",
30+
"SA Pacific Standard Time": "America/Bogota",
31+
"Eastern Standard Time (Mexico)": "America/Cancun",
32+
"Eastern Standard Time": "America/New_York",
33+
"Haiti Standard Time": "America/Port-au-Prince",
34+
"Cuba Standard Time": "America/Havana",
35+
"US Eastern Standard Time": "America/Indianapolis",
36+
"Turks And Caicos Standard Time": "America/Grand_Turk",
37+
"Paraguay Standard Time": "America/Asuncion",
38+
"Atlantic Standard Time": "America/Halifax",
39+
"Venezuela Standard Time": "America/Caracas",
40+
"Central Brazilian Standard Time": "America/Cuiaba",
41+
"SA Western Standard Time": "America/La_Paz",
42+
"Pacific SA Standard Time": "America/Santiago",
43+
"Newfoundland Standard Time": "America/St_Johns",
44+
"Tocantins Standard Time": "America/Araguaina",
45+
"E. South America Standard Time": "America/Sao_Paulo",
46+
"SA Eastern Standard Time": "America/Cayenne",
47+
"Argentina Standard Time": "America/Buenos_Aires",
48+
"Greenland Standard Time": "America/Godthab",
49+
"Montevideo Standard Time": "America/Montevideo",
50+
"Magallanes Standard Time": "America/Punta_Arenas",
51+
"Saint Pierre Standard Time": "America/Miquelon",
52+
"Bahia Standard Time": "America/Bahia",
53+
"UTC-02": "Etc/GMT+2",
54+
"Azores Standard Time": "Atlantic/Azores",
55+
"Cape Verde Standard Time": "Atlantic/Cape_Verde",
56+
"UTC": "Etc/UTC",
57+
"GMT Standard Time": "Europe/London",
58+
"Greenwich Standard Time": "Atlantic/Reykjavik",
59+
"Sao Tome Standard Time": "Africa/Sao_Tome",
60+
"Morocco Standard Time": "Africa/Casablanca",
61+
"W. Europe Standard Time": "Europe/Berlin",
62+
"Central Europe Standard Time": "Europe/Budapest",
63+
"Romance Standard Time": "Europe/Paris",
64+
"Central European Standard Time": "Europe/Warsaw",
65+
"W. Central Africa Standard Time": "Africa/Lagos",
66+
"Jordan Standard Time": "Asia/Amman",
67+
"GTB Standard Time": "Europe/Bucharest",
68+
"Middle East Standard Time": "Asia/Beirut",
69+
"Egypt Standard Time": "Africa/Cairo",
70+
"E. Europe Standard Time": "Europe/Chisinau",
71+
"Syria Standard Time": "Asia/Damascus",
72+
"West Bank Standard Time": "Asia/Hebron",
73+
"South Africa Standard Time": "Africa/Johannesburg",
74+
"FLE Standard Time": "Europe/Kiev",
75+
"Israel Standard Time": "Asia/Jerusalem",
76+
"South Sudan Standard Time": "Africa/Juba",
77+
"Kaliningrad Standard Time": "Europe/Kaliningrad",
78+
"Sudan Standard Time": "Africa/Khartoum",
79+
"Libya Standard Time": "Africa/Tripoli",
80+
"Namibia Standard Time": "Africa/Windhoek",
81+
"Arabic Standard Time": "Asia/Baghdad",
82+
"Turkey Standard Time": "Europe/Istanbul",
83+
"Arab Standard Time": "Asia/Riyadh",
84+
"Belarus Standard Time": "Europe/Minsk",
85+
"Russian Standard Time": "Europe/Moscow",
86+
"E. Africa Standard Time": "Africa/Nairobi",
87+
"Iran Standard Time": "Asia/Tehran",
88+
"Arabian Standard Time": "Asia/Dubai",
89+
"Astrakhan Standard Time": "Europe/Astrakhan",
90+
"Azerbaijan Standard Time": "Asia/Baku",
91+
"Russia Time Zone 3": "Europe/Samara",
92+
"Mauritius Standard Time": "Indian/Mauritius",
93+
"Saratov Standard Time": "Europe/Saratov",
94+
"Georgian Standard Time": "Asia/Tbilisi",
95+
"Volgograd Standard Time": "Europe/Volgograd",
96+
"Caucasus Standard Time": "Asia/Yerevan",
97+
"Afghanistan Standard Time": "Asia/Kabul",
98+
"West Asia Standard Time": "Asia/Tashkent",
99+
"Ekaterinburg Standard Time": "Asia/Yekaterinburg",
100+
"Pakistan Standard Time": "Asia/Karachi",
101+
"Qyzylorda Standard Time": "Asia/Qyzylorda",
102+
"India Standard Time": "Asia/Calcutta",
103+
"Sri Lanka Standard Time": "Asia/Colombo",
104+
"Nepal Standard Time": "Asia/Katmandu",
105+
"Central Asia Standard Time": "Asia/Bishkek",
106+
"Bangladesh Standard Time": "Asia/Dhaka",
107+
"Omsk Standard Time": "Asia/Omsk",
108+
"Myanmar Standard Time": "Asia/Rangoon",
109+
"SE Asia Standard Time": "Asia/Bangkok",
110+
"Altai Standard Time": "Asia/Barnaul",
111+
"W. Mongolia Standard Time": "Asia/Hovd",
112+
"North Asia Standard Time": "Asia/Krasnoyarsk",
113+
"N. Central Asia Standard Time": "Asia/Novosibirsk",
114+
"Tomsk Standard Time": "Asia/Tomsk",
115+
"China Standard Time": "Asia/Shanghai",
116+
"North Asia East Standard Time": "Asia/Irkutsk",
117+
"Singapore Standard Time": "Asia/Singapore",
118+
"W. Australia Standard Time": "Australia/Perth",
119+
"Taipei Standard Time": "Asia/Taipei",
120+
"Ulaanbaatar Standard Time": "Asia/Ulaanbaatar",
121+
"Aus Central W. Standard Time": "Australia/Eucla",
122+
"Transbaikal Standard Time": "Asia/Chita",
123+
"Tokyo Standard Time": "Asia/Tokyo",
124+
"North Korea Standard Time": "Asia/Pyongyang",
125+
"Korea Standard Time": "Asia/Seoul",
126+
"Yakutsk Standard Time": "Asia/Yakutsk",
127+
"Cen. Australia Standard Time": "Australia/Adelaide",
128+
"AUS Central Standard Time": "Australia/Darwin",
129+
"E. Australia Standard Time": "Australia/Brisbane",
130+
"AUS Eastern Standard Time": "Australia/Sydney",
131+
"West Pacific Standard Time": "Pacific/Port_Moresby",
132+
"Tasmania Standard Time": "Australia/Hobart",
133+
"Vladivostok Standard Time": "Asia/Vladivostok",
134+
"Lord Howe Standard Time": "Australia/Lord_Howe",
135+
"Bougainville Standard Time": "Pacific/Bougainville",
136+
"Russia Time Zone 10": "Asia/Srednekolymsk",
137+
"Magadan Standard Time": "Asia/Magadan",
138+
"Norfolk Standard Time": "Pacific/Norfolk",
139+
"Sakhalin Standard Time": "Asia/Sakhalin",
140+
"Central Pacific Standard Time": "Pacific/Guadalcanal",
141+
"Russia Time Zone 11": "Asia/Kamchatka",
142+
"New Zealand Standard Time": "Pacific/Auckland",
143+
"UTC+12": "Etc/GMT-12",
144+
"Fiji Standard Time": "Pacific/Fiji",
145+
"Chatham Islands Standard Time": "Pacific/Chatham",
146+
"UTC+13": "Etc/GMT-13",
147+
"Tonga Standard Time": "Pacific/Tongatapu",
148+
"Samoa Standard Time": "Pacific/Apia",
149+
"Line Islands Standard Time": "Pacific/Kiritimati"
150+
}
151+
}

0 commit comments

Comments
 (0)