mirror of
https://github.com/Ed94/Odin.git
synced 2026-08-05 15:18:49 +00:00
Merge pull request #4335 from colrdavidson/datetime_tz
Add Timezone Support to Odin
This commit is contained in:
@@ -1026,7 +1026,7 @@ absolute_path_from_handle :: proc(fd: Handle) -> (path: string, err: Error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@(require_results)
|
@(require_results)
|
||||||
absolute_path_from_relative :: proc(rel: string) -> (path: string, err: Error) {
|
absolute_path_from_relative :: proc(rel: string, allocator := context.allocator) -> (path: string, err: Error) {
|
||||||
rel := rel
|
rel := rel
|
||||||
if rel == "" {
|
if rel == "" {
|
||||||
rel = "."
|
rel = "."
|
||||||
@@ -1041,9 +1041,7 @@ absolute_path_from_relative :: proc(rel: string) -> (path: string, err: Error) {
|
|||||||
}
|
}
|
||||||
defer _unix_free(rawptr(path_ptr))
|
defer _unix_free(rawptr(path_ptr))
|
||||||
|
|
||||||
path = strings.clone(string(path_ptr))
|
return strings.clone(string(path_ptr), allocator)
|
||||||
|
|
||||||
return path, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
access :: proc(path: string, mask: int) -> bool {
|
access :: proc(path: string, mask: int) -> bool {
|
||||||
|
|||||||
@@ -789,7 +789,7 @@ absolute_path_from_handle :: proc(fd: Handle) -> (string, Error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@(require_results)
|
@(require_results)
|
||||||
absolute_path_from_relative :: proc(rel: string) -> (path: string, err: Error) {
|
absolute_path_from_relative :: proc(rel: string, allocator := context.allocator) -> (path: string, err: Error) {
|
||||||
rel := rel
|
rel := rel
|
||||||
if rel == "" {
|
if rel == "" {
|
||||||
rel = "."
|
rel = "."
|
||||||
@@ -804,10 +804,7 @@ absolute_path_from_relative :: proc(rel: string) -> (path: string, err: Error) {
|
|||||||
}
|
}
|
||||||
defer _unix_free(rawptr(path_ptr))
|
defer _unix_free(rawptr(path_ptr))
|
||||||
|
|
||||||
|
return strings.clone(string(path_ptr), allocator)
|
||||||
path = strings.clone(string(path_ptr))
|
|
||||||
|
|
||||||
return path, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
access :: proc(path: string, mask: int) -> (bool, Error) {
|
access :: proc(path: string, mask: int) -> (bool, Error) {
|
||||||
|
|||||||
@@ -431,7 +431,7 @@ absolute_path_from_handle :: proc(fd: Handle) -> (string, Error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@(require_results)
|
@(require_results)
|
||||||
absolute_path_from_relative :: proc(rel: string) -> (path: string, err: Error) {
|
absolute_path_from_relative :: proc(rel: string, allocator := context.allocator) -> (path: string, err: Error) {
|
||||||
rel := rel
|
rel := rel
|
||||||
if rel == "" {
|
if rel == "" {
|
||||||
rel = "."
|
rel = "."
|
||||||
@@ -447,9 +447,7 @@ absolute_path_from_relative :: proc(rel: string) -> (path: string, err: Error) {
|
|||||||
defer _unix_free(path_ptr)
|
defer _unix_free(path_ptr)
|
||||||
|
|
||||||
path_cstr := cstring(path_ptr)
|
path_cstr := cstring(path_ptr)
|
||||||
path = strings.clone(string(path_cstr))
|
return strings.clone(string(path_cstr), allocator)
|
||||||
|
|
||||||
return path, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
access :: proc(path: string, mask: int) -> (bool, Error) {
|
access :: proc(path: string, mask: int) -> (bool, Error) {
|
||||||
|
|||||||
@@ -917,7 +917,7 @@ absolute_path_from_handle :: proc(fd: Handle) -> (string, Error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@(require_results)
|
@(require_results)
|
||||||
absolute_path_from_relative :: proc(rel: string) -> (path: string, err: Error) {
|
absolute_path_from_relative :: proc(rel: string, allocator := context.allocator) -> (path: string, err: Error) {
|
||||||
rel := rel
|
rel := rel
|
||||||
if rel == "" {
|
if rel == "" {
|
||||||
rel = "."
|
rel = "."
|
||||||
@@ -932,9 +932,7 @@ absolute_path_from_relative :: proc(rel: string) -> (path: string, err: Error) {
|
|||||||
}
|
}
|
||||||
defer _unix_free(rawptr(path_ptr))
|
defer _unix_free(rawptr(path_ptr))
|
||||||
|
|
||||||
path = strings.clone(string(path_ptr))
|
return strings.clone(string(path_ptr), allocator)
|
||||||
|
|
||||||
return path, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
access :: proc(path: string, mask: int) -> (bool, Error) {
|
access :: proc(path: string, mask: int) -> (bool, Error) {
|
||||||
|
|||||||
@@ -844,7 +844,7 @@ absolute_path_from_handle :: proc(fd: Handle) -> (path: string, err: Error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@(require_results)
|
@(require_results)
|
||||||
absolute_path_from_relative :: proc(rel: string) -> (path: string, err: Error) {
|
absolute_path_from_relative :: proc(rel: string, allocator := context.allocator) -> (path: string, err: Error) {
|
||||||
rel := rel
|
rel := rel
|
||||||
if rel == "" {
|
if rel == "" {
|
||||||
rel = "."
|
rel = "."
|
||||||
@@ -859,9 +859,7 @@ absolute_path_from_relative :: proc(rel: string) -> (path: string, err: Error) {
|
|||||||
}
|
}
|
||||||
defer _unix_free(rawptr(path_ptr))
|
defer _unix_free(rawptr(path_ptr))
|
||||||
|
|
||||||
path = strings.clone(string(path_ptr))
|
return strings.clone(string(path_ptr), allocator)
|
||||||
|
|
||||||
return path, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
access :: proc(path: string, mask: int) -> (bool, Error) {
|
access :: proc(path: string, mask: int) -> (bool, Error) {
|
||||||
|
|||||||
@@ -758,7 +758,7 @@ absolute_path_from_handle :: proc(fd: Handle) -> (string, Error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@(require_results)
|
@(require_results)
|
||||||
absolute_path_from_relative :: proc(rel: string) -> (path: string, err: Error) {
|
absolute_path_from_relative :: proc(rel: string, allocator := context.allocator) -> (path: string, err: Error) {
|
||||||
rel := rel
|
rel := rel
|
||||||
if rel == "" {
|
if rel == "" {
|
||||||
rel = "."
|
rel = "."
|
||||||
@@ -773,9 +773,7 @@ absolute_path_from_relative :: proc(rel: string) -> (path: string, err: Error) {
|
|||||||
}
|
}
|
||||||
defer _unix_free(rawptr(path_ptr))
|
defer _unix_free(rawptr(path_ptr))
|
||||||
|
|
||||||
path = strings.clone(string(path_ptr))
|
return strings.clone(string(path_ptr), allocator)
|
||||||
|
|
||||||
return path, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
access :: proc(path: string, mask: int) -> (bool, Error) {
|
access :: proc(path: string, mask: int) -> (bool, Error) {
|
||||||
|
|||||||
@@ -0,0 +1,14 @@
|
|||||||
|
#+build windows
|
||||||
|
package sys_windows
|
||||||
|
|
||||||
|
foreign import "system:icu.lib"
|
||||||
|
|
||||||
|
UError :: enum i32 {
|
||||||
|
U_ZERO_ERROR = 0,
|
||||||
|
}
|
||||||
|
|
||||||
|
@(default_calling_convention="system")
|
||||||
|
foreign icu {
|
||||||
|
ucal_getWindowsTimeZoneID :: proc(id: wstring, len: i32, winid: wstring, winidCapacity: i32, status: ^UError) -> i32 ---
|
||||||
|
ucal_getDefaultTimeZone :: proc(result: wstring, cap: i32, status: ^UError) -> i32 ---
|
||||||
|
}
|
||||||
@@ -77,12 +77,55 @@ Time :: struct {
|
|||||||
nano: i32,
|
nano: i32,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TZ_Record :: struct {
|
||||||
|
time: i64,
|
||||||
|
utc_offset: i64,
|
||||||
|
shortname: string,
|
||||||
|
dst: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
TZ_Date_Kind :: enum {
|
||||||
|
No_Leap,
|
||||||
|
Leap,
|
||||||
|
Month_Week_Day,
|
||||||
|
}
|
||||||
|
|
||||||
|
TZ_Transition_Date :: struct {
|
||||||
|
type: TZ_Date_Kind,
|
||||||
|
|
||||||
|
month: u8,
|
||||||
|
week: u8,
|
||||||
|
day: u16,
|
||||||
|
|
||||||
|
time: i64,
|
||||||
|
}
|
||||||
|
|
||||||
|
TZ_RRule :: struct {
|
||||||
|
has_dst: bool,
|
||||||
|
|
||||||
|
std_name: string,
|
||||||
|
std_offset: i64,
|
||||||
|
std_date: TZ_Transition_Date,
|
||||||
|
|
||||||
|
dst_name: string,
|
||||||
|
dst_offset: i64,
|
||||||
|
dst_date: TZ_Transition_Date,
|
||||||
|
}
|
||||||
|
|
||||||
|
TZ_Region :: struct {
|
||||||
|
name: string,
|
||||||
|
records: []TZ_Record,
|
||||||
|
shortnames: []string,
|
||||||
|
rrule: TZ_RRule,
|
||||||
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
A type representing datetime.
|
A type representing datetime.
|
||||||
*/
|
*/
|
||||||
DateTime :: struct {
|
DateTime :: struct {
|
||||||
using date: Date,
|
using date: Date,
|
||||||
using time: Time,
|
using time: Time,
|
||||||
|
tz: ^TZ_Region,
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
|||||||
@@ -76,7 +76,7 @@ datetime, an error is returned.
|
|||||||
components_to_datetime :: proc "contextless" (#any_int year, #any_int month, #any_int day, #any_int hour, #any_int minute, #any_int second: i64, #any_int nanos := i64(0)) -> (datetime: DateTime, err: Error) {
|
components_to_datetime :: proc "contextless" (#any_int year, #any_int month, #any_int day, #any_int hour, #any_int minute, #any_int second: i64, #any_int nanos := i64(0)) -> (datetime: DateTime, err: Error) {
|
||||||
date := components_to_date(year, month, day) or_return
|
date := components_to_date(year, month, day) or_return
|
||||||
time := components_to_time(hour, minute, second, nanos) or_return
|
time := components_to_time(hour, minute, second, nanos) or_return
|
||||||
return {date, time}, .None
|
return {date, time, nil}, .None
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@@ -88,7 +88,7 @@ object will always have the time equal to `00:00:00.000`.
|
|||||||
*/
|
*/
|
||||||
ordinal_to_datetime :: proc "contextless" (ordinal: Ordinal) -> (datetime: DateTime, err: Error) {
|
ordinal_to_datetime :: proc "contextless" (ordinal: Ordinal) -> (datetime: DateTime, err: Error) {
|
||||||
d := ordinal_to_date(ordinal) or_return
|
d := ordinal_to_date(ordinal) or_return
|
||||||
return {Date(d), {}}, .None
|
return {Date(d), {}, nil}, .None
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
|||||||
+2
-2
@@ -930,7 +930,7 @@ If the datetime represents a time outside of a valid range, `false` is returned
|
|||||||
as the second return value. See `Time` for the representable range.
|
as the second return value. See `Time` for the representable range.
|
||||||
*/
|
*/
|
||||||
compound_to_time :: proc "contextless" (datetime: dt.DateTime) -> (t: Time, ok: bool) {
|
compound_to_time :: proc "contextless" (datetime: dt.DateTime) -> (t: Time, ok: bool) {
|
||||||
unix_epoch := dt.DateTime{{1970, 1, 1}, {0, 0, 0, 0}}
|
unix_epoch := dt.DateTime{{1970, 1, 1}, {0, 0, 0, 0}, nil}
|
||||||
delta, err := dt.sub(datetime, unix_epoch)
|
delta, err := dt.sub(datetime, unix_epoch)
|
||||||
if err != .None {
|
if err != .None {
|
||||||
return
|
return
|
||||||
@@ -958,7 +958,7 @@ datetime_to_time :: proc{components_to_time, compound_to_time}
|
|||||||
Convert time into datetime.
|
Convert time into datetime.
|
||||||
*/
|
*/
|
||||||
time_to_datetime :: proc "contextless" (t: Time) -> (dt.DateTime, bool) {
|
time_to_datetime :: proc "contextless" (t: Time) -> (dt.DateTime, bool) {
|
||||||
unix_epoch := dt.DateTime{{1970, 1, 1}, {0, 0, 0, 0}}
|
unix_epoch := dt.DateTime{{1970, 1, 1}, {0, 0, 0, 0}, nil}
|
||||||
|
|
||||||
datetime, err := dt.add(unix_epoch, dt.Delta{ nanos = t._nsec })
|
datetime, err := dt.add(unix_epoch, dt.Delta{ nanos = t._nsec })
|
||||||
if err != .None {
|
if err != .None {
|
||||||
|
|||||||
@@ -0,0 +1,89 @@
|
|||||||
|
#+build darwin, linux, freebsd, openbsd, netbsd
|
||||||
|
#+private
|
||||||
|
package timezone
|
||||||
|
|
||||||
|
import "core:os"
|
||||||
|
import "core:strings"
|
||||||
|
import "core:path/filepath"
|
||||||
|
import "core:time/datetime"
|
||||||
|
|
||||||
|
local_tz_name :: proc(allocator := context.allocator) -> (name: string, success: bool) {
|
||||||
|
local_str, ok := os.lookup_env("TZ", allocator)
|
||||||
|
if !ok {
|
||||||
|
orig_localtime_path := "/etc/localtime"
|
||||||
|
path, err := os.absolute_path_from_relative(orig_localtime_path, allocator)
|
||||||
|
if err != nil {
|
||||||
|
// If we can't find /etc/localtime, fallback to UTC
|
||||||
|
if err == .ENOENT {
|
||||||
|
str, err2 := strings.clone("UTC", allocator)
|
||||||
|
if err2 != nil { return }
|
||||||
|
return str, true
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer delete(path, allocator)
|
||||||
|
|
||||||
|
// FreeBSD makes me sad.
|
||||||
|
// This is a hackaround, because FreeBSD copies rather than softlinks their local timezone file,
|
||||||
|
// *sometimes* and then stores the original name of the timezone in /var/db/zoneinfo instead
|
||||||
|
if path == orig_localtime_path {
|
||||||
|
data := os.read_entire_file("/var/db/zoneinfo", allocator) or_return
|
||||||
|
return strings.trim_right_space(string(data)), true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Looking for tz path (ex fmt: "UTC", "Etc/UTC" or "America/Los_Angeles")
|
||||||
|
path_dir, path_file := filepath.split(path)
|
||||||
|
if path_dir == "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
upper_path_dir, upper_path_chunk := filepath.split(path_dir[:len(path_dir)-1])
|
||||||
|
if upper_path_dir == "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if strings.contains(upper_path_chunk, "zoneinfo") {
|
||||||
|
region_str, err := strings.clone(path_file, allocator)
|
||||||
|
if err != nil { return }
|
||||||
|
return region_str, true
|
||||||
|
} else {
|
||||||
|
region_str, err := filepath.join({upper_path_chunk, path_file}, allocator = allocator)
|
||||||
|
if err != nil { return }
|
||||||
|
return region_str, true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if local_str == "" {
|
||||||
|
delete(local_str, allocator)
|
||||||
|
|
||||||
|
str, err := strings.clone("UTC", allocator)
|
||||||
|
if err != nil { return }
|
||||||
|
return str, true
|
||||||
|
}
|
||||||
|
|
||||||
|
return local_str, true
|
||||||
|
}
|
||||||
|
|
||||||
|
_region_load :: proc(_reg_str: string, allocator := context.allocator) -> (out_reg: ^datetime.TZ_Region, success: bool) {
|
||||||
|
reg_str := _reg_str
|
||||||
|
if reg_str == "UTC" {
|
||||||
|
return nil, true
|
||||||
|
}
|
||||||
|
|
||||||
|
if reg_str == "local" {
|
||||||
|
local_name := local_tz_name(allocator) or_return
|
||||||
|
if local_name == "UTC" {
|
||||||
|
delete(local_name, allocator)
|
||||||
|
return nil, true
|
||||||
|
}
|
||||||
|
|
||||||
|
reg_str = local_name
|
||||||
|
}
|
||||||
|
defer if _reg_str == "local" { delete(reg_str, allocator) }
|
||||||
|
|
||||||
|
db_path := "/usr/share/zoneinfo"
|
||||||
|
region_path := filepath.join({db_path, reg_str}, allocator)
|
||||||
|
defer delete(region_path, allocator)
|
||||||
|
|
||||||
|
return load_tzif_file(region_path, reg_str, allocator)
|
||||||
|
}
|
||||||
@@ -0,0 +1,295 @@
|
|||||||
|
#+build windows
|
||||||
|
#+private
|
||||||
|
package timezone
|
||||||
|
|
||||||
|
import "core:strings"
|
||||||
|
import "core:sys/windows"
|
||||||
|
import "core:time/datetime"
|
||||||
|
|
||||||
|
TZ_Abbrev :: struct {
|
||||||
|
std: string,
|
||||||
|
dst: string,
|
||||||
|
}
|
||||||
|
|
||||||
|
tz_abbrevs := map[string]TZ_Abbrev {
|
||||||
|
"Egypt Standard Time" = {"EET", "EEST"}, // Africa/Cairo
|
||||||
|
"Morocco Standard Time" = {"+00", "+01"}, // Africa/Casablanca
|
||||||
|
"South Africa Standard Time" = {"SAST", "SAST"}, // Africa/Johannesburg
|
||||||
|
"South Sudan Standard Time" = {"CAT", "CAT"}, // Africa/Juba
|
||||||
|
"Sudan Standard Time" = {"CAT", "CAT"}, // Africa/Khartoum
|
||||||
|
"W. Central Africa Standard Time" = {"WAT", "WAT"}, // Africa/Lagos
|
||||||
|
"E. Africa Standard Time" = {"EAT", "EAT"}, // Africa/Nairobi
|
||||||
|
"Sao Tome Standard Time" = {"GMT", "GMT"}, // Africa/Sao_Tome
|
||||||
|
"Libya Standard Time" = {"EET", "EET"}, // Africa/Tripoli
|
||||||
|
"Namibia Standard Time" = {"CAT", "CAT"}, // Africa/Windhoek
|
||||||
|
"Aleutian Standard Time" = {"HST", "HDT"}, // America/Adak
|
||||||
|
"Alaskan Standard Time" = {"AKST", "AKDT"}, // America/Anchorage
|
||||||
|
"Tocantins Standard Time" = {"-03", "-03"}, // America/Araguaina
|
||||||
|
"Paraguay Standard Time" = {"-04", "-03"}, // America/Asuncion
|
||||||
|
"Bahia Standard Time" = {"-03", "-03"}, // America/Bahia
|
||||||
|
"SA Pacific Standard Time" = {"-05", "-05"}, // America/Bogota
|
||||||
|
"Argentina Standard Time" = {"-03", "-03"}, // America/Buenos_Aires
|
||||||
|
"Eastern Standard Time (Mexico)" = {"EST", "EST"}, // America/Cancun
|
||||||
|
"Venezuela Standard Time" = {"-04", "-04"}, // America/Caracas
|
||||||
|
"SA Eastern Standard Time" = {"-03", "-03"}, // America/Cayenne
|
||||||
|
"Central Standard Time" = {"CST", "CDT"}, // America/Chicago
|
||||||
|
"Central Brazilian Standard Time" = {"-04", "-04"}, // America/Cuiaba
|
||||||
|
"Mountain Standard Time" = {"MST", "MDT"}, // America/Denver
|
||||||
|
"Greenland Standard Time" = {"-03", "-02"}, // America/Godthab
|
||||||
|
"Turks And Caicos Standard Time" = {"EST", "EDT"}, // America/Grand_Turk
|
||||||
|
"Central America Standard Time" = {"CST", "CST"}, // America/Guatemala
|
||||||
|
"Atlantic Standard Time" = {"AST", "ADT"}, // America/Halifax
|
||||||
|
"Cuba Standard Time" = {"CST", "CDT"}, // America/Havana
|
||||||
|
"US Eastern Standard Time" = {"EST", "EDT"}, // America/Indianapolis
|
||||||
|
"SA Western Standard Time" = {"-04", "-04"}, // America/La_Paz
|
||||||
|
"Pacific Standard Time" = {"PST", "PDT"}, // America/Los_Angeles
|
||||||
|
"Mountain Standard Time (Mexico)" = {"MST", "MST"}, // America/Mazatlan
|
||||||
|
"Central Standard Time (Mexico)" = {"CST", "CST"}, // America/Mexico_City
|
||||||
|
"Saint Pierre Standard Time" = {"-03", "-02"}, // America/Miquelon
|
||||||
|
"Montevideo Standard Time" = {"-03", "-03"}, // America/Montevideo
|
||||||
|
"Eastern Standard Time" = {"EST", "EDT"}, // America/New_York
|
||||||
|
"US Mountain Standard Time" = {"MST", "MST"}, // America/Phoenix
|
||||||
|
"Haiti Standard Time" = {"EST", "EDT"}, // America/Port-au-Prince
|
||||||
|
"Magallanes Standard Time" = {"-03", "-03"}, // America/Punta_Arenas
|
||||||
|
"Canada Central Standard Time" = {"CST", "CST"}, // America/Regina
|
||||||
|
"Pacific SA Standard Time" = {"-04", "-03"}, // America/Santiago
|
||||||
|
"E. South America Standard Time" = {"-03", "-03"}, // America/Sao_Paulo
|
||||||
|
"Newfoundland Standard Time" = {"NST", "NDT"}, // America/St_Johns
|
||||||
|
"Pacific Standard Time (Mexico)" = {"PST", "PDT"}, // America/Tijuana
|
||||||
|
"Yukon Standard Time" = {"MST", "MST"}, // America/Whitehorse
|
||||||
|
"Central Asia Standard Time" = {"+06", "+06"}, // Asia/Almaty
|
||||||
|
"Jordan Standard Time" = {"+03", "+03"}, // Asia/Amman
|
||||||
|
"Arabic Standard Time" = {"+03", "+03"}, // Asia/Baghdad
|
||||||
|
"Azerbaijan Standard Time" = {"+04", "+04"}, // Asia/Baku
|
||||||
|
"SE Asia Standard Time" = {"+07", "+07"}, // Asia/Bangkok
|
||||||
|
"Altai Standard Time" = {"+07", "+07"}, // Asia/Barnaul
|
||||||
|
"Middle East Standard Time" = {"EET", "EEST"}, // Asia/Beirut
|
||||||
|
"India Standard Time" = {"IST", "IST"}, // Asia/Calcutta
|
||||||
|
"Transbaikal Standard Time" = {"+09", "+09"}, // Asia/Chita
|
||||||
|
"Sri Lanka Standard Time" = {"+0530", "+0530"}, // Asia/Colombo
|
||||||
|
"Syria Standard Time" = {"+03", "+03"}, // Asia/Damascus
|
||||||
|
"Bangladesh Standard Time" = {"+06", "+06"}, // Asia/Dhaka
|
||||||
|
"Arabian Standard Time" = {"+04", "+04"}, // Asia/Dubai
|
||||||
|
"West Bank Standard Time" = {"EET", "EEST"}, // Asia/Hebron
|
||||||
|
"W. Mongolia Standard Time" = {"+07", "+07"}, // Asia/Hovd
|
||||||
|
"North Asia East Standard Time" = {"+08", "+08"}, // Asia/Irkutsk
|
||||||
|
"Israel Standard Time" = {"IST", "IDT"}, // Asia/Jerusalem
|
||||||
|
"Afghanistan Standard Time" = {"+0430", "+0430"}, // Asia/Kabul
|
||||||
|
"Russia Time Zone 11" = {"+12", "+12"}, // Asia/Kamchatka
|
||||||
|
"Pakistan Standard Time" = {"PKT", "PKT"}, // Asia/Karachi
|
||||||
|
"Nepal Standard Time" = {"+0545", "+0545"}, // Asia/Katmandu
|
||||||
|
"North Asia Standard Time" = {"+07", "+07"}, // Asia/Krasnoyarsk
|
||||||
|
"Magadan Standard Time" = {"+11", "+11"}, // Asia/Magadan
|
||||||
|
"N. Central Asia Standard Time" = {"+07", "+07"}, // Asia/Novosibirsk
|
||||||
|
"Omsk Standard Time" = {"+06", "+06"}, // Asia/Omsk
|
||||||
|
"North Korea Standard Time" = {"KST", "KST"}, // Asia/Pyongyang
|
||||||
|
"Qyzylorda Standard Time" = {"+05", "+05"}, // Asia/Qyzylorda
|
||||||
|
"Myanmar Standard Time" = {"+0630", "+0630"}, // Asia/Rangoon
|
||||||
|
"Arab Standard Time" = {"+03", "+03"}, // Asia/Riyadh
|
||||||
|
"Sakhalin Standard Time" = {"+11", "+11"}, // Asia/Sakhalin
|
||||||
|
"Korea Standard Time" = {"KST", "KST"}, // Asia/Seoul
|
||||||
|
"China Standard Time" = {"CST", "CST"}, // Asia/Shanghai
|
||||||
|
"Singapore Standard Time" = {"+08", "+08"}, // Asia/Singapore
|
||||||
|
"Russia Time Zone 10" = {"+11", "+11"}, // Asia/Srednekolymsk
|
||||||
|
"Taipei Standard Time" = {"CST", "CST"}, // Asia/Taipei
|
||||||
|
"West Asia Standard Time" = {"+05", "+05"}, // Asia/Tashkent
|
||||||
|
"Georgian Standard Time" = {"+04", "+04"}, // Asia/Tbilisi
|
||||||
|
"Iran Standard Time" = {"+0330", "+0330"}, // Asia/Tehran
|
||||||
|
"Tokyo Standard Time" = {"JST", "JST"}, // Asia/Tokyo
|
||||||
|
"Tomsk Standard Time" = {"+07", "+07"}, // Asia/Tomsk
|
||||||
|
"Ulaanbaatar Standard Time" = {"+08", "+08"}, // Asia/Ulaanbaatar
|
||||||
|
"Vladivostok Standard Time" = {"+10", "+10"}, // Asia/Vladivostok
|
||||||
|
"Yakutsk Standard Time" = {"+09", "+09"}, // Asia/Yakutsk
|
||||||
|
"Ekaterinburg Standard Time" = {"+05", "+05"}, // Asia/Yekaterinburg
|
||||||
|
"Caucasus Standard Time" = {"+04", "+04"}, // Asia/Yerevan
|
||||||
|
"Azores Standard Time" = {"-01", "+00"}, // Atlantic/Azores
|
||||||
|
"Cape Verde Standard Time" = {"-01", "-01"}, // Atlantic/Cape_Verde
|
||||||
|
"Greenwich Standard Time" = {"GMT", "GMT"}, // Atlantic/Reykjavik
|
||||||
|
"Cen. Australia Standard Time" = {"ACST", "ACDT"}, // Australia/Adelaide
|
||||||
|
"E. Australia Standard Time" = {"AEST", "AEST"}, // Australia/Brisbane
|
||||||
|
"AUS Central Standard Time" = {"ACST", "ACST"}, // Australia/Darwin
|
||||||
|
"Aus Central W. Standard Time" = {"+0845", "+0845"}, // Australia/Eucla
|
||||||
|
"Tasmania Standard Time" = {"AEST", "AEDT"}, // Australia/Hobart
|
||||||
|
"Lord Howe Standard Time" = {"+1030", "+11"}, // Australia/Lord_Howe
|
||||||
|
"W. Australia Standard Time" = {"AWST", "AWST"}, // Australia/Perth
|
||||||
|
"AUS Eastern Standard Time" = {"AEST", "AEDT"}, // Australia/Sydney
|
||||||
|
"UTC-11" = {"-11", "-11"}, // Etc/GMT+11
|
||||||
|
"Dateline Standard Time" = {"-12", "-12"}, // Etc/GMT+12
|
||||||
|
"UTC-02" = {"-02", "-02"}, // Etc/GMT+2
|
||||||
|
"UTC-08" = {"-08", "-08"}, // Etc/GMT+8
|
||||||
|
"UTC-09" = {"-09", "-09"}, // Etc/GMT+9
|
||||||
|
"UTC+12" = {"+12", "+12"}, // Etc/GMT-12
|
||||||
|
"UTC+13" = {"+13", "+13"}, // Etc/GMT-13
|
||||||
|
"UTC" = {"UTC", "UTC"}, // Etc/UTC
|
||||||
|
"Astrakhan Standard Time" = {"+04", "+04"}, // Europe/Astrakhan
|
||||||
|
"W. Europe Standard Time" = {"CET", "CEST"}, // Europe/Berlin
|
||||||
|
"GTB Standard Time" = {"EET", "EEST"}, // Europe/Bucharest
|
||||||
|
"Central Europe Standard Time" = {"CET", "CEST"}, // Europe/Budapest
|
||||||
|
"E. Europe Standard Time" = {"EET", "EEST"}, // Europe/Chisinau
|
||||||
|
"Turkey Standard Time" = {"+03", "+03"}, // Europe/Istanbul
|
||||||
|
"Kaliningrad Standard Time" = {"EET", "EET"}, // Europe/Kaliningrad
|
||||||
|
"FLE Standard Time" = {"EET", "EEST"}, // Europe/Kiev
|
||||||
|
"GMT Standard Time" = {"GMT", "BST"}, // Europe/London
|
||||||
|
"Belarus Standard Time" = {"+03", "+03"}, // Europe/Minsk
|
||||||
|
"Russian Standard Time" = {"MSK", "MSK"}, // Europe/Moscow
|
||||||
|
"Romance Standard Time" = {"CET", "CEST"}, // Europe/Paris
|
||||||
|
"Russia Time Zone 3" = {"+04", "+04"}, // Europe/Samara
|
||||||
|
"Saratov Standard Time" = {"+04", "+04"}, // Europe/Saratov
|
||||||
|
"Volgograd Standard Time" = {"MSK", "MSK"}, // Europe/Volgograd
|
||||||
|
"Central European Standard Time" = {"CET", "CEST"}, // Europe/Warsaw
|
||||||
|
"Mauritius Standard Time" = {"+04", "+04"}, // Indian/Mauritius
|
||||||
|
"Samoa Standard Time" = {"+13", "+13"}, // Pacific/Apia
|
||||||
|
"New Zealand Standard Time" = {"NZST", "NZDT"}, // Pacific/Auckland
|
||||||
|
"Bougainville Standard Time" = {"+11", "+11"}, // Pacific/Bougainville
|
||||||
|
"Chatham Islands Standard Time" = {"+1245", "+1345"}, // Pacific/Chatham
|
||||||
|
"Easter Island Standard Time" = {"-06", "-05"}, // Pacific/Easter
|
||||||
|
"Fiji Standard Time" = {"+12", "+12"}, // Pacific/Fiji
|
||||||
|
"Central Pacific Standard Time" = {"+11", "+11"}, // Pacific/Guadalcanal
|
||||||
|
"Hawaiian Standard Time" = {"HST", "HST"}, // Pacific/Honolulu
|
||||||
|
"Line Islands Standard Time" = {"+14", "+14"}, // Pacific/Kiritimati
|
||||||
|
"Marquesas Standard Time" = {"-0930", "-0930"}, // Pacific/Marquesas
|
||||||
|
"Norfolk Standard Time" = {"+11", "+12"}, // Pacific/Norfolk
|
||||||
|
"West Pacific Standard Time" = {"+10", "+10"}, // Pacific/Port_Moresby
|
||||||
|
"Tonga Standard Time" = {"+13", "+13"}, // Pacific/Tongatapu
|
||||||
|
}
|
||||||
|
|
||||||
|
iana_to_windows_tz :: proc(iana_name: string, allocator := context.allocator) -> (name: string, success: bool) {
|
||||||
|
wintz_name_buffer: [128]u16
|
||||||
|
status: windows.UError
|
||||||
|
|
||||||
|
iana_name_wstr := windows.utf8_to_wstring(iana_name, allocator)
|
||||||
|
defer free(iana_name_wstr, allocator)
|
||||||
|
|
||||||
|
wintz_name_len := windows.ucal_getWindowsTimeZoneID(iana_name_wstr, -1, raw_data(wintz_name_buffer[:]), len(wintz_name_buffer), &status)
|
||||||
|
if status != .U_ZERO_ERROR {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
wintz_name, err := windows.utf16_to_utf8(wintz_name_buffer[:wintz_name_len], allocator)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
return wintz_name, true
|
||||||
|
}
|
||||||
|
|
||||||
|
local_tz_name :: proc(allocator := context.allocator) -> (name: string, success: bool) {
|
||||||
|
iana_name_buffer: [128]u16
|
||||||
|
status: windows.UError
|
||||||
|
|
||||||
|
zone_str_len := windows.ucal_getDefaultTimeZone(raw_data(iana_name_buffer[:]), len(iana_name_buffer), &status)
|
||||||
|
if status != .U_ZERO_ERROR {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
iana_name, err := windows.utf16_to_utf8(iana_name_buffer[:zone_str_len], allocator)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
return iana_name, true
|
||||||
|
}
|
||||||
|
|
||||||
|
REG_TZI_FORMAT :: struct #packed {
|
||||||
|
bias: windows.LONG,
|
||||||
|
std_bias: windows.LONG,
|
||||||
|
dst_bias: windows.LONG,
|
||||||
|
std_date: windows.SYSTEMTIME,
|
||||||
|
dst_date: windows.SYSTEMTIME,
|
||||||
|
}
|
||||||
|
|
||||||
|
generate_rrule_from_tzi :: proc(tzi: ^REG_TZI_FORMAT, abbrevs: TZ_Abbrev, allocator := context.allocator) -> (rrule: datetime.TZ_RRule, ok: bool) {
|
||||||
|
std_name, err := strings.clone(abbrevs.std, allocator)
|
||||||
|
if err != nil { return }
|
||||||
|
defer if err != nil { delete(std_name, allocator) }
|
||||||
|
|
||||||
|
dst_name: string
|
||||||
|
dst_name, err = strings.clone(abbrevs.dst, allocator)
|
||||||
|
if err != nil { return }
|
||||||
|
defer if err != nil { delete(dst_name, allocator) }
|
||||||
|
|
||||||
|
return datetime.TZ_RRule{
|
||||||
|
has_dst = true,
|
||||||
|
|
||||||
|
std_name = std_name,
|
||||||
|
std_offset = -(i64(tzi.bias) + i64(tzi.std_bias)) * 60,
|
||||||
|
dst_date = datetime.TZ_Transition_Date{
|
||||||
|
type = .Month_Week_Day,
|
||||||
|
month = u8(tzi.std_date.month),
|
||||||
|
week = u8(tzi.std_date.day),
|
||||||
|
day = tzi.std_date.day_of_week,
|
||||||
|
time = (i64(tzi.std_date.hour) * 60 * 60) + (i64(tzi.std_date.minute) * 60) + i64(tzi.std_date.second),
|
||||||
|
},
|
||||||
|
|
||||||
|
dst_name = dst_name,
|
||||||
|
dst_offset = -(i64(tzi.bias) + i64(tzi.dst_bias)) * 60,
|
||||||
|
std_date = datetime.TZ_Transition_Date{
|
||||||
|
type = .Month_Week_Day,
|
||||||
|
month = u8(tzi.dst_date.month),
|
||||||
|
week = u8(tzi.dst_date.day),
|
||||||
|
day = tzi.dst_date.day_of_week,
|
||||||
|
time = (i64(tzi.dst_date.hour) * 60 * 60) + (i64(tzi.dst_date.minute) * 60) + i64(tzi.dst_date.second),
|
||||||
|
},
|
||||||
|
}, true
|
||||||
|
}
|
||||||
|
|
||||||
|
_region_load :: proc(reg_str: string, allocator := context.allocator) -> (out_reg: ^datetime.TZ_Region, success: bool) {
|
||||||
|
wintz_name: string
|
||||||
|
iana_name: string
|
||||||
|
|
||||||
|
if reg_str == "local" {
|
||||||
|
ok := false
|
||||||
|
|
||||||
|
iana_name = local_tz_name(allocator) or_return
|
||||||
|
wintz_name, ok = iana_to_windows_tz(iana_name, allocator)
|
||||||
|
if !ok {
|
||||||
|
delete(iana_name, allocator)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
wintz_name = iana_to_windows_tz(reg_str, allocator) or_return
|
||||||
|
iana_name = strings.clone(reg_str, allocator)
|
||||||
|
}
|
||||||
|
defer delete(wintz_name, allocator)
|
||||||
|
defer delete(iana_name, allocator)
|
||||||
|
|
||||||
|
abbrevs := tz_abbrevs[wintz_name] or_return
|
||||||
|
if abbrevs.std == "UTC" && abbrevs.dst == abbrevs.std {
|
||||||
|
return nil, true
|
||||||
|
}
|
||||||
|
|
||||||
|
key_base := `SOFTWARE\Microsoft\Windows NT\CurrentVersion\Time Zones`
|
||||||
|
tz_key := strings.join({key_base, wintz_name}, "\\", allocator = allocator)
|
||||||
|
defer delete(tz_key, allocator)
|
||||||
|
|
||||||
|
tz_key_wstr := windows.utf8_to_wstring(tz_key, allocator)
|
||||||
|
defer free(tz_key_wstr, allocator)
|
||||||
|
|
||||||
|
key: windows.HKEY
|
||||||
|
res := windows.RegOpenKeyExW(windows.HKEY_LOCAL_MACHINE, tz_key_wstr, 0, windows.KEY_READ, &key)
|
||||||
|
if res != 0 { return }
|
||||||
|
defer windows.RegCloseKey(key)
|
||||||
|
|
||||||
|
tzi: REG_TZI_FORMAT
|
||||||
|
size := u32(size_of(REG_TZI_FORMAT))
|
||||||
|
|
||||||
|
res = windows.RegGetValueW(key, nil, windows.L("TZI"), windows.RRF_RT_ANY, nil, &tzi, &size)
|
||||||
|
if res != 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
rrule := generate_rrule_from_tzi(&tzi, abbrevs, allocator) or_return
|
||||||
|
|
||||||
|
region_name, err := strings.clone(iana_name, allocator)
|
||||||
|
if err != nil { return }
|
||||||
|
defer if err != nil { delete(region_name, allocator) }
|
||||||
|
|
||||||
|
region: ^datetime.TZ_Region
|
||||||
|
region, err = new_clone(datetime.TZ_Region{
|
||||||
|
name = region_name,
|
||||||
|
rrule = rrule,
|
||||||
|
}, allocator)
|
||||||
|
if err != nil { return }
|
||||||
|
|
||||||
|
return region, true
|
||||||
|
}
|
||||||
@@ -0,0 +1,339 @@
|
|||||||
|
package timezone
|
||||||
|
|
||||||
|
import "core:fmt"
|
||||||
|
import "core:slice"
|
||||||
|
import "core:time"
|
||||||
|
import "core:time/datetime"
|
||||||
|
|
||||||
|
region_load :: proc(reg: string, allocator := context.allocator) -> (out_reg: ^datetime.TZ_Region, ok: bool) {
|
||||||
|
return _region_load(reg, allocator)
|
||||||
|
}
|
||||||
|
|
||||||
|
region_load_from_file :: proc(file_path, reg: string, allocator := context.allocator) -> (out_reg: ^datetime.TZ_Region, ok: bool) {
|
||||||
|
return load_tzif_file(file_path, reg, allocator)
|
||||||
|
}
|
||||||
|
|
||||||
|
region_load_from_buffer :: proc(buffer: []u8, reg: string, allocator := context.allocator) -> (out_reg: ^datetime.TZ_Region, ok: bool) {
|
||||||
|
return parse_tzif(buffer, reg, allocator)
|
||||||
|
}
|
||||||
|
|
||||||
|
rrule_destroy :: proc(rrule: datetime.TZ_RRule, allocator := context.allocator) {
|
||||||
|
delete(rrule.std_name, allocator)
|
||||||
|
delete(rrule.dst_name, allocator)
|
||||||
|
}
|
||||||
|
|
||||||
|
region_destroy :: proc(region: ^datetime.TZ_Region, allocator := context.allocator) {
|
||||||
|
if region == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for name in region.shortnames {
|
||||||
|
delete(name, allocator)
|
||||||
|
}
|
||||||
|
delete(region.shortnames, allocator)
|
||||||
|
delete(region.records, allocator)
|
||||||
|
delete(region.name, allocator)
|
||||||
|
rrule_destroy(region.rrule, allocator)
|
||||||
|
free(region, allocator)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@private
|
||||||
|
region_get_nearest :: proc(region: ^datetime.TZ_Region, tm: time.Time) -> (out: datetime.TZ_Record, success: bool) {
|
||||||
|
if len(region.records) == 0 {
|
||||||
|
return process_rrule(region.rrule, tm)
|
||||||
|
}
|
||||||
|
|
||||||
|
n := len(region.records)
|
||||||
|
left, right := 0, n
|
||||||
|
|
||||||
|
tm_sec := time.to_unix_seconds(tm)
|
||||||
|
last_time := region.records[len(region.records)-1].time
|
||||||
|
if tm_sec > last_time {
|
||||||
|
return process_rrule(region.rrule, tm)
|
||||||
|
}
|
||||||
|
|
||||||
|
for left < right {
|
||||||
|
mid := int(uint(left+right) >> 1)
|
||||||
|
if region.records[mid].time < tm_sec {
|
||||||
|
left = mid + 1
|
||||||
|
} else {
|
||||||
|
right = mid
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
idx := max(0, left-1)
|
||||||
|
return region.records[idx], true
|
||||||
|
}
|
||||||
|
|
||||||
|
@private
|
||||||
|
month_to_seconds :: proc(month: int, is_leap: bool) -> i64 {
|
||||||
|
month_seconds := []i64{
|
||||||
|
0, 31 * 86_400, 59 * 86_400, 90 * 86_400,
|
||||||
|
120 * 86_400, 151 * 86_400, 181 * 86_400, 212 * 86_400,
|
||||||
|
243 * 86_400, 273 * 86_400, 304 * 86_400, 334 * 86_400,
|
||||||
|
}
|
||||||
|
|
||||||
|
t := month_seconds[month]
|
||||||
|
if is_leap && month >= 2 {
|
||||||
|
t += 86_400
|
||||||
|
}
|
||||||
|
return t
|
||||||
|
}
|
||||||
|
|
||||||
|
@private
|
||||||
|
trans_date_to_seconds :: proc(year: i64, td: datetime.TZ_Transition_Date) -> (secs: i64, ok: bool) {
|
||||||
|
is_leap := datetime.is_leap_year(year)
|
||||||
|
DAY_SEC :: 86_400
|
||||||
|
|
||||||
|
year_start := datetime.DateTime{{year, 1, 1}, {0, 0, 0, 0}, nil}
|
||||||
|
year_start_time := time.datetime_to_time(year_start) or_return
|
||||||
|
|
||||||
|
t := i64(time.to_unix_seconds(year_start_time))
|
||||||
|
|
||||||
|
switch td.type {
|
||||||
|
case .Month_Week_Day:
|
||||||
|
t += month_to_seconds(int(td.month) - 1, is_leap)
|
||||||
|
|
||||||
|
weekday := ((t + (4 * DAY_SEC)) %% (7 * DAY_SEC)) / DAY_SEC
|
||||||
|
days := i64(td.day) - weekday
|
||||||
|
if days < 0 { days += 7 }
|
||||||
|
|
||||||
|
month_daycount, err := datetime.last_day_of_month(year, td.month)
|
||||||
|
if err != nil { return }
|
||||||
|
|
||||||
|
week := td.week
|
||||||
|
if week == 5 && days + 28 >= i64(month_daycount) {
|
||||||
|
week = 4
|
||||||
|
}
|
||||||
|
|
||||||
|
t += DAY_SEC * (days + (7 * i64(week - 1)))
|
||||||
|
t += td.time
|
||||||
|
|
||||||
|
return t, true
|
||||||
|
|
||||||
|
// Both of these should result in 0 -> 365 days (in seconds)
|
||||||
|
case .No_Leap:
|
||||||
|
day := i64(td.day)
|
||||||
|
|
||||||
|
// if before Feb 29th || not a leap year
|
||||||
|
if day < 60 || !is_leap {
|
||||||
|
day -= 1
|
||||||
|
}
|
||||||
|
t += DAY_SEC * day
|
||||||
|
|
||||||
|
return t, true
|
||||||
|
|
||||||
|
case .Leap:
|
||||||
|
t += DAY_SEC * i64(td.day)
|
||||||
|
|
||||||
|
return t, true
|
||||||
|
|
||||||
|
case:
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
@private
|
||||||
|
process_rrule :: proc(rrule: datetime.TZ_RRule, tm: time.Time) -> (out: datetime.TZ_Record, success: bool) {
|
||||||
|
if !rrule.has_dst {
|
||||||
|
return datetime.TZ_Record{
|
||||||
|
time = time.to_unix_seconds(tm),
|
||||||
|
utc_offset = rrule.std_offset,
|
||||||
|
shortname = rrule.std_name,
|
||||||
|
dst = false,
|
||||||
|
}, true
|
||||||
|
}
|
||||||
|
|
||||||
|
y, _, _ := time.date(tm)
|
||||||
|
std_secs := trans_date_to_seconds(i64(y), rrule.std_date) or_return
|
||||||
|
dst_secs := trans_date_to_seconds(i64(y), rrule.dst_date) or_return
|
||||||
|
|
||||||
|
records := []datetime.TZ_Record{
|
||||||
|
{
|
||||||
|
time = std_secs,
|
||||||
|
utc_offset = rrule.std_offset,
|
||||||
|
shortname = rrule.std_name,
|
||||||
|
dst = false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
time = dst_secs,
|
||||||
|
utc_offset = rrule.dst_offset,
|
||||||
|
shortname = rrule.dst_name,
|
||||||
|
dst = true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
record_sort_proc :: proc(i, j: datetime.TZ_Record) -> bool {
|
||||||
|
return i.time > j.time
|
||||||
|
}
|
||||||
|
slice.sort_by(records, record_sort_proc)
|
||||||
|
|
||||||
|
tm_sec := time.to_unix_seconds(tm)
|
||||||
|
for record in records {
|
||||||
|
if tm_sec < record.time {
|
||||||
|
return record, true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return records[len(records)-1], true
|
||||||
|
}
|
||||||
|
|
||||||
|
datetime_to_utc :: proc(dt: datetime.DateTime) -> (out: datetime.DateTime, success: bool) #optional_ok {
|
||||||
|
if dt.tz == nil {
|
||||||
|
return dt, true
|
||||||
|
}
|
||||||
|
|
||||||
|
tm := time.datetime_to_time(dt) or_return
|
||||||
|
record := region_get_nearest(dt.tz, tm) or_return
|
||||||
|
|
||||||
|
secs := time.time_to_unix(tm)
|
||||||
|
adj_time := time.unix(secs - record.utc_offset, 0)
|
||||||
|
adj_dt := time.time_to_datetime(adj_time) or_return
|
||||||
|
return adj_dt, true
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Converts a datetime on one timezone to another timezone
|
||||||
|
|
||||||
|
Inputs:
|
||||||
|
- dt: The input datetime
|
||||||
|
- tz: The timezone to convert to
|
||||||
|
|
||||||
|
NOTE: tz will be referenced in the result datetime, so it must stay alive/allocated as long as it is used
|
||||||
|
Returns:
|
||||||
|
- out: The converted datetime
|
||||||
|
- success: `false` if the datetime was invalid
|
||||||
|
*/
|
||||||
|
datetime_to_tz :: proc(dt: datetime.DateTime, tz: ^datetime.TZ_Region) -> (out: datetime.DateTime, success: bool) #optional_ok {
|
||||||
|
dt := dt
|
||||||
|
if dt.tz == tz {
|
||||||
|
return dt, true
|
||||||
|
}
|
||||||
|
if dt.tz != nil {
|
||||||
|
dt = datetime_to_utc(dt)
|
||||||
|
}
|
||||||
|
if tz == nil {
|
||||||
|
return dt, true
|
||||||
|
}
|
||||||
|
|
||||||
|
tm := time.datetime_to_time(dt) or_return
|
||||||
|
record := region_get_nearest(tz, tm) or_return
|
||||||
|
|
||||||
|
secs := time.time_to_unix(tm)
|
||||||
|
adj_time := time.unix(secs + record.utc_offset, 0)
|
||||||
|
adj_dt := time.time_to_datetime(adj_time) or_return
|
||||||
|
adj_dt.tz = tz
|
||||||
|
|
||||||
|
return adj_dt, true
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Gets the timezone abbreviation/shortname for a given date.
|
||||||
|
(ex: "PDT")
|
||||||
|
|
||||||
|
Inputs:
|
||||||
|
- dt: The datetime containing the date, time, and timezone pointer for the lookup
|
||||||
|
|
||||||
|
NOTE: The lifetime of name matches the timezone it was pulled from.
|
||||||
|
Returns:
|
||||||
|
- name: The timezone abbreviation
|
||||||
|
- success: returns `false` if the passed datetime is invalid
|
||||||
|
*/
|
||||||
|
shortname :: proc(dt: datetime.DateTime) -> (name: string, success: bool) #optional_ok {
|
||||||
|
tm := time.datetime_to_time(dt) or_return
|
||||||
|
if dt.tz == nil { return "UTC", true }
|
||||||
|
|
||||||
|
record := region_get_nearest(dt.tz, tm) or_return
|
||||||
|
return record.shortname, true
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Gets the timezone abbreviation/shortname for a given date.
|
||||||
|
(ex: "PDT")
|
||||||
|
|
||||||
|
WARNING: This is unsafe because it doesn't check if your datetime is valid or if your region contains a valid record.
|
||||||
|
|
||||||
|
Inputs:
|
||||||
|
- dt: The input datetime
|
||||||
|
|
||||||
|
NOTE: The lifetime of name matches the timezone it was pulled from.
|
||||||
|
Returns:
|
||||||
|
- name: The timezone abbreviation
|
||||||
|
*/
|
||||||
|
shortname_unsafe :: proc(dt: datetime.DateTime) -> string {
|
||||||
|
if dt.tz == nil { return "UTC" }
|
||||||
|
|
||||||
|
tm, _ := time.datetime_to_time(dt)
|
||||||
|
record, _ := region_get_nearest(dt.tz, tm)
|
||||||
|
return record.shortname
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Checks DST for a given date.
|
||||||
|
|
||||||
|
Inputs:
|
||||||
|
- dt: The input datetime
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
- is_dst: returns `true` if dt is in daylight savings time, `false` if not
|
||||||
|
- success: returns `false` if the passed datetime is invalid
|
||||||
|
*/
|
||||||
|
dst :: proc(dt: datetime.DateTime) -> (is_dst: bool, success: bool) #optional_ok {
|
||||||
|
tm := time.datetime_to_time(dt) or_return
|
||||||
|
if dt.tz == nil { return false, true }
|
||||||
|
|
||||||
|
record := region_get_nearest(dt.tz, tm) or_return
|
||||||
|
return record.dst, true
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Checks DST for a given date.
|
||||||
|
|
||||||
|
WARNING: This is unsafe because it doesn't check if your datetime is valid or if your region contains a valid record.
|
||||||
|
|
||||||
|
Inputs:
|
||||||
|
- dt: The input datetime
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
- is_dst: returns `true` if dt is in daylight savings time, `false` if not
|
||||||
|
*/
|
||||||
|
dst_unsafe :: proc(dt: datetime.DateTime) -> bool {
|
||||||
|
if dt.tz == nil { return false }
|
||||||
|
|
||||||
|
tm, _ := time.datetime_to_time(dt)
|
||||||
|
record, _ := region_get_nearest(dt.tz, tm)
|
||||||
|
return record.dst
|
||||||
|
}
|
||||||
|
|
||||||
|
datetime_to_str :: proc(dt: datetime.DateTime, allocator := context.allocator) -> string {
|
||||||
|
if dt.tz == nil {
|
||||||
|
_, ok := time.datetime_to_time(dt)
|
||||||
|
if !ok {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.aprintf("%02d-%02d-%04d @ %02d:%02d:%02d UTC", dt.month, dt.day, dt.year, dt.hour, dt.minute, dt.second, allocator = allocator)
|
||||||
|
|
||||||
|
} else {
|
||||||
|
tm, ok := time.datetime_to_time(dt)
|
||||||
|
if !ok {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
record, ok2 := region_get_nearest(dt.tz, tm)
|
||||||
|
if !ok2 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
hour := dt.hour
|
||||||
|
am_pm_str := "AM"
|
||||||
|
if hour > 12 {
|
||||||
|
am_pm_str = "PM"
|
||||||
|
hour -= 12
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.aprintf("%02d-%02d-%04d @ %02d:%02d:%02d %s %s", dt.month, dt.day, dt.year, hour, dt.minute, dt.second, am_pm_str, record.shortname, allocator = allocator)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,652 @@
|
|||||||
|
package timezone
|
||||||
|
|
||||||
|
import "base:intrinsics"
|
||||||
|
|
||||||
|
import "core:slice"
|
||||||
|
import "core:strings"
|
||||||
|
import "core:os"
|
||||||
|
import "core:strconv"
|
||||||
|
import "core:time/datetime"
|
||||||
|
|
||||||
|
// Implementing RFC8536 [https://datatracker.ietf.org/doc/html/rfc8536]
|
||||||
|
|
||||||
|
TZIF_MAGIC :: u32be(0x545A6966) // 'TZif'
|
||||||
|
TZif_Version :: enum u8 {
|
||||||
|
V1 = 0,
|
||||||
|
V2 = '2',
|
||||||
|
V3 = '3',
|
||||||
|
V4 = '4',
|
||||||
|
}
|
||||||
|
BIG_BANG_ISH :: -0x800000000000000
|
||||||
|
|
||||||
|
TZif_Header :: struct #packed {
|
||||||
|
magic: u32be,
|
||||||
|
version: TZif_Version,
|
||||||
|
reserved: [15]u8,
|
||||||
|
isutcnt: u32be,
|
||||||
|
isstdcnt: u32be,
|
||||||
|
leapcnt: u32be,
|
||||||
|
timecnt: u32be,
|
||||||
|
typecnt: u32be,
|
||||||
|
charcnt: u32be,
|
||||||
|
}
|
||||||
|
|
||||||
|
Sun_Shift :: enum u8 {
|
||||||
|
Standard = 0,
|
||||||
|
DST = 1,
|
||||||
|
}
|
||||||
|
|
||||||
|
Local_Time_Type :: struct #packed {
|
||||||
|
utoff: i32be,
|
||||||
|
dst: Sun_Shift,
|
||||||
|
idx: u8,
|
||||||
|
}
|
||||||
|
|
||||||
|
Leapsecond_Record :: struct #packed {
|
||||||
|
occur: i64be,
|
||||||
|
corr: i32be,
|
||||||
|
}
|
||||||
|
|
||||||
|
@private
|
||||||
|
tzif_data_block_size :: proc(hdr: ^TZif_Header, version: TZif_Version) -> (block_size: int, ok: bool) {
|
||||||
|
time_size : int
|
||||||
|
|
||||||
|
if version == .V1 {
|
||||||
|
time_size = 4
|
||||||
|
} else if version == .V2 || version == .V3 || version == .V4 {
|
||||||
|
time_size = 8
|
||||||
|
} else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
return (int(hdr.timecnt) * time_size) +
|
||||||
|
int(hdr.timecnt) +
|
||||||
|
int(hdr.typecnt * size_of(Local_Time_Type)) +
|
||||||
|
int(hdr.charcnt) +
|
||||||
|
(int(hdr.leapcnt) * (time_size + 4)) +
|
||||||
|
int(hdr.isstdcnt) +
|
||||||
|
int(hdr.isutcnt), true
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
load_tzif_file :: proc(filename: string, region_name: string, allocator := context.allocator) -> (out: ^datetime.TZ_Region, ok: bool) {
|
||||||
|
tzif_data := os.read_entire_file_from_filename(filename, allocator) or_return
|
||||||
|
defer delete(tzif_data, allocator)
|
||||||
|
return parse_tzif(tzif_data, region_name, allocator)
|
||||||
|
}
|
||||||
|
|
||||||
|
@private
|
||||||
|
is_alphabetic :: proc(ch: u8) -> bool {
|
||||||
|
// ('A' -> 'Z') || ('a' -> 'z')
|
||||||
|
return (ch > 0x40 && ch < 0x5B) || (ch > 0x60 && ch < 0x7B)
|
||||||
|
}
|
||||||
|
|
||||||
|
@private
|
||||||
|
is_numeric :: proc(ch: u8) -> bool {
|
||||||
|
// ('0' -> '9')
|
||||||
|
return (ch > 0x2F && ch < 0x3A)
|
||||||
|
}
|
||||||
|
|
||||||
|
@private
|
||||||
|
is_alphanumeric :: proc(ch: u8) -> bool {
|
||||||
|
return is_alphabetic(ch) || is_numeric(ch)
|
||||||
|
}
|
||||||
|
|
||||||
|
@private
|
||||||
|
is_valid_quoted_char :: proc(ch: u8) -> bool {
|
||||||
|
return is_alphabetic(ch) || is_numeric(ch) || ch == '+' || ch == '-'
|
||||||
|
}
|
||||||
|
|
||||||
|
@private
|
||||||
|
parse_posix_tz_shortname :: proc(str: string) -> (out: string, idx: int, ok: bool) {
|
||||||
|
was_quoted := false
|
||||||
|
quoted := false
|
||||||
|
i := 0
|
||||||
|
|
||||||
|
for ; i < len(str); i += 1 {
|
||||||
|
ch := str[i]
|
||||||
|
|
||||||
|
if !quoted && ch == '<' {
|
||||||
|
quoted = true
|
||||||
|
was_quoted = true
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if quoted && ch == '>' {
|
||||||
|
quoted = false
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
if !is_valid_quoted_char(ch) && ch != ',' {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if !quoted && !is_alphabetic(ch) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we didn't see the trailing quote
|
||||||
|
if was_quoted && quoted {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
out_str: string
|
||||||
|
end_idx := i
|
||||||
|
if was_quoted {
|
||||||
|
end_idx += 1
|
||||||
|
out_str = str[1:i]
|
||||||
|
} else {
|
||||||
|
out_str = str[:i]
|
||||||
|
}
|
||||||
|
|
||||||
|
return out_str, end_idx, true
|
||||||
|
}
|
||||||
|
|
||||||
|
@private
|
||||||
|
parse_posix_tz_offset :: proc(str: string) -> (out_sec: i64, idx: int, ok: bool) {
|
||||||
|
str := str
|
||||||
|
|
||||||
|
sign : i64 = 1
|
||||||
|
start_idx := 0
|
||||||
|
i := 0
|
||||||
|
if str[i] == '+' {
|
||||||
|
i += 1
|
||||||
|
sign = 1
|
||||||
|
start_idx = 1
|
||||||
|
} else if str[i] == '-' {
|
||||||
|
i += 1
|
||||||
|
sign = -1
|
||||||
|
start_idx = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
got_more_time := false
|
||||||
|
for ; i < len(str); i += 1 {
|
||||||
|
if is_numeric(str[i]) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if str[i] == ':' {
|
||||||
|
got_more_time = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
ret_sec : i64 = 0
|
||||||
|
hours := strconv.parse_int(str[start_idx:i], 10) or_return
|
||||||
|
if hours > 167 || hours < -167 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ret_sec += i64(hours) * (60 * 60)
|
||||||
|
if !got_more_time {
|
||||||
|
return ret_sec * sign, i, true
|
||||||
|
}
|
||||||
|
|
||||||
|
i += 1
|
||||||
|
start_idx = i
|
||||||
|
|
||||||
|
got_more_time = false
|
||||||
|
for ; i < len(str); i += 1 {
|
||||||
|
if is_numeric(str[i]) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if str[i] == ':' {
|
||||||
|
got_more_time = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
mins_str := str[start_idx:i]
|
||||||
|
if len(mins_str) != 2 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
mins := strconv.parse_int(mins_str, 10) or_return
|
||||||
|
if mins > 59 || mins < 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ret_sec += i64(mins) * 60
|
||||||
|
if !got_more_time {
|
||||||
|
return ret_sec * sign, i, true
|
||||||
|
}
|
||||||
|
|
||||||
|
i += 1
|
||||||
|
start_idx = i
|
||||||
|
|
||||||
|
for ; i < len(str); i += 1 {
|
||||||
|
if !is_numeric(str[i]) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
secs_str := str[start_idx:i]
|
||||||
|
if len(secs_str) != 2 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
secs := strconv.parse_int(secs_str, 10) or_return
|
||||||
|
if secs > 59 || secs < 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ret_sec += i64(secs)
|
||||||
|
return ret_sec * sign, i, true
|
||||||
|
}
|
||||||
|
|
||||||
|
@private
|
||||||
|
skim_digits :: proc(str: string) -> (out: string, idx: int, ok: bool) {
|
||||||
|
i := 0
|
||||||
|
for ; i < len(str); i += 1 {
|
||||||
|
ch := str[i]
|
||||||
|
if ch == '.' || ch == '/' || ch == ',' {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
if !is_numeric(ch) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return str[:i], i, true
|
||||||
|
}
|
||||||
|
|
||||||
|
TWO_AM :: 2 * 60 * 60
|
||||||
|
parse_posix_rrule :: proc(str: string) -> (out: datetime.TZ_Transition_Date, idx: int, ok: bool) {
|
||||||
|
str := str
|
||||||
|
if len(str) < 2 { return }
|
||||||
|
|
||||||
|
i := 0
|
||||||
|
// No leap
|
||||||
|
if str[i] == 'J' {
|
||||||
|
i += 1
|
||||||
|
|
||||||
|
day_str, off := skim_digits(str[i:]) or_return
|
||||||
|
i += off
|
||||||
|
|
||||||
|
day := strconv.parse_int(day_str, 10) or_return
|
||||||
|
if day < 1 || day > 365 { return }
|
||||||
|
|
||||||
|
offset : i64 = TWO_AM
|
||||||
|
if len(str) != i && str[i] == '/' {
|
||||||
|
i += 1
|
||||||
|
|
||||||
|
offset, off = parse_posix_tz_offset(str[i:]) or_return
|
||||||
|
i += off
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(str) != i && str[i] == ',' {
|
||||||
|
i += 1
|
||||||
|
}
|
||||||
|
|
||||||
|
return datetime.TZ_Transition_Date{
|
||||||
|
type = .No_Leap,
|
||||||
|
day = u16(day),
|
||||||
|
time = offset,
|
||||||
|
}, i, true
|
||||||
|
|
||||||
|
// Leap
|
||||||
|
} else if is_numeric(str[i]) {
|
||||||
|
day_str, off := skim_digits(str[i:]) or_return
|
||||||
|
i += off
|
||||||
|
|
||||||
|
day := strconv.parse_int(day_str, 10) or_return
|
||||||
|
if day < 0 || day > 365 { return }
|
||||||
|
|
||||||
|
offset : i64 = TWO_AM
|
||||||
|
if len(str) != i && str[i] == '/' {
|
||||||
|
i += 1
|
||||||
|
|
||||||
|
offset, off = parse_posix_tz_offset(str[i:]) or_return
|
||||||
|
i += off
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(str) != i && str[i] == ',' {
|
||||||
|
i += 1
|
||||||
|
}
|
||||||
|
|
||||||
|
return datetime.TZ_Transition_Date{
|
||||||
|
type = .Leap,
|
||||||
|
day = u16(day),
|
||||||
|
time = offset,
|
||||||
|
}, i, true
|
||||||
|
|
||||||
|
} else if str[i] == 'M' {
|
||||||
|
i += 1
|
||||||
|
|
||||||
|
month_str, week_str, day_str: string
|
||||||
|
off := 0
|
||||||
|
|
||||||
|
month_str, off = skim_digits(str[i:]) or_return
|
||||||
|
i += off + 1
|
||||||
|
|
||||||
|
week_str, off = skim_digits(str[i:]) or_return
|
||||||
|
i += off + 1
|
||||||
|
|
||||||
|
day_str, off = skim_digits(str[i:]) or_return
|
||||||
|
i += off
|
||||||
|
|
||||||
|
month := strconv.parse_int(month_str, 10) or_return
|
||||||
|
if month < 1 || month > 12 { return }
|
||||||
|
|
||||||
|
week := strconv.parse_int(week_str, 10) or_return
|
||||||
|
if week < 1 || week > 5 { return }
|
||||||
|
|
||||||
|
day := strconv.parse_int(day_str, 10) or_return
|
||||||
|
if day < 0 || day > 6 { return }
|
||||||
|
|
||||||
|
offset : i64 = TWO_AM
|
||||||
|
if len(str) != i && str[i] == '/' {
|
||||||
|
i += 1
|
||||||
|
|
||||||
|
offset, off = parse_posix_tz_offset(str[i:]) or_return
|
||||||
|
i += off
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(str) != i && str[i] == ',' {
|
||||||
|
i += 1
|
||||||
|
}
|
||||||
|
|
||||||
|
return datetime.TZ_Transition_Date{
|
||||||
|
type = .Month_Week_Day,
|
||||||
|
month = u8(month),
|
||||||
|
week = u8(week),
|
||||||
|
day = u16(day),
|
||||||
|
time = offset,
|
||||||
|
}, i, true
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
parse_posix_tz :: proc(posix_tz: string, allocator := context.allocator) -> (out: datetime.TZ_RRule, ok: bool) {
|
||||||
|
// TZ string contain at least 3 characters for the STD name, and 1 for the offset
|
||||||
|
if len(posix_tz) < 4 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
str := posix_tz
|
||||||
|
|
||||||
|
std_name, idx := parse_posix_tz_shortname(str) or_return
|
||||||
|
str = str[idx:]
|
||||||
|
|
||||||
|
std_offset, idx2 := parse_posix_tz_offset(str) or_return
|
||||||
|
std_offset *= -1
|
||||||
|
str = str[idx2:]
|
||||||
|
|
||||||
|
std_name_str, err := strings.clone(std_name, allocator)
|
||||||
|
if err != nil { return }
|
||||||
|
defer if !ok { delete(std_name_str, allocator) }
|
||||||
|
|
||||||
|
if len(str) == 0 {
|
||||||
|
return datetime.TZ_RRule{
|
||||||
|
has_dst = false,
|
||||||
|
std_name = std_name_str,
|
||||||
|
std_offset = std_offset,
|
||||||
|
std_date = datetime.TZ_Transition_Date{
|
||||||
|
type = .Leap,
|
||||||
|
day = 0,
|
||||||
|
time = TWO_AM,
|
||||||
|
},
|
||||||
|
}, true
|
||||||
|
}
|
||||||
|
|
||||||
|
dst_name: string
|
||||||
|
dst_offset := std_offset + (1 * 60 * 60)
|
||||||
|
if str[0] != ',' {
|
||||||
|
dst_name, idx = parse_posix_tz_shortname(str) or_return
|
||||||
|
str = str[idx:]
|
||||||
|
|
||||||
|
if str[0] != ',' {
|
||||||
|
dst_offset, idx = parse_posix_tz_offset(str) or_return
|
||||||
|
dst_offset *= -1
|
||||||
|
str = str[idx:]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if str[0] != ',' { return }
|
||||||
|
str = str[1:]
|
||||||
|
|
||||||
|
std_td, idx3 := parse_posix_rrule(str) or_return
|
||||||
|
str = str[idx3:]
|
||||||
|
|
||||||
|
dst_td, idx4 := parse_posix_rrule(str) or_return
|
||||||
|
str = str[idx4:]
|
||||||
|
|
||||||
|
dst_name_str: string
|
||||||
|
dst_name_str, err = strings.clone(dst_name, allocator)
|
||||||
|
if err != nil { return }
|
||||||
|
|
||||||
|
return datetime.TZ_RRule{
|
||||||
|
has_dst = true,
|
||||||
|
|
||||||
|
std_name = std_name_str,
|
||||||
|
std_offset = std_offset,
|
||||||
|
std_date = std_td,
|
||||||
|
|
||||||
|
dst_name = dst_name_str,
|
||||||
|
dst_offset = dst_offset,
|
||||||
|
dst_date = dst_td,
|
||||||
|
}, true
|
||||||
|
}
|
||||||
|
|
||||||
|
parse_tzif :: proc(_buffer: []u8, region_name: string, allocator := context.allocator) -> (out: ^datetime.TZ_Region, ok: bool) {
|
||||||
|
context.allocator = allocator
|
||||||
|
|
||||||
|
buffer := _buffer
|
||||||
|
|
||||||
|
// TZif is crufty. Skip the initial header.
|
||||||
|
|
||||||
|
v1_hdr := slice.to_type(buffer, TZif_Header) or_return
|
||||||
|
if v1_hdr.magic != TZIF_MAGIC {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if v1_hdr.typecnt == 0 || v1_hdr.charcnt == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if v1_hdr.isutcnt != 0 && v1_hdr.isutcnt != v1_hdr.typecnt {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if v1_hdr.isstdcnt != 0 && v1_hdr.isstdcnt != v1_hdr.typecnt {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// We don't bother supporting v1, it uses u32 timestamps
|
||||||
|
if v1_hdr.version == .V1 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// We only support v2 and v3
|
||||||
|
if v1_hdr.version != .V2 && v1_hdr.version != .V3 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Skip the initial v1 block too.
|
||||||
|
first_block_size, _ := tzif_data_block_size(&v1_hdr, .V1)
|
||||||
|
if len(buffer) <= size_of(v1_hdr) + first_block_size {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
buffer = buffer[size_of(v1_hdr)+first_block_size:]
|
||||||
|
|
||||||
|
// Ok, time to parse real things
|
||||||
|
real_hdr := slice.to_type(buffer, TZif_Header) or_return
|
||||||
|
if real_hdr.magic != TZIF_MAGIC {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if real_hdr.typecnt == 0 || real_hdr.charcnt == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if real_hdr.isutcnt != 0 && real_hdr.isutcnt != real_hdr.typecnt {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if real_hdr.isstdcnt != 0 && real_hdr.isstdcnt != real_hdr.typecnt {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Grab the real data block
|
||||||
|
real_block_size, _ := tzif_data_block_size(&real_hdr, v1_hdr.version)
|
||||||
|
if len(buffer) <= size_of(real_hdr) + real_block_size {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
buffer = buffer[size_of(real_hdr):]
|
||||||
|
|
||||||
|
time_size := 8
|
||||||
|
transition_times := slice.reinterpret([]i64be, buffer[:int(real_hdr.timecnt)*size_of(i64be)])
|
||||||
|
for time in transition_times {
|
||||||
|
if time < BIG_BANG_ISH {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
buffer = buffer[int(real_hdr.timecnt)*time_size:]
|
||||||
|
|
||||||
|
transition_types := buffer[:int(real_hdr.timecnt)]
|
||||||
|
for type in transition_types {
|
||||||
|
if int(type) > int(real_hdr.typecnt - 1) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
buffer = buffer[int(real_hdr.timecnt):]
|
||||||
|
|
||||||
|
local_time_types := slice.reinterpret([]Local_Time_Type, buffer[:int(real_hdr.typecnt)*size_of(Local_Time_Type)])
|
||||||
|
for ltt in local_time_types {
|
||||||
|
// UT offset should be > -25 hours and < 26 hours
|
||||||
|
if int(ltt.utoff) < -89999 || int(ltt.utoff) > 93599 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if ltt.dst != .DST && ltt.dst != .Standard {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if int(ltt.idx) > int(real_hdr.charcnt - 1) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
buffer = buffer[int(real_hdr.typecnt) * size_of(Local_Time_Type):]
|
||||||
|
timezone_string_table := buffer[:real_hdr.charcnt]
|
||||||
|
buffer = buffer[real_hdr.charcnt:]
|
||||||
|
|
||||||
|
leapsecond_records := slice.reinterpret([]Leapsecond_Record, buffer[:int(real_hdr.leapcnt)*size_of(Leapsecond_Record)])
|
||||||
|
if len(leapsecond_records) > 0 {
|
||||||
|
if leapsecond_records[0].occur < 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
buffer = buffer[(int(real_hdr.leapcnt) * size_of(Leapsecond_Record)):]
|
||||||
|
|
||||||
|
standard_wall_tags := buffer[:int(real_hdr.isstdcnt)]
|
||||||
|
buffer = buffer[int(real_hdr.isstdcnt):]
|
||||||
|
|
||||||
|
ut_tags := buffer[:int(real_hdr.isutcnt)]
|
||||||
|
|
||||||
|
for stdwall_tag, idx in standard_wall_tags {
|
||||||
|
ut_tag := ut_tags[idx]
|
||||||
|
|
||||||
|
if (stdwall_tag != 0 && stdwall_tag != 1) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (ut_tag != 0 && ut_tag != 1) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if ut_tag == 1 && stdwall_tag != 1 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
buffer = buffer[int(real_hdr.isutcnt):]
|
||||||
|
|
||||||
|
// Start of footer
|
||||||
|
if buffer[0] != '\n' {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
buffer = buffer[1:]
|
||||||
|
|
||||||
|
if buffer[0] == ':' {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
end_idx := 0
|
||||||
|
for ch in buffer {
|
||||||
|
if ch == '\n' {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
if ch == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
end_idx += 1
|
||||||
|
}
|
||||||
|
footer_str := string(buffer[:end_idx])
|
||||||
|
|
||||||
|
// UTC is a special case, we don't need to alloc
|
||||||
|
if len(local_time_types) == 1 {
|
||||||
|
name := cstring(raw_data(timezone_string_table[local_time_types[0].idx:]))
|
||||||
|
if name != "UTC" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, true
|
||||||
|
}
|
||||||
|
|
||||||
|
ltt_names, err := make([dynamic]string, 0, len(local_time_types), allocator)
|
||||||
|
if err != nil { return }
|
||||||
|
defer if err != nil {
|
||||||
|
for name in ltt_names {
|
||||||
|
delete(name, allocator)
|
||||||
|
}
|
||||||
|
delete(ltt_names)
|
||||||
|
}
|
||||||
|
|
||||||
|
for ltt in local_time_types {
|
||||||
|
name := cstring(raw_data(timezone_string_table[ltt.idx:]))
|
||||||
|
ltt_name: string
|
||||||
|
|
||||||
|
ltt_name, err = strings.clone_from_cstring_bounded(name, len(timezone_string_table), allocator)
|
||||||
|
if err != nil { return }
|
||||||
|
|
||||||
|
append(<t_names, ltt_name)
|
||||||
|
}
|
||||||
|
|
||||||
|
records: []datetime.TZ_Record
|
||||||
|
records, err = make([]datetime.TZ_Record, len(transition_times), allocator)
|
||||||
|
if err != nil { return }
|
||||||
|
defer if err != nil { delete(records, allocator) }
|
||||||
|
|
||||||
|
for trans_time, idx in transition_times {
|
||||||
|
trans_idx := transition_types[idx]
|
||||||
|
ltt := local_time_types[trans_idx]
|
||||||
|
|
||||||
|
records[idx] = datetime.TZ_Record{
|
||||||
|
time = i64(trans_time),
|
||||||
|
utc_offset = i64(ltt.utoff),
|
||||||
|
shortname = ltt_names[trans_idx],
|
||||||
|
dst = bool(ltt.dst),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
rrule, ok2 := parse_posix_tz(footer_str, allocator)
|
||||||
|
if !ok2 { return }
|
||||||
|
defer if err != nil {
|
||||||
|
delete(rrule.std_name, allocator)
|
||||||
|
delete(rrule.dst_name, allocator)
|
||||||
|
}
|
||||||
|
|
||||||
|
region_name_out: string
|
||||||
|
region_name_out, err = strings.clone(region_name, allocator)
|
||||||
|
if err != nil { return }
|
||||||
|
defer if err != nil { delete(region_name_out, allocator) }
|
||||||
|
|
||||||
|
region: ^datetime.TZ_Region
|
||||||
|
region, err = new_clone(datetime.TZ_Region{
|
||||||
|
records = records,
|
||||||
|
shortnames = ltt_names[:],
|
||||||
|
name = region_name_out,
|
||||||
|
rrule = rrule,
|
||||||
|
}, allocator)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
return region, true
|
||||||
|
}
|
||||||
@@ -3,6 +3,7 @@ package test_core_time
|
|||||||
import "core:testing"
|
import "core:testing"
|
||||||
import "core:time"
|
import "core:time"
|
||||||
import dt "core:time/datetime"
|
import dt "core:time/datetime"
|
||||||
|
import tz "core:time/timezone"
|
||||||
|
|
||||||
is_leap_year :: time.is_leap_year
|
is_leap_year :: time.is_leap_year
|
||||||
|
|
||||||
@@ -349,3 +350,214 @@ date_component_roundtrip_test :: proc(t: ^testing.T, moment: dt.DateTime) {
|
|||||||
moment.year, moment.month, moment.day, moment.hour, moment.minute, moment.second, YYYY, MM, DD, hh, mm, ss,
|
moment.year, moment.month, moment.day, moment.hour, moment.minute, moment.second, YYYY, MM, DD, hh, mm, ss,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
datetime_eq :: proc(dt1: dt.DateTime, dt2: dt.DateTime) -> bool {
|
||||||
|
return (
|
||||||
|
dt1.year == dt2.year && dt1.month == dt2.month && dt1.day == dt2.day &&
|
||||||
|
dt1.hour == dt2.hour && dt1.minute == dt2.minute && dt1.second == dt2.second
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@test
|
||||||
|
test_convert_timezone_roundtrip :: proc(t: ^testing.T) {
|
||||||
|
dst_dt, _ := dt.components_to_datetime(2024, 10, 4, 23, 47, 0)
|
||||||
|
std_dt, _ := dt.components_to_datetime(2024, 11, 4, 23, 47, 0)
|
||||||
|
|
||||||
|
local_tz, local_load_ok := tz.region_load("local")
|
||||||
|
testing.expectf(t, local_load_ok, "Failed to load local timezone")
|
||||||
|
defer tz.region_destroy(local_tz)
|
||||||
|
|
||||||
|
edm_tz, edm_load_ok := tz.region_load("America/Edmonton")
|
||||||
|
testing.expectf(t, edm_load_ok, "Failed to load America/Edmonton timezone")
|
||||||
|
defer tz.region_destroy(edm_tz)
|
||||||
|
|
||||||
|
shuffle_tz :: proc(start_dt: dt.DateTime, test_tz: ^dt.TZ_Region) -> dt.DateTime {
|
||||||
|
tz_dt := tz.datetime_to_tz(start_dt, test_tz)
|
||||||
|
utc_dt := tz.datetime_to_utc(tz_dt)
|
||||||
|
return utc_dt
|
||||||
|
}
|
||||||
|
|
||||||
|
testing.expectf(t, datetime_eq(dst_dt, shuffle_tz(dst_dt, local_tz)), "Failed to convert to/from local dst timezone")
|
||||||
|
testing.expectf(t, datetime_eq(std_dt, shuffle_tz(std_dt, local_tz)), "Failed to convert to/from local std timezone")
|
||||||
|
testing.expectf(t, datetime_eq(dst_dt, shuffle_tz(dst_dt, edm_tz)), "Failed to convert to/from Edmonton dst timezone")
|
||||||
|
testing.expectf(t, datetime_eq(std_dt, shuffle_tz(std_dt, edm_tz)), "Failed to convert to/from Edmonton std timezone")
|
||||||
|
}
|
||||||
|
|
||||||
|
@test
|
||||||
|
test_check_timezone_metadata :: proc(t: ^testing.T) {
|
||||||
|
dst_dt, _ := dt.components_to_datetime(2024, 10, 4, 23, 47, 0)
|
||||||
|
std_dt, _ := dt.components_to_datetime(2024, 11, 4, 23, 47, 0)
|
||||||
|
|
||||||
|
pac_tz, pac_load_ok := tz.region_load("America/Los_Angeles")
|
||||||
|
testing.expectf(t, pac_load_ok, "Failed to load America/Los_Angeles timezone")
|
||||||
|
defer tz.region_destroy(pac_tz)
|
||||||
|
|
||||||
|
pac_dst_dt := tz.datetime_to_tz(dst_dt, pac_tz)
|
||||||
|
pac_std_dt := tz.datetime_to_tz(std_dt, pac_tz)
|
||||||
|
testing.expectf(t, tz.shortname_unsafe(pac_dst_dt) == "PDT", "Invalid timezone shortname")
|
||||||
|
testing.expectf(t, tz.shortname_unsafe(pac_std_dt) == "PST", "Invalid timezone shortname")
|
||||||
|
testing.expectf(t, tz.dst_unsafe(pac_std_dt) == false, "Expected daylight savings == false, got true")
|
||||||
|
testing.expectf(t, tz.dst_unsafe(pac_dst_dt) == true, "Expected daylight savings == true, got false")
|
||||||
|
|
||||||
|
pac_dst_name, ok := tz.shortname(pac_dst_dt)
|
||||||
|
testing.expectf(t, ok == true, "Invalid datetime")
|
||||||
|
testing.expectf(t, pac_dst_name == "PDT", "Invalid timezone shortname")
|
||||||
|
|
||||||
|
pac_std_name, ok2 := tz.shortname(pac_std_dt)
|
||||||
|
testing.expectf(t, ok2 == true, "Invalid datetime")
|
||||||
|
testing.expectf(t, pac_std_name == "PST", "Invalid timezone shortname")
|
||||||
|
|
||||||
|
pac_is_dst, ok3 := tz.dst(pac_dst_dt)
|
||||||
|
testing.expectf(t, ok3 == true, "Invalid datetime")
|
||||||
|
testing.expectf(t, pac_is_dst == true, "Expected daylight savings == false, got true")
|
||||||
|
|
||||||
|
pac_is_dst, ok3 = tz.dst(pac_std_dt)
|
||||||
|
testing.expectf(t, ok3 == true, "Invalid datetime")
|
||||||
|
testing.expectf(t, pac_is_dst == false, "Expected daylight savings == false, got true")
|
||||||
|
}
|
||||||
|
|
||||||
|
rrule_eq :: proc(r1, r2: dt.TZ_RRule) -> (eq: bool) {
|
||||||
|
if r1.has_dst != r2.has_dst { return }
|
||||||
|
|
||||||
|
if r1.std_name != r2.std_name { return }
|
||||||
|
if r1.std_offset != r2.std_offset { return }
|
||||||
|
if r1.std_date != r2.std_date { return }
|
||||||
|
|
||||||
|
if r1.dst_name != r2.dst_name { return }
|
||||||
|
if r1.dst_offset != r2.dst_offset { return }
|
||||||
|
if r1.dst_date != r2.dst_date { return }
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
@test
|
||||||
|
test_check_timezone_posix_tz :: proc(t: ^testing.T) {
|
||||||
|
correct_simple_rrule := dt.TZ_RRule{
|
||||||
|
has_dst = false,
|
||||||
|
|
||||||
|
std_name = "UTC",
|
||||||
|
std_offset = -(5 * 60 * 60),
|
||||||
|
std_date = dt.TZ_Transition_Date{
|
||||||
|
type = .Leap,
|
||||||
|
day = 0,
|
||||||
|
time = 2 * 60 * 60,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
simple_rrule, simple_rrule_ok := tz.parse_posix_tz("UTC+5")
|
||||||
|
testing.expectf(t, simple_rrule_ok, "Failed to parse posix tz")
|
||||||
|
defer tz.rrule_destroy(simple_rrule)
|
||||||
|
testing.expectf(t, rrule_eq(simple_rrule, correct_simple_rrule), "POSIX TZ parsed incorrectly")
|
||||||
|
|
||||||
|
correct_est_rrule := dt.TZ_RRule{
|
||||||
|
has_dst = true,
|
||||||
|
|
||||||
|
std_name = "EST",
|
||||||
|
std_offset = -(5 * 60 * 60),
|
||||||
|
std_date = dt.TZ_Transition_Date{
|
||||||
|
type = .Month_Week_Day,
|
||||||
|
month = 3,
|
||||||
|
week = 2,
|
||||||
|
day = 0,
|
||||||
|
time = 2 * 60 * 60,
|
||||||
|
},
|
||||||
|
|
||||||
|
dst_name = "EDT",
|
||||||
|
dst_offset = -(4 * 60 * 60),
|
||||||
|
dst_date = dt.TZ_Transition_Date{
|
||||||
|
type = .Month_Week_Day,
|
||||||
|
month = 11,
|
||||||
|
week = 1,
|
||||||
|
day = 0,
|
||||||
|
time = 2 * 60 * 60,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
est_rrule, est_rrule_ok := tz.parse_posix_tz("EST+5EDT,M3.2.0/2,M11.1.0/2")
|
||||||
|
testing.expectf(t, est_rrule_ok, "Failed to parse posix tz")
|
||||||
|
defer tz.rrule_destroy(est_rrule)
|
||||||
|
testing.expectf(t, rrule_eq(est_rrule, correct_est_rrule), "POSIX TZ parsed incorrectly")
|
||||||
|
|
||||||
|
correct_ist_rrule := dt.TZ_RRule{
|
||||||
|
has_dst = true,
|
||||||
|
|
||||||
|
std_name = "IST",
|
||||||
|
std_offset = (2 * 60 * 60),
|
||||||
|
std_date = dt.TZ_Transition_Date{
|
||||||
|
type = .Month_Week_Day,
|
||||||
|
month = 3,
|
||||||
|
week = 4,
|
||||||
|
day = 4,
|
||||||
|
time = 26 * 60 * 60,
|
||||||
|
},
|
||||||
|
|
||||||
|
dst_name = "IDT",
|
||||||
|
dst_offset = (3 * 60 * 60),
|
||||||
|
dst_date = dt.TZ_Transition_Date{
|
||||||
|
type = .Month_Week_Day,
|
||||||
|
month = 10,
|
||||||
|
week = 5,
|
||||||
|
day = 0,
|
||||||
|
time = 2 * 60 * 60,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
ist_rrule, ist_rrule_ok := tz.parse_posix_tz("IST-2IDT,M3.4.4/26,M10.5.0")
|
||||||
|
testing.expectf(t, ist_rrule_ok, "Failed to parse posix tz")
|
||||||
|
defer tz.rrule_destroy(ist_rrule)
|
||||||
|
testing.expectf(t, rrule_eq(ist_rrule, correct_ist_rrule), "POSIX TZ parsed incorrectly")
|
||||||
|
|
||||||
|
correct_warst_rrule := dt.TZ_RRule{
|
||||||
|
has_dst = true,
|
||||||
|
|
||||||
|
std_name = "WART",
|
||||||
|
std_offset = -(4 * 60 * 60),
|
||||||
|
std_date = dt.TZ_Transition_Date{
|
||||||
|
type = .No_Leap,
|
||||||
|
day = 1,
|
||||||
|
time = 0 * 60 * 60,
|
||||||
|
},
|
||||||
|
|
||||||
|
dst_name = "WARST",
|
||||||
|
dst_offset = -(3 * 60 * 60),
|
||||||
|
dst_date = dt.TZ_Transition_Date{
|
||||||
|
type = .No_Leap,
|
||||||
|
day = 365,
|
||||||
|
time = 25 * 60 * 60,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
warst_rrule, warst_rrule_ok := tz.parse_posix_tz("WART4WARST,J1/0,J365/25")
|
||||||
|
testing.expectf(t, warst_rrule_ok, "Failed to parse posix tz")
|
||||||
|
defer tz.rrule_destroy(warst_rrule)
|
||||||
|
testing.expectf(t, rrule_eq(warst_rrule, correct_warst_rrule), "POSIX TZ parsed incorrectly")
|
||||||
|
|
||||||
|
correct_wgt_rrule := dt.TZ_RRule{
|
||||||
|
has_dst = true,
|
||||||
|
|
||||||
|
std_name = "WGT",
|
||||||
|
std_offset = -(3 * 60 * 60),
|
||||||
|
std_date = dt.TZ_Transition_Date{
|
||||||
|
type = .Month_Week_Day,
|
||||||
|
month = 3,
|
||||||
|
week = 5,
|
||||||
|
day = 0,
|
||||||
|
time = -2 * 60 * 60,
|
||||||
|
},
|
||||||
|
|
||||||
|
dst_name = "WGST",
|
||||||
|
dst_offset = -(2 * 60 * 60),
|
||||||
|
dst_date = dt.TZ_Transition_Date{
|
||||||
|
type = .Month_Week_Day,
|
||||||
|
month = 10,
|
||||||
|
week = 5,
|
||||||
|
day = 0,
|
||||||
|
time = -1 * 60 * 60,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
wgt_rrule, wgt_rrule_ok := tz.parse_posix_tz("WGT3WGST,M3.5.0/-2,M10.5.0/-1")
|
||||||
|
testing.expectf(t, wgt_rrule_ok, "Failed to parse posix tz")
|
||||||
|
defer tz.rrule_destroy(wgt_rrule)
|
||||||
|
testing.expectf(t, rrule_eq(wgt_rrule, correct_wgt_rrule), "POSIX TZ parsed incorrectly")
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user