From 1b03928b9bab2073579048fe501ce71da3bc6e82 Mon Sep 17 00:00:00 2001
From: Tom Lane <tgl@sss.pgh.pa.us>
Date: Mon, 19 Oct 2026 21:13:40 -0400
Subject: [PATCH v1 2/3] Create infrastructure for building Windows zone name
 mapping list.

As of commit c1aa3b3c0, we adopted a policy of just following
CLDR's data for mapping Windows timezone display names to TZDB
zone names.  That being the case, let's avoid tedious manual
comparison of CLDR's windowsZones.xml to findtimezone.c by writing
a script to generate the desired constant data from windowsZones.xml.
This commit introduces the script and revises src/timezone/README
to describe using it rather than doing manual comparisons.

Author: Tom Lane <tgl@sss.pgh.pa.us>
Discussion: https://postgr.es/m/179219370621.3670768.16110484606338109870@cybernative.ai
Backpatch-through: 14
---
 src/timezone/README          |  23 +++---
 src/tools/gen_win32_tzmap.pl | 138 +++++++++++++++++++++++++++++++++++
 2 files changed, 151 insertions(+), 10 deletions(-)
 create mode 100644 src/tools/gen_win32_tzmap.pl

diff --git a/src/timezone/README b/src/timezone/README
index 0d48193d021..361f7244ff7 100644
--- a/src/timezone/README
+++ b/src/timezone/README
@@ -46,16 +46,19 @@ as a cross-check on proposed updates.
 
 When there has been a new release of Windows (probably including Service
 Packs), findtimezone.c's mapping from Windows zones to IANA zones may
