carlos / platform Public

Clone
git clone https://amadan.net/carlos/platform

Plain git — no account needed to clone.

Download

Download this file

1package geo
2
3import (
4 _ "embed"
5 "regexp"
6 "strconv"
7 "strings"
8 "sync"
9)
10
11//go:embed data/zone.tab
12var zoneTab string
13
14//go:embed data/links.txt
15var linksTxt string
16
17//go:embed data/VERSION
18var tzVersion string
19
20// TZDataVersion is the IANA release the embedded tables came from.
21func TZDataVersion() string { return strings.TrimSpace(tzVersion) }
22
23var (
24 tzOnce sync.Once
25 zonePoints map[string]Point // lowercased zone name -> point
26 zoneLinks map[string]string
27 countryOne map[string]Point // countries with exactly one row
28 rowsPerCC map[string]int
29)
30
31// zoneShape is spec §6.2 step 1: at most 64 bytes, one to three
32// "/"-separated segments of [A-Za-z0-9_+-].
33var zoneShape = regexp.MustCompile(`^[A-Za-z0-9_+\-]+(/[A-Za-z0-9_+\-]+){0,2}$`)
34
35// notAPlace names an offset or a machine, not a place (R3).
36var notAPlace = map[string]bool{
37 "": true, "local": true, "localtime": true, "posixrules": true,
38 "utc": true, "uct": true, "gmt": true, "universal": true, "zulu": true,
39 "factory": true,
40}
41
42func loadTZ() {
43 zonePoints = map[string]Point{}
44 countryOne = map[string]Point{}
45 rowsPerCC = map[string]int{}
46 for _, line := range strings.Split(zoneTab, "\n") {
47 if line == "" || strings.HasPrefix(line, "#") {
48 continue
49 }
50 f := strings.Split(line, "\t")
51 if len(f) < 3 {
52 continue
53 }
54 p, ok := parseISO6709(f[1])
55 if !ok {
56 continue
57 }
58 zonePoints[strings.ToLower(f[2])] = p
59 cc := strings.ToUpper(f[0])
60 rowsPerCC[cc]++
61 countryOne[cc] = p
62 }
63 zoneLinks = map[string]string{}
64 for _, line := range strings.Split(linksTxt, "\n") {
65 f := strings.Fields(line)
66 if len(f) == 2 {
67 zoneLinks[strings.ToLower(f[1])] = strings.ToLower(f[0])
68 }
69 }
70}
71
72// ZonePoint maps an IANA zone name to its principal location, following
73// tzdata links (at most 4 hops). It refuses anything that is not a place.
74func ZonePoint(name string) (Point, bool) {
75 tzOnce.Do(loadTZ)
76 name = strings.TrimSpace(name)
77 if len(name) > 64 || !zoneShape.MatchString(name) {
78 return Point{}, false
79 }
80 low := strings.ToLower(name)
81 if notAPlace[low] || strings.HasPrefix(low, "etc/") {
82 return Point{}, false
83 }
84 return resolveLinks(low, zoneLinks, zonePoints)
85}
86
87func resolveLinks(name string, links map[string]string, points map[string]Point) (Point, bool) {
88 for hop := 0; hop <= 4; hop++ {
89 if p, ok := points[name]; ok {
90 return p, true
91 }
92 next, ok := links[name]
93 if !ok {
94 return Point{}, false
95 }
96 name = next
97 }
98 return Point{}, false
99}
100
101// countryOverride names the principal zone of every country with more
102// than one zone.tab row (tzdata 2026b). TestCountryOverridesCoverEvery
103// MultiZoneCountry fails when a refresh adds a multi-row country.
104var countryOverride = map[string]string{
105 "AQ": "Antarctica/McMurdo", "AR": "America/Argentina/Buenos_Aires",
106 "AU": "Australia/Sydney", "BR": "America/Sao_Paulo", "CA": "America/Toronto",
107 "CD": "Africa/Kinshasa", "CL": "America/Santiago", "CN": "Asia/Shanghai",
108 "CY": "Asia/Nicosia", "DE": "Europe/Berlin", "EC": "America/Guayaquil",
109 "ES": "Europe/Madrid", "FM": "Pacific/Pohnpei", "GL": "America/Nuuk",
110 "ID": "Asia/Jakarta", "KI": "Pacific/Tarawa", "KZ": "Asia/Almaty",
111 "MH": "Pacific/Majuro", "MN": "Asia/Ulaanbaatar", "MX": "America/Mexico_City",
112 "MY": "Asia/Kuala_Lumpur", "NZ": "Pacific/Auckland", "PF": "Pacific/Tahiti",
113 "PG": "Pacific/Port_Moresby", "PS": "Asia/Gaza", "PT": "Europe/Lisbon",
114 "RU": "Europe/Moscow", "UA": "Europe/Kyiv", "UM": "Pacific/Wake",
115 "US": "America/New_York", "UZ": "Asia/Tashkent",
116}
117
118// CountryPoint maps an ISO 3166-1 alpha-2 code to a point: its only
119// zone.tab row, or its override's zone.
120func CountryPoint(iso2 string) (Point, bool) {
121 tzOnce.Do(loadTZ)
122 cc := strings.ToUpper(strings.TrimSpace(iso2))
123 if zone, ok := countryOverride[cc]; ok {
124 return ZonePoint(zone)
125 }
126 if rowsPerCC[cc] == 1 {
127 return countryOne[cc], true
128 }
129 return Point{}, false
130}
131
132func zoneRowsPerCountry() map[string]int { tzOnce.Do(loadTZ); return rowsPerCC }
133
134// parseISO6709 reads zone.tab's ±DDMM±DDDMM or ±DDMMSS±DDDMMSS.
135func parseISO6709(s string) (Point, bool) {
136 i := strings.IndexAny(s[1:], "+-") + 1
137 if i <= 0 {
138 return Point{}, false
139 }
140 lat, ok1 := dms(s[:i], 2)
141 lon, ok2 := dms(s[i:], 3)
142 return Point{lat, lon}, ok1 && ok2
143}
144
145func dms(s string, degDigits int) (float64, bool) {
146 sign := 1.0
147 if s[0] == '-' {
148 sign = -1
149 }
150 d := s[1:]
151 if len(d) != degDigits+2 && len(d) != degDigits+4 {
152 return 0, false
153 }
154 deg, err1 := strconv.Atoi(d[:degDigits])
155 min, err2 := strconv.Atoi(d[degDigits : degDigits+2])
156 sec := 0
157 var err3 error
158 if len(d) == degDigits+4 {
159 sec, err3 = strconv.Atoi(d[degDigits+2:])
160 }
161 if err1 != nil || err2 != nil || err3 != nil {
162 return 0, false
163 }
164 return sign * (float64(deg) + float64(min)/60 + float64(sec)/3600), true
165}
166