EventNotifier should show dates/times in the client's timezone
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,6 @@
|
|||||||
#include <sourcemod>
|
#include <sourcemod>
|
||||||
#include <ripext>
|
#include <ripext>
|
||||||
|
#include <geoip>
|
||||||
|
|
||||||
#define EVENTNOTIFIER_CORE
|
#define EVENTNOTIFIER_CORE
|
||||||
#include <EventNotifier>
|
#include <EventNotifier>
|
||||||
@@ -7,11 +8,22 @@
|
|||||||
#pragma semicolon 1
|
#pragma semicolon 1
|
||||||
#pragma newdecls required
|
#pragma newdecls required
|
||||||
|
|
||||||
#define PLUGIN_VERSION "1.0"
|
#define PLUGIN_VERSION "1.1"
|
||||||
|
|
||||||
|
#define TIMEZONE_FILE "configs/eventnotifier_timezones.cfg"
|
||||||
|
#define TIMEZONE_NAME_LEN 64
|
||||||
|
|
||||||
|
enum struct TzPeriod
|
||||||
|
{
|
||||||
|
int start; // unix timestamp this offset applies from
|
||||||
|
int offset; // seconds east of UTC
|
||||||
|
char abbr[12];
|
||||||
|
}
|
||||||
|
|
||||||
ConVar g_cvUrl;
|
ConVar g_cvUrl;
|
||||||
ConVar g_cvServer;
|
ConVar g_cvServer;
|
||||||
ConVar g_cvInterval;
|
ConVar g_cvInterval;
|
||||||
|
ConVar g_cvFallbackZone;
|
||||||
|
|
||||||
ArrayList g_Events;
|
ArrayList g_Events;
|
||||||
Handle g_hRefreshTimer;
|
Handle g_hRefreshTimer;
|
||||||
@@ -20,6 +32,10 @@ bool g_bEverFetched;
|
|||||||
|
|
||||||
GlobalForward g_fwdOnEventsUpdated;
|
GlobalForward g_fwdOnEventsUpdated;
|
||||||
|
|
||||||
|
// Zone name -> ArrayList of TzPeriod, ascending by start.
|
||||||
|
StringMap g_Timezones;
|
||||||
|
char g_sClientZone[MAXPLAYERS + 1][TIMEZONE_NAME_LEN];
|
||||||
|
|
||||||
public Plugin myinfo =
|
public Plugin myinfo =
|
||||||
{
|
{
|
||||||
name = "EventNotifier",
|
name = "EventNotifier",
|
||||||
@@ -36,6 +52,7 @@ public APLRes AskPluginLoad2(Handle myself, bool late, char[] error, int err_max
|
|||||||
CreateNative("EventNotifier_FindEventByNumber", Native_FindEventByNumber);
|
CreateNative("EventNotifier_FindEventByNumber", Native_FindEventByNumber);
|
||||||
CreateNative("EventNotifier_GetEvent", Native_GetEvent);
|
CreateNative("EventNotifier_GetEvent", Native_GetEvent);
|
||||||
CreateNative("EventNotifier_Refresh", Native_Refresh);
|
CreateNative("EventNotifier_Refresh", Native_Refresh);
|
||||||
|
CreateNative("EventNotifier_FormatClientTime", Native_FormatClientTime);
|
||||||
RegPluginLibrary("EventNotifier");
|
RegPluginLibrary("EventNotifier");
|
||||||
return APLRes_Success;
|
return APLRes_Success;
|
||||||
}
|
}
|
||||||
@@ -51,6 +68,8 @@ public void OnPluginStart()
|
|||||||
"Value of the 'server' query parameter sent to the events API");
|
"Value of the 'server' query parameter sent to the events API");
|
||||||
g_cvInterval = CreateConVar("sm_event_refresh_interval", "1800",
|
g_cvInterval = CreateConVar("sm_event_refresh_interval", "1800",
|
||||||
"How often to re-fetch the event list, in seconds", _, true, 30.0);
|
"How often to re-fetch the event list, in seconds", _, true, 30.0);
|
||||||
|
g_cvFallbackZone = CreateConVar("sm_event_fallback_timezone", "UTC",
|
||||||
|
"Timezone (IANA name, e.g. Europe/London) used for players whose timezone cannot be looked up");
|
||||||
|
|
||||||
g_cvUrl.AddChangeHook(OnSourceChanged);
|
g_cvUrl.AddChangeHook(OnSourceChanged);
|
||||||
g_cvServer.AddChangeHook(OnSourceChanged);
|
g_cvServer.AddChangeHook(OnSourceChanged);
|
||||||
@@ -59,6 +78,31 @@ public void OnPluginStart()
|
|||||||
AutoExecConfig(true, "EventNotifier");
|
AutoExecConfig(true, "EventNotifier");
|
||||||
|
|
||||||
RegAdminCmd("sm_event_refresh", Command_EventRefresh, ADMFLAG_GENERIC, "Re-fetches the event list now");
|
RegAdminCmd("sm_event_refresh", Command_EventRefresh, ADMFLAG_GENERIC, "Re-fetches the event list now");
|
||||||
|
|
||||||
|
LoadTimezones();
|
||||||
|
|
||||||
|
// Late load: look up everyone already on the server.
|
||||||
|
for (int client = 1; client <= MaxClients; client++)
|
||||||
|
{
|
||||||
|
if (IsClientConnected(client))
|
||||||
|
OnClientConnected(client);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void OnClientConnected(int client)
|
||||||
|
{
|
||||||
|
g_sClientZone[client][0] = '\0';
|
||||||
|
if (IsFakeClient(client))
|
||||||
|
return;
|
||||||
|
|
||||||
|
char ip[32];
|
||||||
|
if (GetClientIP(client, ip, sizeof(ip)) && !GeoipTimezone(ip, g_sClientZone[client], sizeof(g_sClientZone[])))
|
||||||
|
g_sClientZone[client][0] = '\0';
|
||||||
|
}
|
||||||
|
|
||||||
|
public void OnClientDisconnect(int client)
|
||||||
|
{
|
||||||
|
g_sClientZone[client][0] = '\0';
|
||||||
}
|
}
|
||||||
|
|
||||||
public void OnConfigsExecuted()
|
public void OnConfigsExecuted()
|
||||||
@@ -131,6 +175,19 @@ public any Native_Refresh(Handle plugin, int numParams)
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public any Native_FormatClientTime(Handle plugin, int numParams)
|
||||||
|
{
|
||||||
|
int client = GetNativeCell(1);
|
||||||
|
if (client < 0 || client > MaxClients)
|
||||||
|
return ThrowNativeError(SP_ERROR_NATIVE, "Invalid client index %d", client);
|
||||||
|
|
||||||
|
int maxlen = GetNativeCell(4);
|
||||||
|
char[] buffer = new char[maxlen];
|
||||||
|
FormatClientTime(client, GetNativeCell(2), buffer, maxlen, GetNativeCell(5));
|
||||||
|
SetNativeString(3, buffer, maxlen);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Fetching
|
// Fetching
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
@@ -318,6 +375,130 @@ int FindEventByNumber(int number)
|
|||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Timezones
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
// Loads the zone table generated by tools/gen_timezones.py.
|
||||||
|
void LoadTimezones()
|
||||||
|
{
|
||||||
|
g_Timezones = new StringMap();
|
||||||
|
|
||||||
|
char path[PLATFORM_MAX_PATH];
|
||||||
|
BuildPath(Path_SM, path, sizeof(path), TIMEZONE_FILE);
|
||||||
|
|
||||||
|
KeyValues kv = new KeyValues("Timezones");
|
||||||
|
if (!kv.ImportFromFile(path))
|
||||||
|
{
|
||||||
|
LogError("Could not read %s, all times will be shown in UTC", path);
|
||||||
|
delete kv;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (kv.GotoFirstSubKey())
|
||||||
|
{
|
||||||
|
do
|
||||||
|
{
|
||||||
|
char zone[TIMEZONE_NAME_LEN];
|
||||||
|
kv.GetSectionName(zone, sizeof(zone));
|
||||||
|
|
||||||
|
ArrayList periods = new ArrayList(sizeof(TzPeriod));
|
||||||
|
if (kv.GotoFirstSubKey(false))
|
||||||
|
{
|
||||||
|
do
|
||||||
|
{
|
||||||
|
char key[16], value[32], parts[2][16];
|
||||||
|
kv.GetSectionName(key, sizeof(key));
|
||||||
|
kv.GetString(NULL_STRING, value, sizeof(value));
|
||||||
|
if (ExplodeString(value, " ", parts, sizeof(parts), sizeof(parts[])) != 2)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
TzPeriod period;
|
||||||
|
period.start = StringToInt(key);
|
||||||
|
period.offset = StringToInt(parts[0]);
|
||||||
|
strcopy(period.abbr, sizeof(period.abbr), parts[1]);
|
||||||
|
periods.PushArray(period);
|
||||||
|
} while (kv.GotoNextKey(false));
|
||||||
|
kv.GoBack();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (periods.Length > 0)
|
||||||
|
{
|
||||||
|
periods.SortCustom(SortPeriodsByStart);
|
||||||
|
g_Timezones.SetValue(zone, periods);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
delete periods;
|
||||||
|
}
|
||||||
|
} while (kv.GotoNextKey());
|
||||||
|
}
|
||||||
|
delete kv;
|
||||||
|
|
||||||
|
LogMessage("Loaded %d timezone(s)", g_Timezones.Size);
|
||||||
|
}
|
||||||
|
|
||||||
|
int SortPeriodsByStart(int index1, int index2, Handle array, Handle hndl)
|
||||||
|
{
|
||||||
|
ArrayList list = view_as<ArrayList>(array);
|
||||||
|
int a = list.Get(index1, TzPeriod::start);
|
||||||
|
int b = list.Get(index2, TzPeriod::start);
|
||||||
|
return (a == b) ? 0 : ((a < b) ? -1 : 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Finds the offset and abbreviation in effect at stamp for the client's zone,
|
||||||
|
// falling back to sm_event_fallback_timezone and then plain UTC.
|
||||||
|
void GetClientZonePeriod(int client, int stamp, TzPeriod out)
|
||||||
|
{
|
||||||
|
ArrayList periods;
|
||||||
|
if (client <= 0 || !g_sClientZone[client][0] || !g_Timezones.GetValue(g_sClientZone[client], periods))
|
||||||
|
{
|
||||||
|
char fallback[TIMEZONE_NAME_LEN];
|
||||||
|
g_cvFallbackZone.GetString(fallback, sizeof(fallback));
|
||||||
|
if (!g_Timezones.GetValue(fallback, periods))
|
||||||
|
periods = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (periods == null)
|
||||||
|
{
|
||||||
|
out.start = 0;
|
||||||
|
out.offset = 0;
|
||||||
|
strcopy(out.abbr, sizeof(out.abbr), "UTC");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
periods.GetArray(0, out);
|
||||||
|
int count = periods.Length;
|
||||||
|
for (int i = 1; i < count && periods.Get(i, TzPeriod::start) <= stamp; i++)
|
||||||
|
periods.GetArray(i, out);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Formats stamp in the client's timezone as "Thu 01 Oct 2026, 20:00 BST"
|
||||||
|
// (or without the year). FormatTime can't be used, it only knows the server's zone.
|
||||||
|
void FormatClientTime(int client, int stamp, char[] buffer, int maxlen, bool withYear)
|
||||||
|
{
|
||||||
|
static const char weekdays[][] = { "Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat" };
|
||||||
|
static const char months[][] = { "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec" };
|
||||||
|
|
||||||
|
TzPeriod period;
|
||||||
|
GetClientZonePeriod(client, stamp, period);
|
||||||
|
|
||||||
|
int local = stamp + period.offset;
|
||||||
|
int days = local / 86400;
|
||||||
|
int secs = local % 86400;
|
||||||
|
|
||||||
|
int year, month, day;
|
||||||
|
CivilFromDays(days, year, month, day);
|
||||||
|
|
||||||
|
// 1970-01-01 was a Thursday.
|
||||||
|
int weekday = (days + 4) % 7;
|
||||||
|
|
||||||
|
if (withYear)
|
||||||
|
Format(buffer, maxlen, "%s %02d %s %d, %02d:%02d %s", weekdays[weekday], day, months[month - 1], year, secs / 3600, (secs % 3600) / 60, period.abbr);
|
||||||
|
else
|
||||||
|
Format(buffer, maxlen, "%s %02d %s, %02d:%02d %s", weekdays[weekday], day, months[month - 1], secs / 3600, (secs % 3600) / 60, period.abbr);
|
||||||
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Time helpers
|
// Time helpers
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
@@ -390,3 +571,17 @@ int DaysFromCivil(int y, int m, int d)
|
|||||||
int doe = yoe * 365 + yoe / 4 - yoe / 100 + doy;
|
int doe = yoe * 365 + yoe / 4 - yoe / 100 + doy;
|
||||||
return era * 146097 + doe - 719468;
|
return era * 146097 + doe - 719468;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Inverse of DaysFromCivil: date for a day count since 1970-01-01.
|
||||||
|
void CivilFromDays(int z, int &y, int &m, int &d)
|
||||||
|
{
|
||||||
|
z += 719468;
|
||||||
|
int era = ((z >= 0) ? z : z - 146096) / 146097;
|
||||||
|
int doe = z - era * 146097;
|
||||||
|
int yoe = (doe - doe / 1460 + doe / 36524 - doe / 146096) / 365;
|
||||||
|
int doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
|
||||||
|
int mp = (5 * doy + 2) / 153;
|
||||||
|
d = doy - (153 * mp + 2) / 5 + 1;
|
||||||
|
m = (mp < 10) ? mp + 3 : mp - 9;
|
||||||
|
y = yoe + era * 400 + ((m <= 2) ? 1 : 0);
|
||||||
|
}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
#pragma semicolon 1
|
#pragma semicolon 1
|
||||||
#pragma newdecls required
|
#pragma newdecls required
|
||||||
|
|
||||||
#define PLUGIN_VERSION "1.0"
|
#define PLUGIN_VERSION "1.1"
|
||||||
|
|
||||||
// Shortest automatic announcement interval, in seconds. Values below this are
|
// Shortest automatic announcement interval, in seconds. Values below this are
|
||||||
// raised to it; 0 or below disables the timer entirely.
|
// raised to it; 0 or below disables the timer entirely.
|
||||||
@@ -114,21 +114,25 @@ bool PostAnnouncement()
|
|||||||
if (!name[0])
|
if (!name[0])
|
||||||
strcopy(name, sizeof(name), "Unnamed event");
|
strcopy(name, sizeof(name), "Unnamed event");
|
||||||
|
|
||||||
int now = GetTime();
|
|
||||||
char span[32];
|
|
||||||
if (ev.start <= 0)
|
if (ev.start <= 0)
|
||||||
{
|
{
|
||||||
CPrintToChatAll("{darkorange}[Events] {yellow}#%d: {lightgreen}%s {grey}date TBA", ev.number, name);
|
CPrintToChatAll("{darkorange}[Events] {yellow}#%d: {lightgreen}%s {grey}date TBA", ev.number, name);
|
||||||
}
|
}
|
||||||
else if (now < ev.start)
|
|
||||||
{
|
|
||||||
EventNotifier_FormatDuration(ev.start - now, span, sizeof(span));
|
|
||||||
CPrintToChatAll("{darkorange}[Events] {yellow}#%d: {lightgreen}%s {lime}starts in %s", ev.number, name, span);
|
|
||||||
}
|
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
EventNotifier_FormatDuration(now - ev.start, span, sizeof(span));
|
// Each player gets the start time in their own timezone.
|
||||||
CPrintToChatAll("{darkorange}[Events] {yellow}#%d: {lightgreen}%s {orchid}started %s ago", ev.number, name, span);
|
bool started = GetTime() >= ev.start;
|
||||||
|
for (int client = 1; client <= MaxClients; client++)
|
||||||
|
{
|
||||||
|
if (!IsClientInGame(client) || IsFakeClient(client))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
char when[48];
|
||||||
|
EventNotifier_FormatClientTime(client, ev.start, when, sizeof(when), false);
|
||||||
|
// CPrintToChatEx rather than CPrintToChat: the latter only accepts string format arguments.
|
||||||
|
CPrintToChatEx(client, client, "{darkorange}[Events] {yellow}#%d: {lightgreen}%s %s %s", ev.number, name,
|
||||||
|
started ? "{orchid}started" : "{white}starts{lime}", when);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (ev.maps[0])
|
if (ev.maps[0])
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
#pragma semicolon 1
|
#pragma semicolon 1
|
||||||
#pragma newdecls required
|
#pragma newdecls required
|
||||||
|
|
||||||
#define PLUGIN_VERSION "1.1"
|
#define PLUGIN_VERSION "1.2"
|
||||||
#define CHAT_PREFIX "\x04[Events]\x01"
|
#define CHAT_PREFIX "\x04[Events]\x01"
|
||||||
|
|
||||||
#define PANEL_LINE_WIDTH 60
|
#define PANEL_LINE_WIDTH 60
|
||||||
@@ -83,9 +83,15 @@ void ShowEventList(int client)
|
|||||||
if (!EventNotifier_GetEvent(i, ev))
|
if (!EventNotifier_GetEvent(i, ev))
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
char info[16], display[EVENT_STR_LEN + 16];
|
char info[16], display[EVENT_STR_LEN + 64];
|
||||||
IntToString(ev.number, info, sizeof(info));
|
IntToString(ev.number, info, sizeof(info));
|
||||||
Format(display, sizeof(display), "%d: %s", ev.number, ev.name);
|
Format(display, sizeof(display), "%d: %s", ev.number, ev.name);
|
||||||
|
if (ev.start > 0)
|
||||||
|
{
|
||||||
|
char when[48];
|
||||||
|
EventNotifier_FormatClientTime(client, ev.start, when, sizeof(when), false);
|
||||||
|
Format(display, sizeof(display), "%s (%s)", display, when);
|
||||||
|
}
|
||||||
menu.AddItem(info, display);
|
menu.AddItem(info, display);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -140,7 +146,7 @@ void ShowEventDetails(int client, int index)
|
|||||||
if (ev.start > 0)
|
if (ev.start > 0)
|
||||||
{
|
{
|
||||||
char when[64];
|
char when[64];
|
||||||
FormatTime(when, sizeof(when), "%a %d %b %Y, %H:%M %Z", ev.start);
|
EventNotifier_FormatClientTime(client, ev.start, when, sizeof(when));
|
||||||
Format(line, sizeof(line), "Date: %s", when);
|
Format(line, sizeof(line), "Date: %s", when);
|
||||||
panel.DrawText(line);
|
panel.DrawText(line);
|
||||||
|
|
||||||
|
|||||||
@@ -60,6 +60,20 @@ native bool EventNotifier_GetEvent(int index, any[] ev, int size = sizeof(EventI
|
|||||||
*/
|
*/
|
||||||
native void EventNotifier_Refresh();
|
native void EventNotifier_Refresh();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Formats a timestamp in the client's own timezone (looked up via GeoIP),
|
||||||
|
* e.g. "Thu 01 Oct 2026, 20:00 BST". Clients whose timezone is unknown get
|
||||||
|
* sm_event_fallback_timezone.
|
||||||
|
*
|
||||||
|
* @param client Client index, 0 for the fallback timezone.
|
||||||
|
* @param timestamp Unix timestamp (UTC).
|
||||||
|
* @param buffer Destination buffer.
|
||||||
|
* @param maxlen Size of buffer.
|
||||||
|
* @param withYear Whether to include the year.
|
||||||
|
* @error Invalid client index.
|
||||||
|
*/
|
||||||
|
native void EventNotifier_FormatClientTime(int client, int timestamp, char[] buffer, int maxlen, bool withYear = true);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Called after every successful fetch, once the event list has been replaced.
|
* Called after every successful fetch, once the event list has been replaced.
|
||||||
*/
|
*/
|
||||||
@@ -136,6 +150,7 @@ public SharedPlugin __pl_EventNotifier =
|
|||||||
MarkNativeAsOptional("EventNotifier_FindEventByNumber");
|
MarkNativeAsOptional("EventNotifier_FindEventByNumber");
|
||||||
MarkNativeAsOptional("EventNotifier_GetEvent");
|
MarkNativeAsOptional("EventNotifier_GetEvent");
|
||||||
MarkNativeAsOptional("EventNotifier_Refresh");
|
MarkNativeAsOptional("EventNotifier_Refresh");
|
||||||
|
MarkNativeAsOptional("EventNotifier_FormatClientTime");
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,87 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Generates configs/eventnotifier_timezones.cfg from the system tzdata.
|
||||||
|
|
||||||
|
Each zone lists "<utc timestamp>" "<offset seconds> <abbreviation>" pairs; the
|
||||||
|
"0" entry is the offset in effect before the first transition in range.
|
||||||
|
Regenerate before END_YEAR runs out (32-bit timestamps end in 2038 anyway).
|
||||||
|
"""
|
||||||
|
|
||||||
|
import re
|
||||||
|
import zoneinfo
|
||||||
|
from datetime import datetime, timedelta, timezone
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
START_YEAR = 2025
|
||||||
|
END_YEAR = 2037
|
||||||
|
|
||||||
|
OUT = Path(__file__).resolve().parent.parent / "configs" / "eventnotifier_timezones.cfg"
|
||||||
|
|
||||||
|
|
||||||
|
def utc(ts):
|
||||||
|
return datetime.fromtimestamp(ts, timezone.utc)
|
||||||
|
|
||||||
|
|
||||||
|
def state(zone, ts):
|
||||||
|
local = utc(ts).astimezone(zone)
|
||||||
|
return int(local.utcoffset().total_seconds()), local.tzname()
|
||||||
|
|
||||||
|
|
||||||
|
def abbreviation(name, offset):
|
||||||
|
# tzdata uses numeric names like "+03" / "-0330" for zones without a real abbreviation.
|
||||||
|
if name and re.fullmatch(r"[A-Za-z]+", name):
|
||||||
|
return name
|
||||||
|
sign = "+" if offset >= 0 else "-"
|
||||||
|
hours, rem = divmod(abs(offset), 3600)
|
||||||
|
minutes = rem // 60
|
||||||
|
if hours == 0 and minutes == 0:
|
||||||
|
return "UTC"
|
||||||
|
return f"UTC{sign}{hours}:{minutes:02d}" if minutes else f"UTC{sign}{hours}"
|
||||||
|
|
||||||
|
|
||||||
|
def transitions(zone):
|
||||||
|
start = int(datetime(START_YEAR, 1, 1, tzinfo=timezone.utc).timestamp())
|
||||||
|
end = int(datetime(END_YEAR, 12, 31, tzinfo=timezone.utc).timestamp())
|
||||||
|
step = int(timedelta(days=1).total_seconds())
|
||||||
|
|
||||||
|
periods = [(0, *state(zone, start))]
|
||||||
|
prev_ts, prev = start, periods[0][1:]
|
||||||
|
ts = start + step
|
||||||
|
while ts <= end:
|
||||||
|
cur = state(zone, ts)
|
||||||
|
if cur != prev:
|
||||||
|
# Bisect to the first second of the new state.
|
||||||
|
lo, hi = prev_ts, ts
|
||||||
|
while hi - lo > 1:
|
||||||
|
mid = (lo + hi) // 2
|
||||||
|
if state(zone, mid) == prev:
|
||||||
|
lo = mid
|
||||||
|
else:
|
||||||
|
hi = mid
|
||||||
|
periods.append((hi, *state(zone, hi)))
|
||||||
|
prev = cur
|
||||||
|
prev_ts = ts
|
||||||
|
ts += step
|
||||||
|
return periods
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
lines = ["// Generated by tools/gen_timezones.py, do not edit by hand.", '"Timezones"', "{"]
|
||||||
|
for name in sorted(zoneinfo.available_timezones()):
|
||||||
|
if name.startswith(("posix/", "right/")) or name in ("Factory", "localtime"):
|
||||||
|
continue
|
||||||
|
zone = zoneinfo.ZoneInfo(name)
|
||||||
|
lines.append(f'\t"{name}"')
|
||||||
|
lines.append("\t{")
|
||||||
|
for ts, offset, tzname in transitions(zone):
|
||||||
|
lines.append(f'\t\t"{ts}"\t"{offset} {abbreviation(tzname, offset)}"')
|
||||||
|
lines.append("\t}")
|
||||||
|
lines.append("}")
|
||||||
|
|
||||||
|
OUT.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
OUT.write_text("\n".join(lines) + "\n")
|
||||||
|
print(f"Wrote {OUT}")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
Reference in New Issue
Block a user