-need to be updated.  We have two approaches to doing this:
-1. Consult the CLDR project's windowsZones.xml file, and add any zones
-   listed there that we don't have.  Use their "territory=001" mapping
-   if there's more than one IANA zone listed.
-2. Run the script in src/tools/win32tzlist.pl on a Windows machine
-   running the new release, and add any new timezones that it detects.
-   (This is not a full substitute for #1, though, as win32tzlist.pl
-   can't tell you which IANA zone to map to.)
-In either case, never remove any zone names that have disappeared from
-Windows, since we still need to match properly on older versions.
+need to be updated.  We now base that mapping on data maintained by CLDR.
+Obtain the CLDR project's windowsZones.xml file, and run it through
+src/tools/gen_win32_tzmap.pl to obtain a list of current Windows zone names.
+Compare that to the win32_tzmap[] array in src/bin/initdb/findtimezone.c.
+Insert or update entries as necessary, but never remove any zone names
+that have disappeared from Windows, since we still need to match properly
+on older versions.  Instead, move those entries to the list of legacy
+names at the end of the array.  (Multiple array entries mapping to the
+same tzdb zone is not a problem.)
+
+A helpful cross-check on the win32_tzmap[] array is to run the script
+in src/tools/win32tzlist.pl on a Windows machine to see if it detects
+any discrepancies.
 
 
 Time Zone code
diff --git a/src/tools/gen_win32_tzmap.pl b/src/tools/gen_win32_tzmap.pl
new file mode 100644
index 00000000000..ed5954ae205
--- /dev/null
+++ b/src/tools/gen_win32_tzmap.pl
@@ -0,0 +1,138 @@
+#----------------------------------------------------------------------
+#
+# gen_win32_tzmap.pl
+#	Perl script that transforms the CLDR windowsZones.xml file into
+#	suitable entries for findtimezone.c's win32_tzmap[] array.
+#
+# The input is an XML file containing entries like
+#   <!-- (UTC-12:00) International Date Line West -->
+#   <mapZone other="Dateline Standard Time" territory="001" type="Etc/GMT+12"/>
+# as well as some other lines that we ignore.  (This is far from being a
+# full XML parser, as CLDR is sufficiently rigid about their data layout
+# that we don't need that.)  We ignore entries that aren't for territory 001.
+#
+# The output is a fragment of C with entries like
+#	{
+#		/* (UTC-12:00) International Date Line West */
+#		"Dateline Standard Time", "Dateline Daylight Time",
+#		"Etc/GMT+12"
+#	},
+# This can be merged into findtimezone.c; however, don't just blindly
+# replace the contents of the win32_tzmap[] array.  We don't want to
+# drop any obsolete Windows zone names, since initdb might get run on
+# a machine whose timezone registry isn't up to date.  Therefore, check
+# for zone names that are no longer present, and move those entries to
+# the bottom of the array.
+#
+# To ease making that check manually, the output of this program is
+# sorted by Windows zone name by default.
+#
+#
+# Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+# Portions Copyright (c) 1994, Regents of the University of California
+#
+# src/tools/gen_win32_tzmap.pl
+#
+#----------------------------------------------------------------------
+
+use strict;
+use warnings FATAL => 'all';
+use Getopt::Long;
+
+my $output_file = 'tzmap.tmp.c';
+my $sort_rule = 'stdname';
+
+GetOptions(
+	'output:s' => \$output_file,
+	'sort:s' => \$sort_rule) || usage();
+
+my $cldr_input_file = shift @ARGV || die "No input file.\n";
+
+open(my $xmlif, '<', $cldr_input_file) || die "$cldr_input_file: $!\n";
+open(my $outfile, '>', $output_file) || die "$output_file: $!\n";
+
+# Parse input file to extract data.
+my @timezones;
+my $last_comment = '';
+my $okay = 1;
+while (<$xmlif>)
+{
+	if (/^\s*<!-- (.*?) -->/)
+	{
+		$last_comment = $1;
+	}
+	elsif (/^\s*<mapZone/)
+	{
+		if (m|^\s*<mapZone\s+other="(.*)"\s+territory="(.*)"\s+type="(.*)"/>\s*$|
+		  )
+		{
+			my $stdname = $1;
+			my $territory = $2;
+			my $zone = $3;
+			next if $territory ne "001";
+			# print "$stdname => $zone ($last_comment)\n";
+			push @timezones,
+			  {
+				stdname => $stdname,
+				zone => $zone,
+				comment => $last_comment
+			  };
+		}
+		else
+		{
+			print "unintelligible mapZone: $_";
+			$okay = 0;
+		}
+		$last_comment = '';
+	}
+	elsif (!m/^\s*$/)
+	{
+		# useful for debugging:
+		# print "ignoring line: $_";
+	}
+}
+
+exit 1 unless $okay;
+
+if ($sort_rule eq 'stdname')
+{
+	@timezones = sort { lc($a->{stdname}) cmp lc($b->{stdname}) } @timezones;
+}
+elsif ($sort_rule eq 'zone')
+{
+	@timezones = sort { $a->{zone} cmp $b->{zone} } @timezones;
+}
+elsif ($sort_rule eq 'comment')
+{
+	@timezones = sort { $a->{comment} cmp $b->{comment} } @timezones;
+}
+
+for my $timezone (@timezones)
+{
+	my $stdname = ${$timezone}{'stdname'};
+	my $dayname = $stdname;
+	$dayname =~ s/Standard/Daylight/;
+	my $zone = ${$timezone}{'zone'};
+	my $comment = ${$timezone}{'comment'};
+
+	print $outfile "\t{\n";
+	print $outfile "\t\t/* $comment */\n";
+	print $outfile "\t\t\"$stdname\", \"$dayname\",\n";
+	print $outfile "\t\t\"$zone\"\n";
+	print $outfile "\t},\n";
+}
+
+exit 0;
+
+
+sub usage
+{
+	die <<EOM;
+Usage: gen_win32_tzmap.pl [--output/-o <file>] [--sort/-s <rule>] input_file
+    --output        Output file (default 'tzmap.tmp.c')
+    --sort          Sort rule: stdname,zone,comment,none (default 'stdname')
+
+gen_win32_tzmap.pl transforms windowsZones.xml into constant data
+for findtimezone.c.
+EOM
+}
-- 
2.52.0

