···11-#!/usr/bin/env python3
22-"""Generate C timezone list for the Top-1000 airports.
33-44-1. Parse `top1000.html` (downloaded from GetToCenter) to extract IATA codes and
55- airport names.
66-2. Use the `airportsdata` package to obtain the IANA timezone (`tz`) for each
77- airport.
88-3. For each distinct set of (std_offset, dst_offset, dst_start, dst_end)
99- belonging to that timezone (for the current year), build a bucket of airport
1010- IATA codes.
1111-4. Emit a C source file (`src/c/airport_tz_list.c`) that mirrors the structure
1212- of `tz_list.c` already used by the Closest-Noon clock, but with **airport
1313- IATA codes** in the pooled name list instead of city names.
1414-1515-Usage:
1616- # Always parse HTML top1000.html, then fallback for missing offsets
1717- python generate_airport_tz_list.py --html top1000.html --out src/c/airport_tz_list.c --top 10 --max-bucket 1000
1818-"""
1919-from __future__ import annotations
2020-2121-import argparse
2222-import sys
2323-from pathlib import Path
2424-from datetime import datetime, timezone
2525-from typing import Dict, List, Tuple
2626-from tz_common import find_dst_transitions as _find_dst_transitions
2727-from bs4 import BeautifulSoup # type: ignore
2828-import airportsdata
2929-from timezonefinder import TimezoneFinder
3030-import pandas as pd
3131-3232-# ---------------------------------------------------------------------------
3333-# Build ranked list of airports with route counts (fallback if HTML omitted)
3434-# ---------------------------------------------------------------------------
3535-3636-def _download_routes_csv() -> pd.DataFrame:
3737- """Fetch routes.dat from the OpenFlights repo and return a DataFrame."""
3838- url = "https://raw.githubusercontent.com/jpatokal/openflights/master/data/routes.dat"
3939- df = pd.read_csv(url, header=None, usecols=[2, 4], names=["src", "dst"], dtype=str)
4040- return df
4141-4242-def _rank_airports_by_routes(airport_df: pd.DataFrame) -> pd.Series:
4343- """Return Series indexed by IATA with descending route hit counts."""
4444- routes = _download_routes_csv()
4545- counts = pd.concat([routes["src"], routes["dst"]]).value_counts()
4646- return counts
4747-4848-def build_topN_per_timezone(top_n: int) -> List[Tuple[str, str]]:
4949- """Return a balanced list covering all timezones with up to top_n airports each.
5050-5151- The ranking metric is route_hits (descending). Airports lacking route data
5252- default to zero, but they might still be picked to cover empty timezones.
5353- """
5454- adict = airportsdata.load("IATA")
5555- # Build DataFrame and remove record-level 'iata' column to avoid duplicates
5656- df = pd.DataFrame.from_dict(adict, orient="index")
5757- if 'iata' in df.columns:
5858- df = df.drop(columns=['iata'])
5959- df = df.reset_index().rename(columns={'index': 'iata'})
6060-6161- # Add route_hits counts
6262- counts = _rank_airports_by_routes(df)
6363- df["route_hits"] = df["iata"].map(counts).fillna(0).astype(int)
6464-6565- # Sort by route_hits descending
6666- df_sorted = df.sort_values("route_hits", ascending=False, ignore_index=True)
6767-6868- tz_to_codes: Dict[str, List[Tuple[str, str]]] = {}
6969-7070- # First pass: iterate sorted df to fill up to top_n per tz
7171- for _, row in df_sorted.iterrows():
7272- tz = row["tz"]
7373- if not isinstance(tz, str) or tz == "":
7474- continue
7575- lst = tz_to_codes.setdefault(tz, [])
7676- if len(lst) < top_n:
7777- lst.append((row["iata"], row["name"]))
7878- # Early exit optimisation – if all tz have top_n we can break; but we
7979- # don't know total tz count easily, so skip.
8080-8181- # Now build final list
8282- final_list: List[Tuple[str, str]] = []
8383- for codes in tz_to_codes.values():
8484- final_list.extend(codes)
8585- return final_list
8686-8787-# ---------------------------------------------------------------------------
8888-# Parsing HTML (optional) ---------------------------------------------------
8989-# ---------------------------------------------------------------------------
9090-9191-def _parse_top1000(html_path: Path) -> List[Tuple[str, str]]:
9292- """Return list of (IATA, Airport Name) found in the HTML table."""
9393- soup = BeautifulSoup(html_path.read_text(encoding="utf-8"), "html.parser")
9494- rows = soup.find_all("tr")
9595- results: List[Tuple[str, str]] = []
9696- for tr in rows:
9797- tds = tr.find_all("td")
9898- if len(tds) < 3:
9999- continue
100100- iata = tds[2].get_text(strip=True).upper()
101101- if not (iata and len(iata) == 3):
102102- continue # skip ads rows etc.
103103- # Airport name usually inside the 2nd <td>, perhaps in an <h2>
104104- name_cell_text = tds[1].get_text(" ", strip=True)
105105- results.append((iata, name_cell_text))
106106- return results
107107-108108-# ---------------------------------------------------------------------------
109109-# Main C-code generation routine
110110-# ---------------------------------------------------------------------------
111111-112112-def generate_c_code(airports_list: List[Tuple[str, str]], out_path: Path, group_size: int = 0, max_bucket: int = 0) -> None:
113113- """Generate airport_tz_list.c:
114114- 1) Build full buckets for every IATA tz variant (std, dst, transitions).
115115- 2) Pick top group_size codes per std-offset from HTML list.
116116- 3) Fallback for missing offsets (min 1, max max_bucket) using classification + traffic.
117117- 4) Distribute codes evenly across DST buckets, cap each to max_bucket.
118118- """
119119- year = datetime.now(timezone.utc).year
120120- airport_db = airportsdata.load("IATA")
121121-122122- # Ensure unique HTML airport entries by IATA code
123123- seen_iatas: set[str] = set()
124124- unique_airports: List[Tuple[str, str]] = []
125125- for iata, name in airports_list:
126126- if iata not in seen_iatas:
127127- unique_airports.append((iata, name))
128128- seen_iatas.add(iata)
129129- airports_list = unique_airports
130130- # Build fallback DataFrame with classification and traffic for missing offsets
131131- df_all = pd.DataFrame.from_dict(airport_db, orient="index")
132132- if 'iata' in df_all.columns:
133133- df_all = df_all.drop(columns=['iata'])
134134- df_all = df_all.reset_index().rename(columns={'index': 'iata'})
135135- # Recompute tz field from lat/lon to correct misclassified zones _before_ we
136136- # derive any offset‑related columns (important for DUT / America/Adak etc.)
137137- tf = TimezoneFinder()
138138- df_all['tz'] = df_all.apply(
139139- lambda row: tf.timezone_at(lat=row.get('lat'), lng=row.get('lon')) or row.get('tz'),
140140- axis=1,
141141- )
142142-143143- # Merge OurAirports classification
144144- try:
145145- oa = pd.read_csv("https://ourairports.com/data/airports.csv", usecols=["iata_code","type","scheduled_service"]) # type: ignore
146146- oa = oa.rename(columns={"iata_code": "iata"}).dropna(subset=["iata"])
147147- df_all = df_all.merge(oa[['iata','type','scheduled_service']], on='iata', how='left')
148148- except Exception:
149149- df_all['type'] = None
150150- df_all['scheduled_service'] = None
151151- # Add route hit counts
152152- traffic_counts = _rank_airports_by_routes(df_all)
153153- traffic_dict = traffic_counts.to_dict()
154154- # Map route hits using apply to ensure a Series
155155- df_all['route_hits'] = df_all['iata'].apply(lambda x: traffic_dict.get(x, 0)).astype(int)
156156- # Compute standard offset seconds for each record (now that tz is fixed)
157157- df_all['std_offset_s'] = df_all['tz'].apply(lambda tz: _find_dst_transitions(tz, year)[0])
158158-159159- # Fallback selector for a given std_offset
160160- def _fallback_codes(std_s: int) -> List[str]:
161161- """Fallback hierarchy per std offset:
162162- 1) up to max_bucket (or 3) large/international,
163163- 2) up to 2 medium/regional,
164164- 3) up to 1 small_airport,
165165- 4) fill any remaining to reach at least 1, max_bucket total."""
166166- seg = df_all[df_all['std_offset_s'] == std_s]
167167- if seg.empty:
168168- return []
169169- seg_sorted = seg.sort_values('route_hits', ascending=False)
170170- result: List[str] = []
171171- # 1) large_international
172172- large = seg_sorted[(seg_sorted['type'] == 'large_airport') & (seg_sorted['scheduled_service'] == 'yes')]
173173- if not large.empty:
174174- cap = max_bucket if max_bucket > 0 else 3
175175- result = large['iata'].head(cap).tolist()
176176- # 2) medium_regional
177177- remain = (max_bucket - len(result)) if max_bucket > 0 else (3 - len(result))
178178- if remain > 0:
179179- medium = seg_sorted[(seg_sorted['type'] == 'medium_airport') & (seg_sorted['scheduled_service'] == 'yes')]
180180- if not medium.empty:
181181- mcap = min(remain, 2)
182182- result.extend(medium['iata'].head(mcap).tolist())
183183- remain = (max_bucket - len(result)) if max_bucket > 0 else (3 - len(result))
184184- # 3) small_airport
185185- if remain > 0:
186186- small = seg_sorted[(seg_sorted['type'] == 'small_airport') & (seg_sorted['scheduled_service'] == 'yes')]
187187- if not small.empty:
188188- result.extend(small['iata'].head(1).tolist())
189189- remain = (max_bucket - len(result)) if max_bucket > 0 else (3 - len(result))
190190- # 4) any to ensure at least one
191191- if not result:
192192- result = [seg_sorted['iata'].iloc[0]]
193193- # enforce max_bucket hard limit
194194- if max_bucket > 0 and len(result) > max_bucket:
195195- result = result[:max_bucket]
196196- return result
197197-198198- # 1) Build full buckets from all tz names in df_all
199199- full_buckets: Dict[Tuple[int,int,int,int], Dict[str, object]] = {}
200200- group_keys: Dict[int, List[Tuple[int,int,int,int]]] = {}
201201- for tz_name in df_all['tz'].dropna().unique():
202202- std_s, dst_s, start_ts, end_ts = _find_dst_transitions(tz_name, year)
203203- key = (std_s, dst_s, start_ts, end_ts)
204204- if key not in full_buckets:
205205- full_buckets[key] = {
206206- 'std': std_s,
207207- 'dst': dst_s,
208208- 'start': start_ts,
209209- 'end': end_ts,
210210- 'tz_names': [tz_name],
211211- }
212212- group_keys.setdefault(std_s, []).append(key)
213213- else:
214214- full_buckets[key]['tz_names'].append(tz_name)
215215-216216- # 2) Collect codes from HTML for each std_offset (popular timezones)
217217- group_codes: Dict[int, List[str]] = {}
218218- for iata, _ in airports_list:
219219- rec = airport_db.get(iata)
220220- if not rec or not rec.get('tz'):
221221- continue
222222- std_s = _find_dst_transitions(rec['tz'], year)[0]
223223- codes = group_codes.setdefault(std_s, [])
224224- if iata not in codes:
225225- codes.append(iata)
226226- # Trim HTML-based codes to group_size for popular timezones
227227- if group_size > 0:
228228- for std_s, codes in list(group_codes.items()):
229229- group_codes[std_s] = codes[:group_size]
230230-231231- # 3) Fallback for offsets lacking HTML codes (unpopular timezones)
232232- for std_s, keys in group_keys.items():
233233- # ensure at least one code per std_offset
234234- if not group_codes.get(std_s):
235235- group_codes[std_s] = _fallback_codes(std_s)
236236-237237- # 4) Assign popular & fallback codes to their actual DST buckets
238238- # initialize codes list for each bucket
239239- for bucket_key, meta in full_buckets.items():
240240- meta['codes'] = []
241241- used_codes: set[str] = set()
242242-243243- def _assign_to_bucket(iata_code: str):
244244- if iata_code in used_codes:
245245- return False
246246- rec = airport_db.get(iata_code)
247247- if rec and rec.get('tz'):
248248- std2, dst2, st2, ed2 = _find_dst_transitions(rec['tz'], year)
249249- key = (std2, dst2, st2, ed2)
250250- if key in full_buckets:
251251- full_buckets[key]['codes'].append(iata_code)
252252- used_codes.add(iata_code)
253253- return True
254254- return False
255255-256256- for std_s, codes in group_codes.items():
257257- for iata in codes:
258258- _assign_to_bucket(iata)
259259-260260- # fallback for buckets still empty: only populate the first empty bucket per std-offset
261261- for std_s, keys in group_keys.items():
262262- assigned = set(group_codes.get(std_s, []))
263263- fallback_candidates = [c for c in group_codes.get(std_s, []) if c not in used_codes]
264264- # track if we've used fallback for this std-offset
265265- used = False
266266- for bucket_key in keys:
267267- codes_list = full_buckets[bucket_key].get('codes', [])
268268- if not codes_list and not used:
269269- # pick first unassigned fallback candidates
270270- fallback_pool = [c for c in group_codes.get(std_s, []) if c not in used_codes]
271271- for candidate in fallback_pool:
272272- if _assign_to_bucket(candidate):
273273- codes_list = [candidate]
274274- break
275275- else:
276276- codes_list = []
277277- used = True
278278- full_buckets[bucket_key]['codes'] = codes_list
279279-280280- # FINAL safety pass: if a bucket is still empty try to grab 1 airport that
281281- # actually sits in *this* timezone (e.g. DUT for America/Adak). This never
282282- # duplicates because we consult used_codes.
283283- for bucket_key, meta in full_buckets.items():
284284- if meta['codes']:
285285- continue
286286- tz_names = meta.get('tz_names', [])
287287- if not tz_names:
288288- continue
289289- seg = df_all[df_all['tz'].isin(tz_names)].sort_values('route_hits', ascending=False)
290290- for code in seg['iata']:
291291- if _assign_to_bucket(code):
292292- meta['codes'] = [code]
293293- break
294294-295295- # build ordered bucket list
296296- buckets_list = [
297297- full_buckets[k]
298298- for k in sorted(
299299- full_buckets.keys(),
300300- key=lambda k: (full_buckets[k]['std'], full_buckets[k]['dst'], full_buckets[k]['start'])
301301- )
302302- ]
303303-304304- # 5) Build flat pool and offsets
305305- code_pool = []
306306- seen_for_pool: set[str] = set()
307307- for b in buckets_list:
308308- unique_codes = [c for c in b.get('codes', []) if c not in seen_for_pool]
309309- b['offset'] = len(code_pool)
310310- b['count'] = len(unique_codes)
311311- code_pool.extend(unique_codes)
312312- seen_for_pool.update(unique_codes)
313313-314314- # Build name pool parallel to code_pool
315315- name_pool = []
316316- for code in code_pool:
317317- rec = airport_db.get(code)
318318- if rec and rec.get('name'):
319319- name = rec['name']
320320- else:
321321- name = code
322322- # Remove ' International Airport' or ' Airport' from the end
323323- if name.endswith(' International Airport'):
324324- name = name[:-len(' International Airport')]
325325- elif name.endswith(' Airport'):
326326- name = name[:-len(' Airport')]
327327- name = name.rstrip()
328328- name_pool.append(name)
329329-330330- # Emit C file
331331- with out_path.open("w", encoding="utf-8") as f:
332332- f.write("// Auto-generated by generate_airport_tz_list.py\n")
333333- f.write(f"// Year-specific DST data for {year}\n\n")
334334- f.write("#include <stdint.h>\n\n")
335335- # Code pool
336336- f.write("static const char airport_code_pool[] =\n")
337337- for i, code in enumerate(code_pool):
338338- if i % 8 == 0:
339339- f.write(" ") # Indent new line
340340- f.write(f'"{code}"')
341341- if (i + 1) % 8 == 0 or (i + 1) == len(code_pool):
342342- f.write("\n") # Newline every 8 codes or at the end
343343- else:
344344- f.write(" ") # Space between codes on the same line
345345- f.write(";\n\n")
346346-347347- # Name pool
348348- f.write("static const char* airport_name_pool[] = {\n")
349349- for name in name_pool:
350350- f.write(f" \"{name}\",\n")
351351- f.write("};\n\n")
352352-353353- # Struct matches TzInfo definition
354354- f.write("typedef struct {\n")
355355- f.write(" float std_offset_hours;\n")
356356- f.write(" float dst_offset_hours;\n")
357357- f.write(" int64_t dst_start_utc;\n")
358358- f.write(" int64_t dst_end_utc;\n")
359359- f.write(" int name_offset;\n")
360360- f.write(" int name_count;\n")
361361- f.write("} TzInfo;\n\n")
362362-363363- f.write("static const TzInfo airport_tz_list[] = {\n")
364364- for bucket in buckets_list:
365365- std_h = bucket["std"] / 3600.0
366366- dst_h = bucket["dst"] / 3600.0
367367- start = bucket["start"]
368368- end = bucket["end"]
369369- off = bucket["offset"]
370370- cnt = bucket["count"]
371371- f.write(f" {{ {std_h:.2f}f, {dst_h:.2f}f, {start}LL, {end}LL, {off}, {cnt} }},\n")
372372- f.write("};\n\n")
373373- f.write("#define AIRPORT_TZ_LIST_COUNT (sizeof(airport_tz_list)/sizeof(airport_tz_list[0]))\n")
374374- f.write("#define AIRPORT_CODE_POOL_COUNT (sizeof(airport_code_pool)/3)\n")
375375- f.write("#define AIRPORT_NAME_POOL_COUNT (sizeof(airport_name_pool)/sizeof(airport_name_pool[0]))\n")
376376-377377- print(
378378- f"Generated {out_path} with {len(buckets_list)} tz buckets and {len(code_pool)} airports."
379379- )
380380-381381-# ---------------------------------------------------------------------------
382382-# CLI
383383-# ---------------------------------------------------------------------------
384384-385385-def main(argv: List[str] | None = None) -> None:
386386- parser = argparse.ArgumentParser(
387387- description="Generate airport_tz_list.c: hybrid grouping by standard offset, split DST buckets, fallback for missing offsets"
388388- )
389389-390390- default_html_path = Path(__file__).parent / "top1000.html"
391391- parser.add_argument(
392392- "--html",
393393- type=Path,
394394- default=default_html_path,
395395- help="Path to GetToCenter HTML file (top1000.html)",
396396- )
397397-398398- default_out_path = Path(__file__).parent / "../src/c/airport_tz_list.c"
399399- parser.add_argument(
400400- "--out",
401401- type=Path,
402402- default=default_out_path,
403403- help="C output file path"
404404- )
405405- parser.add_argument(
406406- "--top",
407407- type=int,
408408- default=10,
409409- help="Number of airports to pick per standard offset group before splitting across DST buckets",
410410- )
411411- parser.add_argument(
412412- "--max-bucket",
413413- type=int,
414414- default=10,
415415- help="Maximum number of airport codes to include per DST bucket (default: 10)",
416416- )
417417- args = parser.parse_args(argv)
418418-419419- # Always parse the HTML source for list of top airports
420420- if not args.html.exists():
421421- print(f"ERROR: HTML file not found: {args.html}", file=sys.stderr)
422422- sys.exit(1)
423423- airports_list = _parse_top1000(args.html)
424424- if not airports_list:
425425- print(f"ERROR: No airports found in HTML: {args.html}", file=sys.stderr)
426426- sys.exit(1)
427427-428428- # group_size = top N per std-offset, max_bucket = cap per DST bucket
429429- generate_c_code(airports_list, args.out, group_size=args.top, max_bucket=args.max_bucket)
430430-431431-432432-if __name__ == "__main__":
433433- main()
-129
scripts/generate_tz_list.py
···11-# Requires Python 3.9+ for zoneinfo
22-import zoneinfo
33-from datetime import datetime
44-from os import path
55-# Use shared DST tools
66-from tz_common import find_dst_transitions as find_dst_transitions_accurate
77-88-def generate_tz_list_c_code():
99- """Generates C code for a static timezone list with DST transition timestamps."""
1010-1111- target_year = datetime.now().year # Use current year for transitions
1212- print(f"Finding DST transitions for year {target_year}...")
1313-1414- available_zones = zoneinfo.available_timezones()
1515- print(f"Found {len(available_zones)} available timezones.")
1616-1717- processed_zones = {} # Key: TUPLE(std_offset_s, dst_offset_s, start_utc, end_utc), Value: Dict of zone data
1818-1919- for tz_name in available_zones:
2020- # Basic filtering (no dead code here)
2121- if tz_name.startswith("Etc/") or "/" not in tz_name: continue
2222- if tz_name in ["Factory", "factory"] or tz_name.lower().startswith("right/") or tz_name.lower().startswith("posix/"): continue
2323-2424- std_offset_s, dst_offset_s, start_utc, end_utc = find_dst_transitions_accurate(tz_name, target_year)
2525- city_name = tz_name.split('/')[-1].replace('_', ' ')
2626-2727- # --- Filter out generic names ---
2828- # Comprehensive list based on review of tz_list.c
2929- generic_names_to_exclude = {
3030- "Samoa", "Hawaii", "Aleutian", "Alaska", "Pacific", "Arizona", "Yukon",
3131- "Mountain", "General", "Saskatchewan", "Central", "Knox IN", "EasterIsland",
3232- "Acre", "Jamaica", "Michigan", "Eastern", "East-Indiana", "Atlantic",
3333- "Continental", "Newfoundland", "East", "Bahia", "Noronha", "South Georgia",
3434- "Canary", "Faeroe", "Faroe", "Guernsey", "Isle of Man", "Jersey",
3535- "Madeira", "Jan Mayen", "West", "North", "South", "ACT", "NSW",
3636- "Tasmania", "Victoria", "Queensland", "Yap", "South Pole", "Kanton",
3737- # Add or remove names as needed
3838- }
3939- # Case-insensitive check for exclusion
4040- if city_name.lower() in {name.lower() for name in generic_names_to_exclude}:
4141- continue # Skip this generic name
4242-4343- # Convert offsets back to hours for potential display, but keep seconds for key
4444- std_offset_h = std_offset_s / 3600.0
4545- dst_offset_h = dst_offset_s / 3600.0
4646-4747- # Group by the unique combination of std offset, dst offset, and transitions
4848- key_tuple = (std_offset_s, dst_offset_s, start_utc, end_utc)
4949- if city_name and city_name[0].isupper():
5050- if key_tuple not in processed_zones:
5151- processed_zones[key_tuple] = {
5252- "std_offset_s": std_offset_s, # Store seconds internally
5353- "dst_offset_s": dst_offset_s,
5454- "start_utc": start_utc,
5555- "end_utc": end_utc,
5656- "names": []
5757- }
5858- # Add city name if not already present
5959- if city_name not in processed_zones[key_tuple]["names"]:
6060- processed_zones[key_tuple]["names"].append(city_name)
6161-6262- # Convert dict values to a list and sort by std offset, then DST offset, then by DST start/end to keep consistent ordering
6363- tz_data_list = sorted(
6464- processed_zones.values(),
6565- key=lambda x: (
6666- x["std_offset_s"],
6767- x["dst_offset_s"],
6868- x["start_utc"],
6969- x["end_utc"]
7070- )
7171- )
7272- print(f"Generated data for {len(tz_data_list)} unique offset/DST rule combinations.")
7373-7474- # --- C Code Generation: Flatten name pool and tz_list entries ---
7575- # Build a flat pool of city names and compute offsets
7676- names_pool = []
7777- for zone in tz_data_list:
7878- sorted_names = sorted(zone['names'])
7979- zone['name_offset'] = len(names_pool)
8080- zone['name_count'] = len(sorted_names)
8181- names_pool.extend(sorted_names)
8282-8383- # Begin C output
8484- c_code = "// Generated by Python script using zoneinfo\n"
8585- c_code += f"// Contains Standard & DST offsets for {target_year}.\n"
8686- c_code += "// WARNING: DST rules accurate only for the generated year.\n\n"
8787- c_code += "#include <stdint.h>\n\n"
8888-8989- # Flattened list of all city names
9090- c_code += "static const char* tz_name_pool[] = {\n"
9191- for name in names_pool:
9292- c_code += f" \"{name}\",\n"
9393- c_code += "};\n\n"
9494-9595- # TzInfo struct with name pool indices
9696- c_code += "typedef struct {\n"
9797- c_code += " float std_offset_hours;\n"
9898- c_code += " float dst_offset_hours;\n"
9999- c_code += " int64_t dst_start_utc;\n"
100100- c_code += " int64_t dst_end_utc;\n"
101101- c_code += " int name_offset;\n"
102102- c_code += " int name_count;\n"
103103- c_code += "} TzInfo;\n\n"
104104-105105- # Main tz_list entries
106106- c_code += "static const TzInfo tz_list[] = {\n"
107107- for zone in tz_data_list:
108108- std_h = zone['std_offset_s'] / 3600.0
109109- dst_h = zone['dst_offset_s'] / 3600.0
110110- start = zone['start_utc']
111111- end = zone['end_utc']
112112- offs = zone['name_offset']
113113- cnt = zone['name_count']
114114- c_code += f" {{ {std_h:.2f}f, {dst_h:.2f}f, {start}LL, {end}LL, {offs}, {cnt} }},\n"
115115- c_code += "};\n\n"
116116- c_code += f"#define TZ_LIST_COUNT (sizeof(tz_list)/sizeof(tz_list[0]))\n"
117117- c_code += f"#define TZ_NAME_POOL_COUNT (sizeof(tz_name_pool)/sizeof(tz_name_pool[0]))\n"
118118- return c_code
119119-120120-# --- Main execution ---
121121-if __name__ == "__main__":
122122- c_code_output = generate_tz_list_c_code()
123123- output_filename = path.join(path.dirname(__file__), "../src/c/tz_list.c") # Default output path
124124- try:
125125- with open(output_filename, "w") as f:
126126- f.write(c_code_output)
127127- print(f"\nSuccessfully written timezone data (with accurate DST timestamps) to {output_filename}")
128128- except IOError as e:
129129- print(f"\nError: Could not write to file {output_filename}: {e}")