diff --git a/std/time/go.jule b/std/time/go.jule new file mode 100644 index 00000000..e1febcac --- /dev/null +++ b/std/time/go.jule @@ -0,0 +1,41 @@ +// Copyright 2024 The Jule Programming Language. +// Use of this source code is governed by a BSD 3-Clause +// license that can be found in the LICENSE file. + +// The Jule implementation of Unix time conversions, TZ-handling and some other +// algoritmhs is based on the original Go code. +// +// The Jule implementation will not follows Go code. +// It only uses Go implementation as base implementation. +// API may be work/implemented different. +// +// The original Go code came with this notice: +// ==================================================== +// Copyright (c) 2009 The Go Authors. All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following disclaimer +// in the documentation and/or other materials provided with the +// distribution. +// * Neither the name of Google Inc. nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +// ==================================================== \ No newline at end of file diff --git a/std/time/sys.jule b/std/time/sys.jule new file mode 100644 index 00000000..05e42454 --- /dev/null +++ b/std/time/sys.jule @@ -0,0 +1,8 @@ +// Copyright 2024 The Jule Programming Language. +// Use of this source code is governed by a BSD 3-Clause +// license that can be found in the LICENSE file. + +// Copies of os.Seek.* enum fields to avoid importing "std/os": +const seekStart = 0 +const seekCurrent = 1 +const seekEnd = 2 \ No newline at end of file diff --git a/std/time/sys_unix.jule b/std/time/sys_unix.jule new file mode 100644 index 00000000..2183010d --- /dev/null +++ b/std/time/sys_unix.jule @@ -0,0 +1,48 @@ +// Copyright 2024 The Jule Programming Language. +// Use of this source code is governed by a BSD 3-Clause +// license that can be found in the LICENSE file. + +use integ "std/jule/integrated" +use "std/sys" + +fn open(name: str): (uintptr, ok: bool) { + sName := integ::StrToBytes(name) + fd := unsafe { sys::Open(&sName[0], sys::O_RDONLY, 0) } + if fd == -1 { + ret 0, false + } + ret uintptr(fd), true +} + +fn read(fd: uintptr, mut buf: []byte): (n: int, ok: bool) { + if len(buf) == 0 { + // If the caller wanted a zero byte read, return immediately + // without trying to read. + ret 0, true + } + n = unsafe { sys::Read(int(fd), &buf[0], uint(len(buf))) } + ok = n != -1 + ret +} + +fn preadn(fd: uintptr, mut buf: []byte, off: int): (ok: bool) { + mut whence := seekStart + if off < 0 { + whence = seekEnd + } + if sys::Seek(int(fd), off, whence) == -1 { + ret false + } + for len(buf) > 0 { + m := unsafe { sys::Read(int(fd), &buf[0], uint(len(buf))) } + if m <= 0 { + ret false + } + buf = buf[m:] + } + ret true +} + +fn closefd(fd: uintptr) { + sys::Close(int(fd)) +} \ No newline at end of file diff --git a/std/time/sys_windows.jule b/std/time/sys_windows.jule new file mode 100644 index 00000000..3f2ed2e7 --- /dev/null +++ b/std/time/sys_windows.jule @@ -0,0 +1,48 @@ +// Copyright 2024 The Jule Programming Language. +// Use of this source code is governed by a BSD 3-Clause +// license that can be found in the LICENSE file. + +use integ "std/jule/integrated" +use "std/sys" + +fn open(name: str): (uintptr, ok: bool) { + sName := integ::UTF16FromStr(name) + fd := unsafe { sys::Wopen(&sName[0], sys::O_RDONLY, 0) } + if fd == -1 { + ret 0, false + } + ret uintptr(fd), true +} + +fn read(fd: uintptr, mut buf: []byte): (n: int, ok: bool) { + if len(buf) == 0 { + // If the caller wanted a zero byte read, return immediately + // without trying to read. + ret 0, true + } + n = unsafe { sys::Read(int(fd), &buf[0], uint(len(buf))) } + ok = n != -1 + ret +} + +fn preadn(fd: uintptr, mut buf: []byte, off: int): (ok: bool) { + mut whence := seekStart + if off < 0 { + whence = seekEnd + } + if sys::Seek(int(fd), off, whence) == -1 { + ret false + } + for len(buf) > 0 { + m := unsafe { sys::Read(int(fd), &buf[0], uint(len(buf))) } + if m <= 0 { + ret false + } + buf = buf[m:] + } + ret true +} + +fn closefd(fd: uintptr) { + sys::Close(int(fd)) +} diff --git a/std/time/time.jule b/std/time/time.jule index 0e71ed87..9a40210c 100644 --- a/std/time/time.jule +++ b/std/time/time.jule @@ -2,6 +2,7 @@ // Use of this source code is governed by a BSD 3-Clause // license that can be found in the LICENSE file. +use "std/math/bits" use "std/runtime" // The unsigned zero year for internal Unix time calculations. @@ -31,6 +32,41 @@ const October = Month(10) const November = Month(11) const December = Month(12) +fn daysIn(m: Month, year: int): int { + if m == February { + if isLeap(year) { + ret 29 + } + ret 28 + } + // With the special case of February eliminated, the pattern is + // 31 30 31 30 31 30 31 31 30 31 30 31 + // Adding m&1 produces the basic alternation; + // adding (m>>3)&1 inverts the alternation starting in August. + ret 30 + int((m+m>>3)&1) +} + +// daysBefore returns the number of days in a non-leap year before month m. +// daysBefore(December+1) returns 365. +fn daysBefore(m: Month): int { + mut adj := 0 + if m >= March { + adj = -2 + } + + // With the -2 adjustment after February, + // we need to compute the running sum of: + // 0 31 30 31 30 31 30 31 31 30 31 30 31 + // which is: + // 0 31 61 92 122 153 183 214 245 275 306 336 367 + // This is almost exactly 367/12×(m-1) except for the + // occasonal off-by-one suggesting there may be an + // integer approximation of the form (a×m + b)/c. + // A brute force search over small a, b, c finds that + // (214×m - 211) / 7 computes the function perfectly. + ret (214*int(m)-211)/7 + adj +} + // Specifies a day of the week (Sunday = 0, ...). type Weekday: int @@ -51,6 +87,8 @@ const Saturday = Weekday(6) struct Time { sec: i64 nsec: i32 // In the range [0, 999999999]. + + mut loc: &Location } impl Time { @@ -59,10 +97,44 @@ impl Time { ret self.sec } - // Returns the time as an absolute time. + fn setLoc(mut self, mut &loc: Location) { + if &loc == &utcLoc { + self.loc = nil + ret + } + self.loc = unsafe { (&Location)(&loc) } + } + + // Returns time with the location set to UTC. + fn UTC(self): Time { + mut t := self + t.setLoc(utcLoc) + ret t + } + + // Returns time with the location set to local time. + fn Local(self): Time { + mut t := self + t.setLoc(localLoc) + ret t + } + + // Returns the time as an absolute time, adjusted by the zone offset. // It is called when computing a presentation property like Month or Hour. fn abs(self): u64 { - sec := self.Unix() + mut l := self.loc + if l == nil || l == Local { + l = l.get() + } + mut sec := self.Unix() + if l != UTC { + if l.cacheZone != nil && l.cacheStart <= sec && sec < l.cacheEnd { + sec += i64(l.cacheZone.offset) + } else { + _, offset, _, _, _ := l.lookup(sec) + sec += i64(offset) + } + } ret u64(sec + unixToAbsolute) } @@ -223,13 +295,13 @@ fn absDate(abs: u64, full: bool): (year: int, month: Month, day: int, yday: int) // Estimate month on assumption that every month has 31 days. // The estimate may be too low by at most one month, so adjust. month = Month(day / 31) - end := int(daysBefore[month+1]) + end := int(_daysBefore[month+1]) mut begin := 0 if day >= end { month++ begin = end } else { - begin = int(daysBefore[month]) + begin = int(_daysBefore[month]) } month++ // because January is 1 @@ -237,13 +309,14 @@ fn absDate(abs: u64, full: bool): (year: int, month: Month, day: int, yday: int) ret } -// Returns the current system time with UTC local. +// Returns the current system-time UTC. fn Now(): Time { sec, nsec := runtime::timeNow() ret Time{sec: sec, nsec: i32(nsec)} } // Returns new time by Unix time with nanoseconds. +// Seconds since January 1, 1970 UTC. // It is valid to pass nsec outside the range [0, 999999999]. // Not all sec values have a corresponding time value. One such // value is 1<<63-1 (the largest i64 value). @@ -283,6 +356,7 @@ const daysPer100Y = daysPerY*100 + 24 const daysPer4Y = daysPerY*4 + 1 // Returns new absolute time by Unix time without nanoseconds. +// Seconds since January 1, 1970 UTC. fn UnixAbs(sec: i64): AbsTime { abs := u64(sec) + unixToAbsolute mut t := AbsTime{} @@ -338,10 +412,10 @@ fn norm(mut hi: int, mut lo: int, base: int): (nhi: int, nlo: int) { ret hi, lo } -// daysBefore[m] counts the number of days in a non-leap year +// _daysBefore[m] counts the number of days in a non-leap year // before month m begins. There is an entry for m=12, counting // the number of days before January of next year (365). -static daysBefore: [...]i32 = [ +static _daysBefore: [...]i32 = [ 0, 31, 31 + 28, @@ -362,7 +436,7 @@ static daysBefore: [...]i32 = [ // stored in nsecond after normalization. // See the Date function for public documentation. fn absUnix(mut year: int, mut month: Month, mut day: int, - mut hour: int, mut minute: int, mut second: int, mut &nsecond: int): i64 { + mut hour: int, mut minute: int, mut second: int, mut &nsecond: int, mut &loc: &Location): i64 { // Normalize month, overflowing into year. mut m := int(month - 1) year, m = norm(year, m, 12) @@ -378,7 +452,7 @@ fn absUnix(mut year: int, mut month: Month, mut day: int, mut d := daysSinceEpoch(year) // Add in days before this month. - d += u64(daysBefore[month-1]) + d += u64(_daysBefore[month-1]) if isLeap(year) && month >= March { d++ // February 29 } @@ -391,7 +465,23 @@ fn absUnix(mut year: int, mut month: Month, mut day: int, abs += u64(hour*secPerHour + minute*60 + second) // Convert absolute time to Unix time. - unix := i64(abs) + absoluteToUnix + mut unix := i64(abs) + absoluteToUnix + + // Look for zone offset for expected time, so we can adjust to UTC. + // The lookup function expects UTC, so first we pass unix in the + // hope that it will not be too close to a zone transition, + // and then adjust if it is. + _, mut offset, start, end, _ := loc.lookup(unix) + if offset != 0 { + utc := unix - i64(offset) + // If utc is valid for the time zone we found, then we have the right offset. + // If not, we get the correct offset by looking up utc in the location. + if utc < start || utc >= end { + _, offset, _, _, _ = loc.lookup(utc) + } + unix -= i64(offset) + } + ret unix } @@ -404,9 +494,153 @@ fn absUnix(mut year: int, mut month: Month, mut day: int, // The month, day, hour, minute, second, and nsecond values may be outside // their usual ranges and will be normalized during the conversion. // For example, October 32 converts to November 1. +// +// A daylight savings time transition skips or repeats times. +// For example, in the United States, March 13, 2011 2:15am never occurred, +// while November 6, 2011 1:15am occurred twice. In such cases, the +// choice of time zone, and therefore the time, is not well-defined. +// Date returns a time that is correct in one of the two zones involved +// in the transition, but it does not guarantee which. fn Date(year: int, month: Month, day: int, - hour: int, minute: int, second: int, nsecond: int): (t: Time) { - t.sec = absUnix(year, month, day, hour, minute, second, unsafe { *(&nsecond) }) + hour: int, minute: int, second: int, nsecond: int, loc: &Location): (t: Time) { + t.sec = absUnix(year, month, day, hour, minute, second, unsafe { *(&nsecond) }, unsafe { *(&loc) }) t.nsec = i32(nsecond) ret t +} + +// Days from March 1 through end of year. +const marchThruDecember = 31 + 30 + 31 + 30 + 31 + 31 + 30 + 31 + 30 + 31 + +// The number of years we subtract from internal time to get absolute time. +// This value must be 0 mod 400, and it defines the “absolute zero instant” +// mentioned in the “Computations on Times” comment above: March 1, -absoluteYears. +// Dates before the absolute epoch will not compute correctly, +// but otherwise the value can be changed as needed. +const absoluteYears = 292277022400 + +// Counts the number of seconds since the absolute zero instant. +type absSeconds: u64 + +// Counts the number of days since the absolute zero instant. +type absDays: u64 + +// Counts the number of centuries since the absolute zero instant. +type absCentury: u64 + +// Counts the number of years since the start of a century. +type absCyear: int + +// Counts the number of days since the start of a year. +// Note that absolute years start on March 1. +type absYday: int + +// Counts the number of months since the start of a year. +// absMonth=0 denotes March. +type absMonth: int + +// Single bit (0 or 1) denoting whether a given year is a leap year. +type absLeap: int + +// Single bit (0 or 1) denoting whether a given day falls in January or February. +// That is a special case because the absolute years start in March (unlike normal calendar years). +type absJanFeb: int + +impl absSeconds { + // Converts absolute seconds to absolute days. + fn days(self): absDays { + ret absDays(self / secPerDay) + } +} + +impl absDays { + // Splits days into century, cyear, ayday. + fn split(self): (century: absCentury, cyear: absCyear, ayday: absYday) { + // See “Computations on Times” comment above. + d := 4*u64(self) + 3 + century = absCentury(d / 146097) + + // This should be + // cday := uint32(d % 146097) / 4 + // cd := 4*cday + 3 + // which is to say + // cday := uint32(d % 146097) >> 2 + // cd := cday<<2 + 3 + // but of course (x>>2<<2)+3 == x|3, + // so do that instead. + cd := u32(d%146097) | 3 + + // For cdays in the range [0,146097] (100 years), we want: + // + // cyear := (4 cdays + 3) / 1461 + // yday := (4 cdays + 3) % 1461 / 4 + // + // (See the “Computations on Times” comment above + // as well as Neri and Schneider, section 7.) + // + // That is equivalent to: + // + // cyear := (2939745 cdays) >> 32 + // yday := (2939745 cdays) & 0xFFFFFFFF / 2939745 / 4 + // + // so do that instead, saving a few cycles. + // See Neri and Schneider, section 8.3 + // for more about this optimization. + hi, lo := bits::Mul32(2939745, u32(cd)) + cyear = absCyear(hi) + ayday = absYday(lo / 2939745 / 4) + ret + } + + // Converts days into the standard year and 1-based yday. + fn yearYday(self): (year: int, yday: int) { + century, cyear, ayday := self.split() + janFeb := ayday.janFeb() + year = century.year(cyear, janFeb) + yday = ayday.yday(janFeb, century.leap(cyear)) + ret + } +} + +impl absCentury { + // Returns 1 if (century, cyear) is a leap year, 0 otherwise. + fn leap(self, cyear: absCyear): absLeap { + // See “Computations on Times” comment above. + mut y4ok := 0 + if cyear%4 == 0 { + y4ok = 1 + } + mut y100ok := 0 + if cyear != 0 { + y100ok = 1 + } + mut y400ok := 0 + if self%4 == 0 { + y400ok = 1 + } + ret absLeap(y4ok & (y100ok | y400ok)) + } + + // Returns the standard year for (century, cyear, janFeb). + fn year(self, cyear: absCyear, janFeb: absJanFeb): int { + // See “Computations on Times” comment above. + ret int(u64(self)*100-absoluteYears) + int(cyear) + int(janFeb) + } +} + +impl absYday { + // Returns 1 if the March 1-based ayday is in January or February, 0 otherwise. + fn janFeb(self): absJanFeb { + // See “Computations on Times” comment above. + mut jf := absJanFeb(0) + if self >= marchThruDecember { + jf = 1 + } + ret jf + } + + // Returns the standard 1-based yday for (ayday, janFeb, leap). + fn yday(self, janFeb: absJanFeb, leap: absLeap): int { + // See “Computations on Times” comment above. + ret int(self) + (1 + 31 + 28) + int(leap)&^int(janFeb) - 365*int(janFeb) + } } \ No newline at end of file diff --git a/std/time/time_test.jule b/std/time/time_test.jule index 6ddeea9a..87876265 100644 --- a/std/time/time_test.jule +++ b/std/time/time_test.jule @@ -147,7 +147,7 @@ fn testDate(t: &testing::T) { for i, test in unixAbsTests { unixtime := Date( test.abs.Year, test.abs.Month, test.abs.Day, - test.abs.Hour, test.abs.Minute, test.abs.Second, 0) + test.abs.Hour, test.abs.Minute, test.abs.Second, 0, UTC) if unixtime.Unix() != test.sec { t.Errorf("#{} conversion failed", i) } @@ -201,7 +201,7 @@ static isoTests = []isoTest([ #test fn testTimeISO(t: &testing::T) { for i, test in isoTests { - time := Date(test.year, Month(test.month), test.day, 0, 0, 0, 0) + time := Date(test.year, Month(test.month), test.day, 0, 0, 0, 0, UTC) y, w := time.ISO() if y != test.yex || w != test.wex { t.Errorf("#{} conversion failed", i) diff --git a/std/time/zoneinfo.jule b/std/time/zoneinfo.jule new file mode 100644 index 00000000..514e86b0 --- /dev/null +++ b/std/time/zoneinfo.jule @@ -0,0 +1,555 @@ +// Copyright 2024 The Jule Programming Language. +// Use of this source code is governed by a BSD 3-Clause +// license that can be found in the LICENSE file. + +use "std/sync" + +// Maps time instants to the zone in use at that time. +// Typically, the Location represents the collection of time offsets +// in use in a geographical area. For many Locations the time offset varies +// depending on whether daylight savings time is in use at the time instant. +// +// Location is used to provide a time zone in a printed Time value and for +// calculations involving intervals that may cross daylight savings time +// boundaries. +struct Location { + name: str + zone: []zone + tx: []zoneTrans + + // The tzdata information can be followed by a string that describes + // how to handle DST transitions not recorded in zoneTrans. + // The format is the TZ environment variable without a colon; see + // https://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap08.html. + // Example string, for America/Los_Angeles: PST8PDT,M3.2.0,M11.1.0 + extend: str + + // Most lookups will be for the current time. + // To avoid the binary search through tx, keep a + // static one-element cache that gives the correct + // zone for the time when the Location was created. + // if cacheStart <= t < cacheEnd, + // lookup can return cacheZone. + // The units for cacheStart and cacheEnd are seconds + // since January 1, 1970 UTC, to match the argument + // to lookup. + cacheStart: i64 + cacheEnd: i64 + cacheZone: &zone +} + +static localOnce = sync::Once.New() + +impl Location { + // Returns a descriptive name for the time zone information. + fn Str(self): str { + ret unsafe { (&Location)(&self).get().name } + } + + fn get(mut &self): &Location { + if self == nil { + ret unsafe { *(&UTC) } + } + if self == Local { + localOnce.Do(initLocal) + } + ret self + } + + // Returns information about the time zone in use at an + // instant in time expressed as seconds since January 1, 1970 00:00:00 UTC. + // + // The returned information gives the name of the zone (such as "CET"), + // the start and end times bracketing sec when that zone is in effect, + // the offset in seconds east of UTC (such as -5*60*60), and whether + // the daylight savings is being observed at that time. + fn lookup(mut &self, sec: i64): (name: str, offset: int, start: i64, end: i64, isDST: bool) { + mut l := self.get() + + if len(l.zone) == 0 { + name = "UTC" + offset = 0 + start = alpha + end = omega + isDST = false + ret + } + + mut zone := l.cacheZone + if zone != nil && l.cacheStart <= sec && sec < l.cacheEnd { + name = zone.name + offset = zone.offset + start = l.cacheStart + end = l.cacheEnd + isDST = zone.isDST + ret + } + + if len(l.tx) == 0 || sec < l.tx[0].when { + zone = unsafe { (&zone)(&l.zone[l.lookupFirstZone()]) } + name = zone.name + offset = zone.offset + start = alpha + if len(l.tx) > 0 { + end = l.tx[0].when + } else { + end = omega + } + isDST = zone.isDST + ret + } + + // Binary search for entry with largest time <= sec. + tx := l.tx + end = omega + mut lo := 0 + mut hi := len(tx) + for hi-lo > 1 { + m := int(uint(lo+hi) >> 1) + lim := tx[m].when + if sec < lim { + end = lim + hi = m + } else { + lo = m + } + } + zone = unsafe { (&zone)(&l.zone[tx[lo].index]) } + name = zone.name + offset = zone.offset + start = tx[lo].when + // end = maintained during the search + isDST = zone.isDST + + // If we're at the end of the known zone transitions, + // try the extend string. + if lo == len(tx)-1 && l.extend != "" { + ename, eoffset, estart, eend, eisDST, ok := tzset(l.extend, start, sec) + if ok { + ret ename, eoffset, estart, eend, eisDST + } + } + + ret + } + + // Returns the index of the time zone to use for times + // before the first transition time, or when there are no transition + // times. + // + // The reference implementation in localtime.c from + // https://www.iana.org/time-zones/repository/releases/tzcode2013g.tar.gz + // implements the following algorithm for these cases: + // 1. If the first zone is unused by the transitions, use it. + // 2. Otherwise, if there are transition times, and the first + // transition is to a zone in daylight time, find the first + // non-daylight-time zone before and closest to the first transition + // zone. + // 3. Otherwise, use the first zone that is not daylight time, if + // there is one. + // 4. Otherwise, use the first zone. + fn lookupFirstZone(self): int { + // Case 1. + if !self.firstZoneUsed() { + ret 0 + } + + // Case 2. + if len(self.tx) > 0 && self.zone[self.tx[0].index].isDST { + mut zi := int(self.tx[0].index) - 1 + for zi >= 0; zi-- { + if !self.zone[zi].isDST { + ret zi + } + } + } + + // Case 3. + for zi in self.zone { + if !self.zone[zi].isDST { + ret zi + } + } + + // Case 4. + ret 0 + } + + // Reports whether the first zone is used by some transition. + fn firstZoneUsed(self): bool { + for _, tx in self.tx { + if tx.index == 0 { + ret true + } + } + ret false + } +} + +// Represents a single time zone such as CET. +struct zone { + name: str // abbreviated name, "CET" + offset: int // seconds east of UTC + isDST: bool // is this zone Daylight Savings Time? +} + +// Represents a single time zone transition. +struct zoneTrans { + when: i64 // transition time, in seconds since 1970 GMT + index: u8 // the index of the zone that goes into effect at that time + isstd: bool + isutc: bool // ignored - no idea what these mean +} + +// The kinds of rules that can be seen in a tzset string. +enum ruleKind { + Julian, + DOY, + MonthWeekDay, +} + +// Rule read from a tzset string. +struct rule { + kind: ruleKind + day: int + week: int + mon: int + time: int // transition time +} + +// Represents Universal Coordinated Time (UTC). +static UTC = unsafe { (&Location)(&utcLoc) } + +// Represents the system's local time zone. +// On Unix systems, Local consults the TZ environment +// variable to find the time zone to use. No TZ means +// use the system default /etc/localtime. +// TZ="" means use UTC. +// TZ="foo" means use file foo in the system timezone directory. +static Local = unsafe { (&Location)(&localLoc) } + +static mut utcLoc = Location{name: "UTC"} +static mut localLoc = Location{} + +// Returns the timezone name at the start of the tzset string s, +// and the remainder of s, and reports whether the parsing is OK. +fn tzsetName(s: str): (str, str, bool) { + if len(s) == 0 { + ret "", "", false + } + if s[0] != '<' { + for i, r in s { + match r { + | '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9' | ',' | '-' | '+': + if i < 3 { + ret "", "", false + } + ret s[:i], s[i:], true + } + } + if len(s) < 3 { + ret "", "", false + } + ret s, "", true + } else { + for i, r in s { + if r == '>' { + ret s[1:i], s[i+1:], true + } + } + ret "", "", false + } +} + +// Returns the timezone offset at the start of the tzset string s, +// and the remainder of s, and reports whether the parsing is OK. +// The timezone offset is returned as a number of seconds. +fn tzsetOffset(mut s: str): (offset: int, rest: str, ok: bool) { + if len(s) == 0 { + ret 0, "", false + } + mut neg := false + if s[0] == '+' { + s = s[1:] + } else if s[0] == '-' { + s = s[1:] + neg = true + } + + // The tzdata code permits values up to 24 * 7 here, + // although POSIX does not. + let mut hours: int + hours, s, ok = tzsetNum(s, 0, 24*7) + if !ok { + ret 0, "", false + } + mut off := hours * secPerHour + if len(s) == 0 || s[0] != ':' { + if neg { + off = -off + } + ret off, s, true + } + + let mut mins: int + mins, s, ok = tzsetNum(s[1:], 0, 59) + if !ok { + ret 0, "", false + } + off += mins * secPerMinute + if len(s) == 0 || s[0] != ':' { + if neg { + off = -off + } + ret off, s, true + } + + let mut secs: int + secs, s, ok = tzsetNum(s[1:], 0, 59) + if !ok { + ret 0, "", false + } + off += secs + + if neg { + off = -off + } + ret off, s, true +} + +// Parses a number from a tzset string. +// It returns the number, and the remainder of the string, and reports success. +// The number must be between min and max. +fn tzsetNum(s: str, min: int, max: int): (num: int, rest: str, ok: bool) { + if len(s) == 0 { + ret 0, "", false + } + num = 0 + for i, r in s { + if r < '0' || r > '9' { + if i == 0 || num < min { + ret 0, "", false + } + ret num, s[i:], true + } + num *= 10 + num += int(r) - '0' + if num > max { + ret 0, "", false + } + } + if num < min { + ret 0, "", false + } + ret num, "", true +} + +// Takes a year, a rule, and a timezone offset, +// and returns the number of seconds since the start of the year +// that the rule takes effect. +fn tzruleTime(year: int, r: rule, off: int): int { + let mut s: int + match r.kind { + | ruleKind.Julian: + s = (r.day - 1) * secPerDay + if isLeap(year) && r.day >= 60 { + s += secPerDay + } + | ruleKind.DOY: + s = r.day * secPerDay + | ruleKind.MonthWeekDay: + // Zeller's Congruence. + m1 := (r.mon+9)%12 + 1 + mut yy0 := year + if r.mon <= 2 { + yy0-- + } + yy1 := yy0 / 100 + yy2 := yy0 % 100 + mut dow := ((26*m1-2)/10 + 1 + yy2 + yy2/4 + yy1/4 - 2*yy1) % 7 + if dow < 0 { + dow += 7 + } + // Now dow is the day-of-week of the first day of r.mon. + // Get the day-of-month of the first "dow" day. + mut d := r.day - dow + if d < 0 { + d += 7 + } + mut i := 1 + for i < r.week; i++ { + if d+7 >= daysIn(Month(r.mon), year) { + break + } + d += 7 + } + d += int(daysBefore(Month(r.mon))) + if isLeap(year) && r.mon > 2 { + d++ + } + s = d * secPerDay + } + + ret s + r.time - off +} + +// Parses a rule from a tzset string. +// It returns the rule, and the remainder of the string, and reports success. +fn tzsetRule(mut s: str): (rule, str, bool) { + let mut r: rule + if len(s) == 0 { + ret rule{}, "", false + } + mut ok := false + if s[0] == 'J' { + let mut jday: int + jday, s, ok = tzsetNum(s[1:], 1, 365) + if !ok { + ret rule{}, "", false + } + r.kind = ruleKind.Julian + r.day = jday + } else if s[0] == 'M' { + let mut mon: int + mon, s, ok = tzsetNum(s[1:], 1, 12) + if !ok || len(s) == 0 || s[0] != '.' { + ret rule{}, "", false + } + let mut week: int + week, s, ok = tzsetNum(s[1:], 1, 5) + if !ok || len(s) == 0 || s[0] != '.' { + ret rule{}, "", false + } + let mut day: int + day, s, ok = tzsetNum(s[1:], 0, 6) + if !ok { + ret rule{}, "", false + } + r.kind = ruleKind.MonthWeekDay + r.day = day + r.week = week + r.mon = mon + } else { + let mut day: int + day, s, ok = tzsetNum(s, 0, 365) + if !ok { + ret rule{}, "", false + } + r.kind = ruleKind.DOY + r.day = day + } + + if len(s) == 0 || s[0] != '/' { + r.time = 2 * secPerHour // 2am is the default + ret r, s, true + } + + offset, s, ok := tzsetOffset(s[1:]) + if !ok { + ret rule{}, "", false + } + r.time = offset + + ret r, s, true +} + +// alpha and omega are the beginning and end of time for zone transitions. +const alpha = -1 << 63 // Min value of i64 +const omega = 1<<63 - 1 // Max value of i64 + +// Takes a timezone string like the one found in the TZ environment +// variable, the time of the last time zone transition expressed as seconds +// since January 1, 1970 00:00:00 UTC, and a time expressed the same way. +// We call this a tzset string since in C the function tzset reads TZ. +// The return values are as for lookup, plus ok which reports whether the +// parse succeeded. +fn tzset(mut s: str, lastTxSec: i64, sec: i64): (name: str, offset: int, start: i64, end: i64, isDST: bool, ok: bool) { + let mut stdName: str + let mut dstName: str + let mut stdOffset: int + let mut dstOffset: int + + stdName, s, ok = tzsetName(s) + if ok { + stdOffset, s, ok = tzsetOffset(s) + } + if !ok { + ret "", 0, 0, 0, false, false + } + + // The numbers in the tzset string are added to local time to get UTC, + // but our offsets are added to UTC to get local time, + // so we negate the number we see here. + stdOffset = -stdOffset + + if len(s) == 0 || s[0] == ',' { + // No daylight savings time. + ret stdName, stdOffset, lastTxSec, omega, false, true + } + + dstName, s, ok = tzsetName(s) + if ok { + if len(s) == 0 || s[0] == ',' { + dstOffset = stdOffset + secPerHour + } else { + dstOffset, s, ok = tzsetOffset(s) + dstOffset = -dstOffset // as with stdOffset, above + } + } + if !ok { + ret "", 0, 0, 0, false, false + } + + if len(s) == 0 { + // Default DST rules per tzcode. + s = ",M3.2.0,M11.1.0" + } + // The TZ definition does not mention ';' here but tzcode accepts it. + if s[0] != ',' && s[0] != ';' { + ret "", 0, 0, 0, false, false + } + s = s[1:] + + let mut startRule: rule + let mut endRule: rule + startRule, s, ok = tzsetRule(s) + if !ok || len(s) == 0 || s[0] != ',' { + ret "", 0, 0, 0, false, false + } + s = s[1:] + endRule, s, ok = tzsetRule(s) + if !ok || len(s) > 0 { + ret "", 0, 0, 0, false, false + } + + // Compute start of year in seconds since Unix epoch, + // and seconds since then to get to sec. + year, yday := absSeconds(sec + unixToAbsolute).days().yearYday() + ysec := i64((yday-1)*secPerDay) + sec%secPerDay + ystart := sec - ysec + + mut startSec := i64(tzruleTime(year, startRule, stdOffset)) + mut endSec := i64(tzruleTime(year, endRule, dstOffset)) + mut dstIsDST, mut stdIsDST := true, false + // Note: this is a flipping of "DST" and "STD" while retaining the labels + // This happens in southern hemispheres. The labelling here thus is a little + // inconsistent with the goal. + if endSec < startSec { + startSec, endSec = endSec, startSec + stdName, dstName = dstName, stdName + stdOffset, dstOffset = dstOffset, stdOffset + stdIsDST, dstIsDST = dstIsDST, stdIsDST + } + + // The start and end values that we return are accurate + // close to a daylight savings transition, but are otherwise + // just the start and end of the year. That suffices for + // the only caller that cares, which is Date. + if ysec < startSec { + ret stdName, stdOffset, ystart, startSec + ystart, stdIsDST, true + } else if ysec >= endSec { + ret stdName, stdOffset, endSec + ystart, ystart + 365*secPerDay, stdIsDST, true + } else { + ret dstName, dstOffset, startSec + ystart, endSec + ystart, dstIsDST, true + } +} \ No newline at end of file diff --git a/std/time/zoneinfo_abbrs_windows.jule b/std/time/zoneinfo_abbrs_windows.jule new file mode 100644 index 00000000..905f2fa5 --- /dev/null +++ b/std/time/zoneinfo_abbrs_windows.jule @@ -0,0 +1,152 @@ +// Copyright 2024 The Jule Programming Language. +// Use of this source code is governed by a BSD 3-Clause +// license that can be found in the LICENSE file. + +// Based on information from https://raw.githubusercontent.com/unicode-org/cldr/main/common/supplemental/windowsZones.xml + +struct abbr { + std: str + dst: str +} + +static abbrs: map[str]abbr = { + "Egypt Standard Time": abbr{"EET", "EEST"}, // Africa/Cairo + "Morocco Standard Time": abbr{"+00", "+01"}, // Africa/Casablanca + "South Africa Standard Time": abbr{"SAST", "SAST"}, // Africa/Johannesburg + "South Sudan Standard Time": abbr{"CAT", "CAT"}, // Africa/Juba + "Sudan Standard Time": abbr{"CAT", "CAT"}, // Africa/Khartoum + "W. Central Africa Standard Time": abbr{"WAT", "WAT"}, // Africa/Lagos + "E. Africa Standard Time": abbr{"EAT", "EAT"}, // Africa/Nairobi + "Sao Tome Standard Time": abbr{"GMT", "GMT"}, // Africa/Sao_Tome + "Libya Standard Time": abbr{"EET", "EET"}, // Africa/Tripoli + "Namibia Standard Time": abbr{"CAT", "CAT"}, // Africa/Windhoek + "Aleutian Standard Time": abbr{"HST", "HDT"}, // America/Adak + "Alaskan Standard Time": abbr{"AKST", "AKDT"}, // America/Anchorage + "Tocantins Standard Time": abbr{"-03", "-03"}, // America/Araguaina + "Paraguay Standard Time": abbr{"-04", "-03"}, // America/Asuncion + "Bahia Standard Time": abbr{"-03", "-03"}, // America/Bahia + "SA Pacific Standard Time": abbr{"-05", "-05"}, // America/Bogota + "Argentina Standard Time": abbr{"-03", "-03"}, // America/Buenos_Aires + "Eastern Standard Time (Mexico)": abbr{"EST", "EST"}, // America/Cancun + "Venezuela Standard Time": abbr{"-04", "-04"}, // America/Caracas + "SA Eastern Standard Time": abbr{"-03", "-03"}, // America/Cayenne + "Central Standard Time": abbr{"CST", "CDT"}, // America/Chicago + "Central Brazilian Standard Time": abbr{"-04", "-04"}, // America/Cuiaba + "Mountain Standard Time": abbr{"MST", "MDT"}, // America/Denver + "Greenland Standard Time": abbr{"-02", "-01"}, // America/Godthab + "Turks And Caicos Standard Time": abbr{"EST", "EDT"}, // America/Grand_Turk + "Central America Standard Time": abbr{"CST", "CST"}, // America/Guatemala + "Atlantic Standard Time": abbr{"AST", "ADT"}, // America/Halifax + "Cuba Standard Time": abbr{"CST", "CDT"}, // America/Havana + "US Eastern Standard Time": abbr{"EST", "EDT"}, // America/Indianapolis + "SA Western Standard Time": abbr{"-04", "-04"}, // America/La_Paz + "Pacific Standard Time": abbr{"PST", "PDT"}, // America/Los_Angeles + "Mountain Standard Time (Mexico)": abbr{"MST", "MST"}, // America/Mazatlan + "Central Standard Time (Mexico)": abbr{"CST", "CST"}, // America/Mexico_City + "Saint Pierre Standard Time": abbr{"-03", "-02"}, // America/Miquelon + "Montevideo Standard Time": abbr{"-03", "-03"}, // America/Montevideo + "Eastern Standard Time": abbr{"EST", "EDT"}, // America/New_York + "US Mountain Standard Time": abbr{"MST", "MST"}, // America/Phoenix + "Haiti Standard Time": abbr{"EST", "EDT"}, // America/Port-au-Prince + "Magallanes Standard Time": abbr{"-03", "-03"}, // America/Punta_Arenas + "Canada Central Standard Time": abbr{"CST", "CST"}, // America/Regina + "Pacific SA Standard Time": abbr{"-04", "-03"}, // America/Santiago + "E. South America Standard Time": abbr{"-03", "-03"}, // America/Sao_Paulo + "Newfoundland Standard Time": abbr{"NST", "NDT"}, // America/St_Johns + "Pacific Standard Time (Mexico)": abbr{"PST", "PDT"}, // America/Tijuana + "Yukon Standard Time": abbr{"MST", "MST"}, // America/Whitehorse + "Jordan Standard Time": abbr{"+03", "+03"}, // Asia/Amman + "Arabic Standard Time": abbr{"+03", "+03"}, // Asia/Baghdad + "Azerbaijan Standard Time": abbr{"+04", "+04"}, // Asia/Baku + "SE Asia Standard Time": abbr{"+07", "+07"}, // Asia/Bangkok + "Altai Standard Time": abbr{"+07", "+07"}, // Asia/Barnaul + "Middle East Standard Time": abbr{"EET", "EEST"}, // Asia/Beirut + "Central Asia Standard Time": abbr{"+06", "+06"}, // Asia/Bishkek + "India Standard Time": abbr{"IST", "IST"}, // Asia/Calcutta + "Transbaikal Standard Time": abbr{"+09", "+09"}, // Asia/Chita + "Sri Lanka Standard Time": abbr{"+0530", "+0530"}, // Asia/Colombo + "Syria Standard Time": abbr{"+03", "+03"}, // Asia/Damascus + "Bangladesh Standard Time": abbr{"+06", "+06"}, // Asia/Dhaka + "Arabian Standard Time": abbr{"+04", "+04"}, // Asia/Dubai + "West Bank Standard Time": abbr{"EET", "EEST"}, // Asia/Hebron + "W. Mongolia Standard Time": abbr{"+07", "+07"}, // Asia/Hovd + "North Asia East Standard Time": abbr{"+08", "+08"}, // Asia/Irkutsk + "Israel Standard Time": abbr{"IST", "IDT"}, // Asia/Jerusalem + "Afghanistan Standard Time": abbr{"+0430", "+0430"}, // Asia/Kabul + "Russia Time Zone 11": abbr{"+12", "+12"}, // Asia/Kamchatka + "Pakistan Standard Time": abbr{"PKT", "PKT"}, // Asia/Karachi + "Nepal Standard Time": abbr{"+0545", "+0545"}, // Asia/Katmandu + "North Asia Standard Time": abbr{"+07", "+07"}, // Asia/Krasnoyarsk + "Magadan Standard Time": abbr{"+11", "+11"}, // Asia/Magadan + "N. Central Asia Standard Time": abbr{"+07", "+07"}, // Asia/Novosibirsk + "Omsk Standard Time": abbr{"+06", "+06"}, // Asia/Omsk + "North Korea Standard Time": abbr{"KST", "KST"}, // Asia/Pyongyang + "Qyzylorda Standard Time": abbr{"+05", "+05"}, // Asia/Qyzylorda + "Myanmar Standard Time": abbr{"+0630", "+0630"}, // Asia/Rangoon + "Arab Standard Time": abbr{"+03", "+03"}, // Asia/Riyadh + "Sakhalin Standard Time": abbr{"+11", "+11"}, // Asia/Sakhalin + "Korea Standard Time": abbr{"KST", "KST"}, // Asia/Seoul + "China Standard Time": abbr{"CST", "CST"}, // Asia/Shanghai + "Singapore Standard Time": abbr{"+08", "+08"}, // Asia/Singapore + "Russia Time Zone 10": abbr{"+11", "+11"}, // Asia/Srednekolymsk + "Taipei Standard Time": abbr{"CST", "CST"}, // Asia/Taipei + "West Asia Standard Time": abbr{"+05", "+05"}, // Asia/Tashkent + "Georgian Standard Time": abbr{"+04", "+04"}, // Asia/Tbilisi + "Iran Standard Time": abbr{"+0330", "+0330"}, // Asia/Tehran + "Tokyo Standard Time": abbr{"JST", "JST"}, // Asia/Tokyo + "Tomsk Standard Time": abbr{"+07", "+07"}, // Asia/Tomsk + "Ulaanbaatar Standard Time": abbr{"+08", "+08"}, // Asia/Ulaanbaatar + "Vladivostok Standard Time": abbr{"+10", "+10"}, // Asia/Vladivostok + "Yakutsk Standard Time": abbr{"+09", "+09"}, // Asia/Yakutsk + "Ekaterinburg Standard Time": abbr{"+05", "+05"}, // Asia/Yekaterinburg + "Caucasus Standard Time": abbr{"+04", "+04"}, // Asia/Yerevan + "Azores Standard Time": abbr{"-01", "+00"}, // Atlantic/Azores + "Cape Verde Standard Time": abbr{"-01", "-01"}, // Atlantic/Cape_Verde + "Greenwich Standard Time": abbr{"GMT", "GMT"}, // Atlantic/Reykjavik + "Cen. Australia Standard Time": abbr{"ACST", "ACDT"}, // Australia/Adelaide + "E. Australia Standard Time": abbr{"AEST", "AEST"}, // Australia/Brisbane + "AUS Central Standard Time": abbr{"ACST", "ACST"}, // Australia/Darwin + "Aus Central W. Standard Time": abbr{"+0845", "+0845"}, // Australia/Eucla + "Tasmania Standard Time": abbr{"AEST", "AEDT"}, // Australia/Hobart + "Lord Howe Standard Time": abbr{"+1030", "+11"}, // Australia/Lord_Howe + "W. Australia Standard Time": abbr{"AWST", "AWST"}, // Australia/Perth + "AUS Eastern Standard Time": abbr{"AEST", "AEDT"}, // Australia/Sydney + "UTC-11": abbr{"-11", "-11"}, // Etc/GMT+11 + "Dateline Standard Time": abbr{"-12", "-12"}, // Etc/GMT+12 + "UTC-02": abbr{"-02", "-02"}, // Etc/GMT+2 + "UTC-08": abbr{"-08", "-08"}, // Etc/GMT+8 + "UTC-09": abbr{"-09", "-09"}, // Etc/GMT+9 + "UTC+12": abbr{"+12", "+12"}, // Etc/GMT-12 + "UTC+13": abbr{"+13", "+13"}, // Etc/GMT-13 + "UTC": abbr{"UTC", "UTC"}, // Etc/UTC + "Astrakhan Standard Time": abbr{"+04", "+04"}, // Europe/Astrakhan + "W. Europe Standard Time": abbr{"CET", "CEST"}, // Europe/Berlin + "GTB Standard Time": abbr{"EET", "EEST"}, // Europe/Bucharest + "Central Europe Standard Time": abbr{"CET", "CEST"}, // Europe/Budapest + "E. Europe Standard Time": abbr{"EET", "EEST"}, // Europe/Chisinau + "Turkey Standard Time": abbr{"+03", "+03"}, // Europe/Istanbul + "Kaliningrad Standard Time": abbr{"EET", "EET"}, // Europe/Kaliningrad + "FLE Standard Time": abbr{"EET", "EEST"}, // Europe/Kiev + "GMT Standard Time": abbr{"GMT", "BST"}, // Europe/London + "Belarus Standard Time": abbr{"+03", "+03"}, // Europe/Minsk + "Russian Standard Time": abbr{"MSK", "MSK"}, // Europe/Moscow + "Romance Standard Time": abbr{"CET", "CEST"}, // Europe/Paris + "Russia Time Zone 3": abbr{"+04", "+04"}, // Europe/Samara + "Saratov Standard Time": abbr{"+04", "+04"}, // Europe/Saratov + "Volgograd Standard Time": abbr{"MSK", "MSK"}, // Europe/Volgograd + "Central European Standard Time": abbr{"CET", "CEST"}, // Europe/Warsaw + "Mauritius Standard Time": abbr{"+04", "+04"}, // Indian/Mauritius + "Samoa Standard Time": abbr{"+13", "+13"}, // Pacific/Apia + "New Zealand Standard Time": abbr{"NZST", "NZDT"}, // Pacific/Auckland + "Bougainville Standard Time": abbr{"+11", "+11"}, // Pacific/Bougainville + "Chatham Islands Standard Time": abbr{"+1245", "+1345"}, // Pacific/Chatham + "Easter Island Standard Time": abbr{"-06", "-05"}, // Pacific/Easter + "Fiji Standard Time": abbr{"+12", "+12"}, // Pacific/Fiji + "Central Pacific Standard Time": abbr{"+11", "+11"}, // Pacific/Guadalcanal + "Hawaiian Standard Time": abbr{"HST", "HST"}, // Pacific/Honolulu + "Line Islands Standard Time": abbr{"+14", "+14"}, // Pacific/Kiritimati + "Marquesas Standard Time": abbr{"-0930", "-0930"}, // Pacific/Marquesas + "Norfolk Standard Time": abbr{"+11", "+12"}, // Pacific/Norfolk + "West Pacific Standard Time": abbr{"+10", "+10"}, // Pacific/Port_Moresby + "Tonga Standard Time": abbr{"+13", "+13"}, // Pacific/Tongatapu +} \ No newline at end of file diff --git a/std/time/zoneinfo_read.jule b/std/time/zoneinfo_read.jule new file mode 100644 index 00000000..0087779a --- /dev/null +++ b/std/time/zoneinfo_read.jule @@ -0,0 +1,535 @@ +// Copyright 2024 The Jule Programming Language. +// Use of this source code is governed by a BSD 3-Clause +// license that can be found in the LICENSE file. + +use "std/internal/fastbytes" +use "std/runtime" +use "std/unsafe" + +// Returns the time zone information of the time zone +// with the given name, from a given source. A source may be a +// timezone database directory, tzdata database file or an uncompressed +// zip file, containing the contents of such a directory. +fn loadTzinfo(name: str, source: str): ([]byte, ok: bool) { + ret loadTzinfoFromDirOrZip(source, name) +} + +// Returns the contents of the file with the given name +// in dir. dir can either be an uncompressed zip file, or a directory. +fn loadTzinfoFromDirOrZip(dir: str, mut name: str): ([]byte, ok: bool) { + if len(dir) > 4 && dir[len(dir)-4:] == ".zip" { + ret loadTzinfoFromZip(dir, name) + } + if dir != "" { + name = dir + "/" + name + } + ret readFile(name) +} + +// Returns the Location with the given name from one of +// the specified sources. See loadTzinfo for a list of supported sources. +// The first timezone data matching the given name that is successfully loaded +// and parsed is returned as a Location. +fn loadLocation(name: str, sources: []str): (z: &Location, ok: bool) { + for _, source in sources { + mut zoneData, ok2 := loadTzinfo(name, source) + if ok2 { + z, ok = LoadLocationFromTZData(name, zoneData) + if ok { + ret + } + } + } + ret +} + +// Simple I/O interface to binary blob of data. +struct dataIO { + p: []byte + fail: bool +} + +impl dataIO { + fn read(mut self, n: int): []byte { + if len(self.p) < n { + self.p = nil + self.fail = true + ret nil + } + mut p := self.p[0:n] + self.p = self.p[n:] + ret p + } + + fn big4(mut self): (n: u32, ok: bool) { + p := self.read(4) + if len(p) < 4 { + self.fail = true + ret 0, false + } + ret u32(p[3]) | u32(p[2])<<8 | u32(p[1])<<16 | u32(p[0])<<24, true + } + + fn big8(mut self): (n: u64, ok: bool) { + n1, ok1 := self.big4() + n2, ok2 := self.big4() + if !ok1 || !ok2 { + self.fail = true + ret 0, false + } + ret (u64(n1) << 32) | u64(n2), true + } + + fn byte(mut self): (n: byte, ok: bool) { + p := self.read(1) + if len(p) < 1 { + self.fail = true + ret 0, false + } + ret p[0], true + } + + // rest returns the rest of the data in the buffer. + fn rest(mut self): []byte { + mut r := self.p + self.p = nil + ret r + } +} + +// Returns a Location with the given name +// initialized from the IANA Time Zone database-formatted data. +// The data should be in the format of a standard IANA time zone file +// (for example, the content of /etc/localtime on Unix systems). +fn LoadLocationFromTZData(name: str, mut data: []byte): (&Location, ok: bool) { + mut d := dataIO{data, false} + + // 4-byte magic "TZif" + { + magic := d.read(4) + if str(magic) != "TZif" { + ret + } + } + + // 1-byte version, then 15 bytes of padding + let mut version: int + p := d.read(16) + if len(p) != 16 { + ret + } else { + match p[0] { + | 0: + version = 1 + | '2': + version = 2 + | '3': + version = 3 + |: + ret + } + } + + // six big-endian 32-bit integers: + // number of UTC/local indicators + // number of standard/wall indicators + // number of leap seconds + // number of transition times + // number of local time zones + // number of characters of time zone abbrev strings + const NUTCLocal = 0 + const NStdWall = 1 + const NLeap = 2 + const NTime = 3 + const NZone = 4 + const NChar = 5 + + let mut n: [6]int + mut i := 0 + for i < 6; i++ { + nn, ok2 := d.big4() + if !ok2 { + ret + } + if u32(int(nn)) != nn { + ret + } + n[i] = int(nn) + } + + // If we have version 2 or 3, then the data is first written out + // in a 32-bit format, then written out again in a 64-bit format. + // Skip the 32-bit format and read the 64-bit one, as it can + // describe a broader range of dates. + + mut is64 := false + if version > 1 { + // Skip the 32-bit data. + mut skip := n[NTime]*4 + + n[NTime] + + n[NZone]*6 + + n[NChar] + + n[NLeap]*8 + + n[NStdWall] + + n[NUTCLocal] + // Skip the version 2 header that we just read. + skip += 4 + 16 + d.read(skip) + + is64 = true + + // Read the counts again, they can differ. + i = 0 + for i < 6; i++ { + nn, ok2 := d.big4() + if !ok2 { + ret + } + if u32(int(nn)) != nn { + ret + } + n[i] = int(nn) + } + } + + mut size := 4 + if is64 { + size = 8 + } + + // Transition times. + mut txtimes := dataIO{d.read(n[NTime] * size), false} + + // Time zone indices for transition times. + mut txzones := d.read(n[NTime]) + + // Zone info structures + mut zonedata := dataIO{d.read(n[NZone] * 6), false} + + // Time zone abbreviations. + mut abbrev := d.read(n[NChar]) + + // Leap-second time pairs + d.read(n[NLeap] * (size + 4)) + + // Whether tx times associated with local time types + // are specified as standard time or wall time. + isstd := d.read(n[NStdWall]) + + // Whether tx times associated with local time types + // are specified as UTC or local time. + isutc := d.read(n[NUTCLocal]) + + if d.fail { // ran out of data + ret + } + + let mut extend: str + rest := d.rest() + if len(rest) > 2 && rest[0] == '\n' && rest[len(rest)-1] == '\n' { + extend = str(rest[1:len(rest)-1]) + } + + // Now we can build up a useful data structure. + // First the zone information. + // utcoff[4] isdst[1] nameindex[1] + nzone := n[NZone] + if nzone == 0 { + // Reject tzdata files with no zones. There's nothing useful in them. + // This also avoids a panic later when we add and then use a fake transition (golang.org/issue/29437). + ret + } + mut zones := make([]zone, nzone) + i = 0 + for i < len(zones); i++ { + let mut ok2: bool + let mut n2: u32 + n2, ok2 = zonedata.big4() + if !ok2 { + ret + } + if u32(int(n2)) != n2 { + ret + } + zones[i].offset = int(i32(n2)) + let mut b: byte + b, ok2 = zonedata.byte() + if !ok2 { + ret + } + zones[i].isDST = b != 0 + b, ok2 = zonedata.byte() + if !ok2 || int(b) >= len(abbrev) { + ret + } + zones[i].name = byteStr(abbrev[b:]) + } + + // Now the transition time info. + mut tx := make([]zoneTrans, n[NTime]) + i = 0 + for i < len(tx); i++ { + let mut n2: i64 + if !is64 { + n4, ok2 := txtimes.big4() + if !ok2 { + ret + } else { + n2 = i64(i32(n4)) + } + } else { + n8, ok2 := txtimes.big8() + if !ok2 { + ret + } else { + n2 = i64(n8) + } + } + tx[i].when = n2 + if int(txzones[i]) >= len(zones) { + ret + } + tx[i].index = u8(txzones[i]) + if i < len(isstd) { + tx[i].isstd = isstd[i] != 0 + } + if i < len(isutc) { + tx[i].isutc = isutc[i] != 0 + } + } + + if len(tx) == 0 { + // Build fake transition to cover all time. + // This happens in fixed locations like "Etc/GMT0". + tx = append(tx, zoneTrans{when: alpha, index: 0}) + } + + // Committed to succeed. + mut l := &Location{zone: zones, tx: tx, name: name, extend: extend} + + // Fill in the cache with information about right now, + // since that will be the most common lookup. + sec, _ := runtime::timeNow() + i = 0 + for i < len(tx); i++ { + if tx[i].when <= sec && (i+1 == len(tx) || sec < tx[i+1].when) { + l.cacheStart = tx[i].when + l.cacheEnd = omega + l.cacheZone = unsafe { (&zone)(&l.zone[tx[i].index]) } + if i+1 < len(tx) { + l.cacheEnd = tx[i+1].when + } else if l.extend != "" { + // If we're at the end of the known zone transitions, + // try the extend string. + name2, offset, estart, eend, isDST, ok2 := tzset(l.extend, l.cacheStart, sec) + if ok2 { + l.cacheStart = estart + l.cacheEnd = eend + // Find the zone that is returned by tzset to avoid allocation if possible. + zoneIdx := findZone(l.zone, name2, offset, isDST) + if zoneIdx != -1 { + l.cacheZone = unsafe { (&zone)(&l.zone[zoneIdx]) } + } else { + l.cacheZone = &zone{ + name: name2, + offset: offset, + isDST: isDST, + } + } + } + } + break + } + } + + ret l, true +} + +// Returns the contents of the file with the given name in the given uncompressed zip file. +fn loadTzinfoFromZip(zipfile: str, name: str): ([]byte, bool) { + fd, ok := open(zipfile) + if !ok { + ret nil, false + } + + const zecheader = 0x06054b50 + const zcheader = 0x02014b50 + const ztailsize = 22 + + const zheadersize = 30 + const zheader = 0x04034b50 + + mut buf := make([]byte, ztailsize) + if !preadn(fd, buf, -ztailsize) || get4(buf) != zecheader { + closefd(fd) + ret nil, false + } + n := get2(buf[10:]) + mut size := get4(buf[12:]) + mut off := get4(buf[16:]) + + buf = make([]byte, size) + if !preadn(fd, buf, off) { + closefd(fd) + ret nil, false + } + + mut i := 0 + for i < n; i++ { + // zip entry layout: + // 0 magic[4] + // 4 madevers[1] + // 5 madeos[1] + // 6 extvers[1] + // 7 extos[1] + // 8 flags[2] + // 10 meth[2] + // 12 modtime[2] + // 14 moddate[2] + // 16 crc[4] + // 20 csize[4] + // 24 uncsize[4] + // 28 namelen[2] + // 30 xlen[2] + // 32 fclen[2] + // 34 disknum[2] + // 36 iattr[2] + // 38 eattr[4] + // 42 off[4] + // 46 name[namelen] + // 46+namelen+xlen+fclen - next header + // + if get4(buf) != zcheader { + break + } + meth := get2(buf[10:]) + size = get4(buf[24:]) + namelen := get2(buf[28:]) + mut xlen := get2(buf[30:]) + fclen := get2(buf[32:]) + off = get4(buf[42:]) + zname := buf[46:46+namelen] + buf = buf[46+namelen+xlen+fclen:] + if str(zname) != name { + continue + } + if meth != 0 { + closefd(fd) + ret nil, false + } + + // zip per-file header layout: + // 0 magic[4] + // 4 extvers[1] + // 5 extos[1] + // 6 flags[2] + // 8 meth[2] + // 10 modtime[2] + // 12 moddate[2] + // 14 crc[4] + // 18 csize[4] + // 22 uncsize[4] + // 26 namelen[2] + // 28 xlen[2] + // 30 name[namelen] + // 30+namelen+xlen - file data + // + buf = make([]byte, zheadersize+namelen) + if !preadn(fd, buf, off) || + get4(buf) != zheader || + get2(buf[8:]) != meth || + get2(buf[26:]) != namelen || + str(buf[30:30+namelen]) != name { + closefd(fd) + ret nil, false + } + xlen = get2(buf[28:]) + + buf = make([]byte, size) + if !preadn(fd, buf, off+30+namelen+xlen) { + closefd(fd) + ret nil, false + } + + closefd(fd) + ret buf, true + } + + closefd(fd) + ret nil, false +} + +fn findZone(zones: []zone, name: str, offset: int, isDST: bool): int { + for i, z in zones { + if z.name == name && z.offset == offset && z.isDST == isDST { + ret i + } + } + ret -1 +} + +// Make a string by stopping at the first NUL +fn byteStr(mut p: []byte): str { + i := fastbytes::FindByte(p, 0) + if i != -1 { + p = p[:i] + } + ret str(p) +} + +// There are 500+ zoneinfo files. Rather than distribute them all +// individually, we ship them in an uncompressed zip file. +// Used this way, the zip file format serves as a commonly readable +// container for the individual small files. We choose zip over tar +// because zip files have a contiguous table of contents, making +// individual file lookups faster, and because the per-file overhead +// in a zip file is considerably less than tar's 512 bytes. + +// Returns the little-endian 32-bit value in b. +fn get4(b: []byte): int { + if len(b) < 4 { + ret 0 + } + ret int(b[0]) | int(b[1])<<8 | int(b[2])<<16 | int(b[3])<<24 +} + +// Returns the little-endian 16-bit value in b. +fn get2(b: []byte): int { + if len(b) < 2 { + ret 0 + } + ret int(b[0]) | int(b[1])<<8 +} + +// The max permitted size of files read by readFile. +const maxFileSize = 10 << 20 + +// Reads and returns the content of the named file. +// It is a trivial implementation of os::File.Read, reimplemented +// here to avoid depending. +// It reports false if name exceeds maxFileSize bytes. +fn readFile(name: str): ([]byte, ok: bool) { + f, ok := open(name) + if !ok { + ret nil, false + } + let mut buf: [4096]byte + mut bufs := unsafe::Slice(&buf[0], len(buf), len(buf)) + let mut r: []byte + let mut n: int + for { + n, ok = read(f, bufs) + if n > 0 { + r = append(r, bufs[:n]...) + } + if n == 0 || !ok { + break + } + if len(r) > maxFileSize { + closefd(f) + ret nil, false + } + } + closefd(f) + ret r, true +} \ No newline at end of file diff --git a/std/time/zoneinfo_unix.jule b/std/time/zoneinfo_unix.jule new file mode 100644 index 00000000..2e457e87 --- /dev/null +++ b/std/time/zoneinfo_unix.jule @@ -0,0 +1,69 @@ +// Copyright 2024 The Jule Programming Language. +// Use of this source code is governed by a BSD 3-Clause +// license that can be found in the LICENSE file. + +// Parse "zoneinfo" time zone file. +// This is a fairly standard file format used on OS X, Linux, BSD, Sun, and others. +// See tzfile(5), https://en.wikipedia.org/wiki/Zoneinfo, +// and ftp://munnari.oz.au/pub/oldtz/ + +use integ "std/jule/integrated" + +cpp unsafe fn getenv(name: *integ::Char): *integ::Char +cpp unsafe fn setenv(name: *integ::Char, value: *integ::Char, overwrite: int): int + +// Many systems use /usr/share/zoneinfo, Solaris 2 has +// /usr/share/lib/zoneinfo, IRIX 6 has /usr/lib/locale/TZ, +// NixOS has /etc/zoneinfo. +static platformZoneSources = [ + "/usr/share/zoneinfo/", + "/usr/share/lib/zoneinfo/", + "/usr/lib/locale/TZ/", + "/etc/zoneinfo", +] + +fn initLocal() { + // consult $TZ to find the time zone to use. + // no $TZ means use the system default /etc/localtime. + // $TZ="" means use UTC. + // $TZ="foo" or $TZ=":foo" if foo is an absolute path, then the file pointed + // by foo will be used to initialize timezone; otherwise, file + // /usr/share/zoneinfo/foo will be used. + + mut tz := "TZ\000" + tz = unsafe { integ::BytePtrToStr((*byte)(cpp.getenv((*integ::Char)(&tz[0])))) } + match { + | len(tz) == 0: + mut z, ok := loadLocation("localtime", ["/etc"]) + if ok { + localLoc = *z + localLoc.name = "Local" + ret + } + |: + if tz[0] == ':' { + tz = tz[1:] + } + if tz != "" && tz[0] == '/' { + mut z, ok := loadLocation(tz, [""]) + if ok { + localLoc = *z + if tz == "/etc/localtime" { + localLoc.name = "Local" + } else { + localLoc.name = tz + } + ret + } + } else if tz != "" && tz != "UTC" { + mut z, ok := loadLocation(tz, platformZoneSources) + if ok { + localLoc = *z + ret + } + } + } + + // Fall back to UTC. + localLoc.name = "UTC" +} \ No newline at end of file diff --git a/std/time/zoneinfo_windows.jule b/std/time/zoneinfo_windows.jule new file mode 100644 index 00000000..ec1346f5 --- /dev/null +++ b/std/time/zoneinfo_windows.jule @@ -0,0 +1,168 @@ +// Copyright 2024 The Jule Programming Language. +// Use of this source code is governed by a BSD 3-Clause +// license that can be found in the LICENSE file. + +use integ "std/jule/integrated" +use "std/unsafe" + +#typedef +cpp struct TIME_ZONE_INFORMATION {} +cpp unsafe fn GetTimeZoneInformation(mut i: *cpp.TIME_ZONE_INFORMATION): u32 + +const _TIME_ZONE_ID_INVALID = 0xffffffff + +struct systemtime { + Year: u16 + Month: u16 + DayOfWeek: u16 + Day: u16 + Hour: u16 + Minute: u16 + Second: u16 + Milliseconds: u16 +} + + +// Windows's TIME_ZONE_INFORMATION structure. +struct timezoneinformation { + Bias: i32 + StandardName: [32]u16 + StandardDate: systemtime + StandardBias: i32 + DaylightName: [32]u16 + DaylightDate: systemtime + DaylightBias: i32 +} + +// Extracts capital letters from description desc. +fn extractCAPS(desc: str): str { + let mut short: []rune + for _, c in []rune(desc) { + if 'A' <= c && c <= 'Z' { + short = append(short, c) + } + } + ret str(short) +} + +// Returns the abbreviations to use for the given zone z. +fn abbrev(&z: timezoneinformation): (std: str, dst: str) { + stdNameU16 := unsafe::Slice(&z.StandardName[0], len(z.StandardName), len(z.StandardName)) + stdName := integ::UTF16ToStr(stdNameU16) + mut a, mut ok := abbrs[stdName] + if !ok { + dstNameU16 := unsafe::Slice(&z.DaylightName[0], len(z.DaylightName), len(z.DaylightName)) + dstName := integ::UTF16ToStr(dstNameU16) + // fallback to using capital letters + ret extractCAPS(stdName), extractCAPS(dstName) + } + ret a.std, a.dst +} + +// Returns the pseudo-Unix time (seconds since Jan 1 1970 *LOCAL TIME*) +// denoted by the system date+time d in the given year. +// It is up to the caller to convert this local time into a UTC-based time. +fn pseudoUnix(year: int, &d: systemtime): i64 { + // Windows specifies daylight savings information in "day in month" format: + // d.Month is month number (1-12) + // d.DayOfWeek is appropriate weekday (Sunday=0 to Saturday=6) + // d.Day is week within the month (1 to 5, where 5 is last week of the month) + // d.Hour, d.Minute and d.Second are absolute time + mut day := 1 + t := Date(year, Month(d.Month), day, int(d.Hour), int(d.Minute), int(d.Second), 0, UTC) + mut i := int(d.DayOfWeek) - int(t.Weekday()) + if i < 0 { + i += 7 + } + day += i + week := int(d.Day) - 1 + if week < 4 { + day += week * 7 + } else { + // "Last" instance of the day. + day += 4 * 7 + if day > daysIn(Month(d.Month), year) { + day -= 7 + } + } + ret t.sec + i64(day-1)*secPerDay +} + +fn initLocalFromTZI(&i: timezoneinformation) { + mut &l := localLoc + + l.name = "Local" + + mut nzone := 1 + if i.StandardDate.Month > 0 { + nzone++ + } + l.zone = make([]zone, nzone) + + stdname, dstname := abbrev(i) + + mut std := unsafe { (&zone)(&l.zone[0]) } + std.name = stdname + if nzone == 1 { + // No daylight savings. + std.offset = -int(i.Bias) * 60 + l.cacheStart = alpha + l.cacheEnd = omega + l.cacheZone = std + l.tx = make([]zoneTrans, 1) + l.tx[0].when = l.cacheStart + l.tx[0].index = 0 + ret + } + + // StandardBias must be ignored if StandardDate is not set, + // so this computation is delayed until after the nzone==1 + // return above. + std.offset = -int(i.Bias + i.StandardBias) * 60 + + mut dst := unsafe { (&zone)(&l.zone[1]) } + dst.name = dstname + dst.offset = -int(i.Bias + i.DaylightBias) * 60 + dst.isDST = true + + // Arrange so that d0 is first transition date, d1 second, + // i0 is index of zone after first transition, i1 second. + mut d0 := unsafe { (&systemtime)(&i.StandardDate) } + mut d1 := unsafe { (&systemtime)(&i.DaylightDate) } + mut i0 := 0 + mut i1 := 1 + if d0.Month > d1.Month { + d0, d1 = d1, d0 + i0, i1 = i1, i0 + } + + // 2 tx per year, 100 years on each side of this year + l.tx = make([]zoneTrans, 400) + + t := Now() + year := t.Year() + mut txi := 0 + mut y := year - 100 + for y < year+100; y++ { + mut tx := unsafe { (&zoneTrans)(&l.tx[txi]) } + tx.when = pseudoUnix(y, *d0) - i64(l.zone[i1].offset) + tx.index = u8(i0) + txi++ + + tx = unsafe { (&zoneTrans)(&l.tx[txi]) } + tx.when = pseudoUnix(y, *d1) - i64(l.zone[i0].offset) + tx.index = u8(i1) + txi++ + } +} + +fn initLocal() { + mut i := timezoneinformation{} + r := unsafe { cpp.GetTimeZoneInformation((*cpp.TIME_ZONE_INFORMATION)(&i)) } + if r == _TIME_ZONE_ID_INVALID { + // Fall back to UTC. + localLoc.name = "UTC" + ret + } + initLocalFromTZI(i) +} \ No newline at end of file