├── .gitignore ├── LICENSE ├── README.md ├── TODO.md ├── build.zig ├── examples ├── dump.zig ├── localtime.zig └── read-all-zoneinfo.zig ├── tzif.zig ├── zig.mod └── zoneinfo ├── Pacific └── Honolulu └── UTC /.gitignore: -------------------------------------------------------------------------------- 1 | .zig-cache/ 2 | zig-out/ 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 LeRoyce Pearson 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Zig TZif 2 | 3 | This repository implements TZif parsing, according to [RFC 8536][]. 4 | 5 | [rfc 8536]: https://datatracker.ietf.org/doc/html/rfc8536 6 | 7 | ## Usage 8 | 9 | Take a look at the [examples][] to get an idea of this library works. I 10 | recommend starting with the [localtime][] example. 11 | 12 | [examples]: ./examples/ 13 | [localtime]: ./examples/localtime.zig 14 | 15 | ### Add it as a package 16 | 17 | To start, add zig-tzif to your `build.zig.zon`: 18 | 19 | ```zig 20 | .{ 21 | .name = "your-project", 22 | .version = "0.1.0", 23 | .dependencies = .{ 24 | .tzif = .{ 25 | .url = "https://github.com/leroycep/zig-tzif/archive/fdac55aa9b4a59b5b0dcba20866b6943fc00765d.tar.gz", 26 | .hash = "1220459c1522d67e7541b3500518c9db7d380aaa962d433e6704d87a21b643502e69", 27 | }, 28 | }, 29 | } 30 | ``` 31 | 32 | Then, add `zig-tzif` to executable (or library) in the `build.zig`: 33 | 34 | ```zig 35 | const Build = @import("std").Build; 36 | 37 | pub fn build(b: *Build) void { 38 | const target = b.standardTargetOptions(.{}); 39 | const optimize = b.standardOptimizeOption(.{}); 40 | 41 | // Get the tzif dependency 42 | const tzif = b.dependency("tzif", .{ 43 | .target = target, 44 | .optimize = optimize, 45 | }); 46 | 47 | const exe = b.addExecutable(.{ 48 | .name = "tzif", 49 | .root_source_file = .{ .path = "tzif.zig" }, 50 | .target = target, 51 | .optimize = optimize, 52 | }); 53 | 54 | // Add it as a module 55 | exe.addModule("tzif", tzif.module("tzif")); 56 | 57 | b.installArtifact(exe); 58 | } 59 | 60 | ``` 61 | 62 | ### Useful functions 63 | 64 | #### `tzif.parseFile(allocator, filename) !TimeZone` 65 | 66 | #### `tzif.parse(allocator, reader, seekableStream) !TimeZone` 67 | 68 | #### `TimeZone.localTimeFromUTC(this, utc_timestamp) ?ConversionResult` 69 | 70 | ## Caveats 71 | 72 | - This library has not been rigorously tested, it might not always produce the 73 | correct offset, especially for time zones that have changed between 74 | different Daylight Savings schemes. 75 | - Does not support version 1 files. Files must be version 2 or 3. 76 | -------------------------------------------------------------------------------- /TODO.md: -------------------------------------------------------------------------------- 1 | - [ ] Make use of leap seconds 2 | - [ ] Make use of `transitionIsStd` 3 | - [ ] Make use of `transitionIsUT` 4 | -------------------------------------------------------------------------------- /build.zig: -------------------------------------------------------------------------------- 1 | const Build = @import("std").Build; 2 | 3 | const Example = struct { 4 | name: []const u8, 5 | path: []const u8, 6 | }; 7 | 8 | const EXAMPLES = [_]Example{ 9 | .{ .name = "localtime", .path = "examples/localtime.zig" }, 10 | .{ .name = "dump", .path = "examples/dump.zig" }, 11 | .{ .name = "read-all-zoneinfo", .path = "examples/read-all-zoneinfo.zig" }, 12 | }; 13 | 14 | pub fn build(b: *Build) void { 15 | const optimize = b.standardOptimizeOption(.{}); 16 | const target = b.standardTargetOptions(.{}); 17 | 18 | const module = b.addModule("tzif", .{ 19 | .root_source_file = b.path("tzif.zig"), 20 | }); 21 | 22 | const lib = b.addStaticLibrary(.{ 23 | .name = "tzif", 24 | .root_source_file = b.path("tzif.zig"), 25 | .target = target, 26 | .optimize = optimize, 27 | }); 28 | b.installArtifact(lib); 29 | 30 | const main_tests = b.addTest(.{ 31 | .root_source_file = b.path("tzif.zig"), 32 | .optimize = optimize, 33 | }); 34 | 35 | const run_main_tests = b.addRunArtifact(main_tests); 36 | 37 | const test_step = b.step("test", "Run library tests"); 38 | test_step.dependOn(&run_main_tests.step); 39 | 40 | inline for (EXAMPLES) |example| { 41 | const exe = b.addExecutable(.{ 42 | .name = example.name, 43 | .root_source_file = b.path(example.path), 44 | .optimize = optimize, 45 | .target = target, 46 | }); 47 | exe.root_module.addImport("tzif", module); 48 | 49 | const run_example = b.addRunArtifact(exe); 50 | if (b.args) |args| { 51 | run_example.addArgs(args); 52 | } 53 | 54 | const run_example_step = b.step("example-" ++ example.name, "Run the `" ++ example.name ++ "` example"); 55 | run_example_step.dependOn(&run_example.step); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /examples/dump.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const tzif = @import("tzif"); 3 | 4 | pub fn main() !u8 { 5 | var gpa = std.heap.GeneralPurposeAllocator(.{}){}; 6 | defer _ = gpa.deinit(); 7 | const allocator = gpa.allocator(); 8 | 9 | const args = try std.process.argsAlloc(allocator); 10 | defer std.process.argsFree(allocator, args); 11 | 12 | if (args.len != 2) { 13 | std.log.err("Path to TZif file is required", .{}); 14 | return 1; 15 | } 16 | 17 | const localtime = try tzif.parseFile(allocator, args[1]); 18 | defer localtime.deinit(); 19 | 20 | std.log.info("TZ string: {s}", .{localtime.string}); 21 | std.log.info("TZif version: {s}", .{localtime.version.string()}); 22 | std.log.info("{} transition times", .{localtime.transitionTimes.len}); 23 | std.log.info("{} leap seconds", .{localtime.leapSeconds.len}); 24 | 25 | return 0; 26 | } 27 | -------------------------------------------------------------------------------- /examples/localtime.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const tzif = @import("tzif"); 3 | 4 | pub fn main() !void { 5 | var gpa = std.heap.GeneralPurposeAllocator(.{}){}; 6 | defer _ = gpa.deinit(); 7 | const allocator = gpa.allocator(); 8 | 9 | const localtime = try tzif.parseFile(allocator, "/etc/localtime"); 10 | defer localtime.deinit(); 11 | 12 | const now_utc = std.time.timestamp(); 13 | const now_converted = localtime.localTimeFromUTC(now_utc) orelse { 14 | std.log.err("Offset is not specified for current timezone", .{}); 15 | return; 16 | }; 17 | 18 | const out = std.io.getStdOut(); 19 | try out.writer().print("{}\n", .{now_converted.timestamp}); 20 | } 21 | -------------------------------------------------------------------------------- /examples/read-all-zoneinfo.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const tzif = @import("tzif"); 3 | 4 | pub fn main() !void { 5 | var gpa = std.heap.GeneralPurposeAllocator(.{}){}; 6 | defer _ = gpa.deinit(); 7 | const allocator = gpa.allocator(); 8 | 9 | const args = try std.process.argsAlloc(allocator); 10 | defer std.process.argsFree(allocator, args); 11 | 12 | const path_to_dir = if (args.len > 1) args[1] else "/usr/share/zoneinfo"; 13 | 14 | var successful_parse: usize = 0; 15 | var successful_tz_formatting: usize = 0; 16 | var successful_convert: usize = 0; 17 | var failed_parse: usize = 0; 18 | var failed_tz_formatting: usize = 0; 19 | var failed_convert: usize = 0; 20 | 21 | const cwd = std.fs.cwd(); 22 | const zoneinfo = try cwd.openIterableDir(path_to_dir, .{}); 23 | 24 | var walker = try zoneinfo.walk(allocator); 25 | defer walker.deinit(); 26 | while (try walker.next()) |entry| { 27 | if (entry.kind == .file) { 28 | const file = try zoneinfo.dir.openFile(entry.path, .{}); 29 | defer file.close(); 30 | 31 | if (tzif.parse(allocator, file.reader(), file.seekableStream())) |timezone| { 32 | defer timezone.deinit(); 33 | 34 | if (timezone.posixTZ) |posix_tz| { 35 | const formatted_by_tzifzig_library = try std.fmt.allocPrint(allocator, "{}", .{posix_tz}); 36 | defer allocator.free(formatted_by_tzifzig_library); 37 | if (!std.mem.eql(u8, formatted_by_tzifzig_library, timezone.string)) { 38 | failed_tz_formatting += 1; 39 | std.debug.print("{s}: PosixTZ formatting differs between library and file: file = \"{}\"; library = \"{}\"\n", .{ entry.path, std.zig.fmtEscapes(timezone.string), posix_tz }); 40 | } else { 41 | successful_tz_formatting += 1; 42 | } 43 | } 44 | 45 | successful_parse += 1; 46 | const utc = std.time.timestamp(); 47 | if (timezone.localTimeFromUTC(utc) != null) { 48 | successful_convert += 1; 49 | } else { 50 | failed_convert += 1; 51 | std.log.warn("Failed to convert with {s}", .{entry.path}); 52 | } 53 | } else |err| { 54 | failed_parse += 1; 55 | std.log.warn("Failed to parse {s}: {}", .{ entry.path, err }); 56 | } 57 | } 58 | } 59 | 60 | std.log.info("Parsed: {}/{}", .{ successful_parse, successful_parse + failed_parse }); 61 | std.log.info("Matching TZ parse/format roundtrip: {}/{}", .{ successful_tz_formatting, successful_tz_formatting + failed_tz_formatting }); 62 | std.log.info("Converted: {}/{}", .{ successful_convert, successful_convert + failed_convert }); 63 | } 64 | -------------------------------------------------------------------------------- /tzif.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const testing = std.testing; 3 | 4 | const log = std.log.scoped(.tzif); 5 | 6 | pub const TimeZone = struct { 7 | allocator: std.mem.Allocator, 8 | version: Version, 9 | transitionTimes: []i64, 10 | transitionTypes: []u8, 11 | localTimeTypes: []LocalTimeType, 12 | designations: []u8, 13 | leapSeconds: []LeapSecond, 14 | transitionIsStd: []bool, 15 | transitionIsUT: []bool, 16 | string: []u8, 17 | posixTZ: ?PosixTZ, 18 | 19 | pub fn deinit(this: @This()) void { 20 | this.allocator.free(this.transitionTimes); 21 | this.allocator.free(this.transitionTypes); 22 | this.allocator.free(this.localTimeTypes); 23 | this.allocator.free(this.designations); 24 | this.allocator.free(this.leapSeconds); 25 | this.allocator.free(this.transitionIsStd); 26 | this.allocator.free(this.transitionIsUT); 27 | this.allocator.free(this.string); 28 | } 29 | 30 | pub const ConversionResult = struct { 31 | timestamp: i64, 32 | offset: i32, 33 | is_daylight_saving_time: bool, 34 | designation: []const u8, 35 | }; 36 | 37 | pub fn localTimeFromUTC(this: @This(), utc: i64) ?ConversionResult { 38 | const transition_type_by_timestamp = getTransitionTypeByTimestamp(this.transitionTimes, utc); 39 | switch (transition_type_by_timestamp) { 40 | .first_local_time_type => { 41 | const local_time_type = this.localTimeTypes[0]; 42 | 43 | var designation = this.designations[local_time_type.designation_index .. this.designations.len - 1]; 44 | for (designation, 0..) |c, i| { 45 | if (c == 0) { 46 | designation = designation[0..i]; 47 | break; 48 | } 49 | } 50 | 51 | return ConversionResult{ 52 | .timestamp = utc + local_time_type.ut_offset, 53 | .offset = local_time_type.ut_offset, 54 | .is_daylight_saving_time = local_time_type.is_daylight_saving_time, 55 | .designation = designation, 56 | }; 57 | }, 58 | .transition_index => |transition_index| { 59 | const local_time_type_idx = this.transitionTypes[transition_index]; 60 | const local_time_type = this.localTimeTypes[local_time_type_idx]; 61 | 62 | var designation = this.designations[local_time_type.designation_index .. this.designations.len - 1]; 63 | for (designation, 0..) |c, i| { 64 | if (c == 0) { 65 | designation = designation[0..i]; 66 | break; 67 | } 68 | } 69 | 70 | return ConversionResult{ 71 | .timestamp = utc + local_time_type.ut_offset, 72 | .offset = local_time_type.ut_offset, 73 | .is_daylight_saving_time = local_time_type.is_daylight_saving_time, 74 | .designation = designation, 75 | }; 76 | }, 77 | .specified_by_posix_tz, 78 | .specified_by_posix_tz_or_index_0, 79 | => if (this.posixTZ) |posixTZ| { 80 | // Base offset on the TZ string 81 | const offset_res = posixTZ.offset(utc); 82 | return ConversionResult{ 83 | .timestamp = utc + offset_res.offset, 84 | .offset = offset_res.offset, 85 | .is_daylight_saving_time = offset_res.is_daylight_saving_time, 86 | .designation = offset_res.designation, 87 | }; 88 | } else { 89 | switch (transition_type_by_timestamp) { 90 | .specified_by_posix_tz => return null, 91 | .specified_by_posix_tz_or_index_0 => { 92 | const local_time_type = this.localTimeTypes[0]; 93 | 94 | var designation = this.designations[local_time_type.designation_index .. this.designations.len - 1]; 95 | for (designation, 0..) |c, i| { 96 | if (c == 0) { 97 | designation = designation[0..i]; 98 | break; 99 | } 100 | } 101 | 102 | return ConversionResult{ 103 | .timestamp = utc + local_time_type.ut_offset, 104 | .offset = local_time_type.ut_offset, 105 | .is_daylight_saving_time = local_time_type.is_daylight_saving_time, 106 | .designation = designation, 107 | }; 108 | }, 109 | else => unreachable, 110 | } 111 | }, 112 | } 113 | } 114 | }; 115 | 116 | pub const Version = enum(u8) { 117 | V1 = 0, 118 | V2 = '2', 119 | V3 = '3', 120 | 121 | pub fn timeSize(this: @This()) u32 { 122 | return switch (this) { 123 | .V1 => 4, 124 | .V2, .V3 => 8, 125 | }; 126 | } 127 | 128 | pub fn leapSize(this: @This()) u32 { 129 | return this.timeSize() + 4; 130 | } 131 | 132 | pub fn string(this: @This()) []const u8 { 133 | return switch (this) { 134 | .V1 => "1", 135 | .V2 => "2", 136 | .V3 => "3", 137 | }; 138 | } 139 | }; 140 | 141 | pub const LocalTimeType = struct { 142 | /// An i32 specifying the number of seconds to be added to UT in order to determine local time. 143 | /// The value MUST NOT be -2**31 and SHOULD be in the range 144 | /// [-89999, 93599] (i.e., its value SHOULD be more than -25 hours 145 | /// and less than 26 hours). Avoiding -2**31 allows 32-bit clients 146 | /// to negate the value without overflow. Restricting it to 147 | /// [-89999, 93599] allows easy support by implementations that 148 | /// already support the POSIX-required range [-24:59:59, 25:59:59]. 149 | ut_offset: i32, 150 | 151 | /// A value indicating whether local time should be considered Daylight Saving Time (DST). 152 | /// 153 | /// A value of `true` indicates that this type of time is DST. 154 | /// A value of `false` indicates that this time type is standard time. 155 | is_daylight_saving_time: bool, 156 | 157 | /// A u8 specifying an index into the time zone designations, thereby 158 | /// selecting a particular designation string. Each index MUST be 159 | /// in the range [0, "charcnt" - 1]; it designates the 160 | /// NUL-terminated string of octets starting at position `designation_index` in 161 | /// the time zone designations. (This string MAY be empty.) A NUL 162 | /// octet MUST exist in the time zone designations at or after 163 | /// position `designation_index`. 164 | designation_index: u8, 165 | }; 166 | 167 | pub const LeapSecond = struct { 168 | occur: i64, 169 | corr: i32, 170 | }; 171 | 172 | /// This is based on Posix definition of the TZ environment variable 173 | pub const PosixTZ = struct { 174 | std_designation: []const u8, 175 | std_offset: i32, 176 | dst_designation: ?[]const u8 = null, 177 | /// This field is ignored when dst is null 178 | dst_offset: i32 = 0, 179 | dst_range: ?struct { 180 | start: Rule, 181 | end: Rule, 182 | } = null, 183 | 184 | pub const Rule = union(enum) { 185 | JulianDay: struct { 186 | /// 1 <= day <= 365. Leap days are not counted and are impossible to refer to 187 | day: u16, 188 | /// The default DST transition time is 02:00:00 local time 189 | time: i32 = 2 * std.time.s_per_hour, 190 | }, 191 | JulianDayZero: struct { 192 | /// 0 <= day <= 365. Leap days are counted, and can be referred to. 193 | day: u16, 194 | /// The default DST transition time is 02:00:00 local time 195 | time: i32 = 2 * std.time.s_per_hour, 196 | }, 197 | /// In the format of "Mm.n.d", where m = month, n = n, and d = day. 198 | MonthNthWeekDay: struct { 199 | /// Month of the year. 1 <= month <= 12 200 | month: u8, 201 | /// Specifies which of the weekdays should be used. Does NOT specify the week of the month! 1 <= week <= 5. 202 | /// 203 | /// Let's use M3.2.0 as an example. The month is 3, which translates to March. 204 | /// The day is 0, which means Sunday. `n` is 2, which means the second Sunday 205 | /// in the month, NOT Sunday of the second week! 206 | /// 207 | /// In 2021, this is difference between 2023-03-07 (Sunday of the second week of March) 208 | /// and 2023-03-14 (the Second Sunday of March). 209 | /// 210 | /// * When n is 1, it means the first week in which the day `day` occurs. 211 | /// * 5 is a special case. When n is 5, it means "the last day `day` in the month", which may occur in either the fourth or the fifth week. 212 | n: u8, 213 | /// Day of the week. 0 <= day <= 6. Day zero is Sunday. 214 | day: u8, 215 | /// The default DST transition time is 02:00:00 local time 216 | time: i32 = 2 * std.time.s_per_hour, 217 | }, 218 | 219 | pub fn isAtStartOfYear(this: @This()) bool { 220 | switch (this) { 221 | .JulianDay => |j| return j.day == 1 and j.time == 0, 222 | .JulianDayZero => |j| return j.day == 0 and j.time == 0, 223 | .MonthNthWeekDay => |mwd| return mwd.month == 1 and mwd.n == 1 and mwd.day == 0 and mwd.time == 0, 224 | } 225 | } 226 | 227 | pub fn isAtEndOfYear(this: @This()) bool { 228 | switch (this) { 229 | .JulianDay => |j| return j.day == 365 and j.time >= 24, 230 | // Since JulianDayZero dates account for leap year, it would vary depending on the year. 231 | .JulianDayZero => return false, 232 | // There is also no way to specify "end of the year" with MonthNthWeekDay rules 233 | .MonthNthWeekDay => return false, 234 | } 235 | } 236 | 237 | /// Returned value is the local timestamp when the timezone will transition in the given year. 238 | pub fn toSecs(this: @This(), year: i32) i64 { 239 | const is_leap: bool = isLeapYear(year); 240 | const start_of_year = year_to_secs(year); 241 | 242 | var t = start_of_year; 243 | 244 | switch (this) { 245 | .JulianDay => |j| { 246 | var x: i64 = j.day; 247 | if (x < 60 or !is_leap) x -= 1; 248 | t += std.time.s_per_day * x; 249 | t += j.time; 250 | }, 251 | .JulianDayZero => |j| { 252 | t += std.time.s_per_day * @as(i64, j.day); 253 | t += j.time; 254 | }, 255 | .MonthNthWeekDay => |mwd| { 256 | const offset_of_month_in_year = month_to_secs(mwd.month - 1, is_leap); 257 | 258 | const UNIX_EPOCH_WEEKDAY = 4; // Thursday 259 | const DAYS_PER_WEEK = 7; 260 | 261 | const days_since_epoch = @divFloor(start_of_year + offset_of_month_in_year, std.time.s_per_day); 262 | 263 | const first_weekday_of_month = @mod(days_since_epoch + UNIX_EPOCH_WEEKDAY, DAYS_PER_WEEK); 264 | 265 | const weekday_offset_for_month = if (first_weekday_of_month <= mwd.day) 266 | // the first matching weekday is during the first week of the month 267 | mwd.day - first_weekday_of_month 268 | else 269 | // the first matching weekday is during the second week of the month 270 | mwd.day + DAYS_PER_WEEK - first_weekday_of_month; 271 | 272 | const days_since_start_of_month = switch (mwd.n) { 273 | 1...4 => |n| (n - 1) * DAYS_PER_WEEK + weekday_offset_for_month, 274 | 5 => if (weekday_offset_for_month + (4 * DAYS_PER_WEEK) >= days_in_month(mwd.month, is_leap)) 275 | // the last matching weekday is during the 4th week of the month 276 | (4 - 1) * DAYS_PER_WEEK + weekday_offset_for_month 277 | else 278 | // the last matching weekday is during the 5th week of the month 279 | (5 - 1) * DAYS_PER_WEEK + weekday_offset_for_month, 280 | else => unreachable, 281 | }; 282 | 283 | t += offset_of_month_in_year + std.time.s_per_day * days_since_start_of_month; 284 | t += mwd.time; 285 | }, 286 | } 287 | return t; 288 | } 289 | 290 | pub fn format( 291 | this: @This(), 292 | comptime fmt: []const u8, 293 | options: std.fmt.FormatOptions, 294 | writer: anytype, 295 | ) !void { 296 | _ = fmt; 297 | _ = options; 298 | 299 | switch (this) { 300 | .JulianDay => |julian_day| { 301 | try std.fmt.format(writer, "J{}", .{julian_day.day}); 302 | }, 303 | .JulianDayZero => |julian_day_zero| { 304 | try std.fmt.format(writer, "{}", .{julian_day_zero.day}); 305 | }, 306 | .MonthNthWeekDay => |month_week_day| { 307 | try std.fmt.format(writer, "M{}.{}.{}", .{ 308 | month_week_day.month, 309 | month_week_day.n, 310 | month_week_day.day, 311 | }); 312 | }, 313 | } 314 | 315 | const time = switch (this) { 316 | inline else => |rule| rule.time, 317 | }; 318 | 319 | // Only write out the time if it is not the default time of 02:00 320 | if (time != 2 * std.time.s_per_hour) { 321 | const seconds = @mod(time, std.time.s_per_min); 322 | const minutes = @mod(@divTrunc(time, std.time.s_per_min), 60); 323 | const hours = @divTrunc(@divTrunc(time, std.time.s_per_min), 60); 324 | 325 | try std.fmt.format(writer, "/{}", .{hours}); 326 | if (minutes != 0 or seconds != 0) { 327 | try std.fmt.format(writer, ":{}", .{minutes}); 328 | } 329 | if (seconds != 0) { 330 | try std.fmt.format(writer, ":{}", .{seconds}); 331 | } 332 | } 333 | } 334 | }; 335 | 336 | pub const OffsetResult = struct { 337 | offset: i32, 338 | designation: []const u8, 339 | is_daylight_saving_time: bool, 340 | }; 341 | 342 | /// Get the offset from UTC for this PosixTZ, factoring in Daylight Saving Time. 343 | pub fn offset(this: @This(), utc: i64) OffsetResult { 344 | const dst_designation = this.dst_designation orelse { 345 | std.debug.assert(this.dst_range == null); 346 | return .{ .offset = this.std_offset, .designation = this.std_designation, .is_daylight_saving_time = false }; 347 | }; 348 | if (this.dst_range) |range| { 349 | const utc_year = secs_to_year(utc); 350 | const start_dst = range.start.toSecs(utc_year) - this.std_offset; 351 | const end_dst = range.end.toSecs(utc_year) - this.dst_offset; 352 | 353 | const is_dst_all_year = range.start.isAtStartOfYear() and range.end.isAtEndOfYear(); 354 | if (is_dst_all_year) { 355 | return .{ .offset = this.dst_offset, .designation = dst_designation, .is_daylight_saving_time = true }; 356 | } 357 | 358 | if (start_dst < end_dst) { 359 | if (utc >= start_dst and utc < end_dst) { 360 | return .{ .offset = this.dst_offset, .designation = dst_designation, .is_daylight_saving_time = true }; 361 | } else { 362 | return .{ .offset = this.std_offset, .designation = this.std_designation, .is_daylight_saving_time = false }; 363 | } 364 | } else { 365 | if (utc >= end_dst and utc < start_dst) { 366 | return .{ .offset = this.std_offset, .designation = this.std_designation, .is_daylight_saving_time = false }; 367 | } else { 368 | return .{ .offset = this.dst_offset, .designation = dst_designation, .is_daylight_saving_time = true }; 369 | } 370 | } 371 | } else { 372 | return .{ .offset = this.std_offset, .designation = this.std_designation, .is_daylight_saving_time = false }; 373 | } 374 | } 375 | 376 | pub fn format( 377 | this: @This(), 378 | comptime fmt: []const u8, 379 | options: std.fmt.FormatOptions, 380 | writer: anytype, 381 | ) !void { 382 | _ = fmt; 383 | _ = options; 384 | 385 | const should_quote_std_designation = for (this.std_designation) |character| { 386 | if (!std.ascii.isAlphabetic(character)) { 387 | break true; 388 | } 389 | } else false; 390 | 391 | if (should_quote_std_designation) { 392 | try writer.writeAll("<"); 393 | try writer.writeAll(this.std_designation); 394 | try writer.writeAll(">"); 395 | } else { 396 | try writer.writeAll(this.std_designation); 397 | } 398 | 399 | const std_offset_west = -this.std_offset; 400 | const std_seconds = @rem(std_offset_west, std.time.s_per_min); 401 | const std_minutes = @rem(@divTrunc(std_offset_west, std.time.s_per_min), 60); 402 | const std_hours = @divTrunc(@divTrunc(std_offset_west, std.time.s_per_min), 60); 403 | 404 | try std.fmt.format(writer, "{}", .{std_hours}); 405 | if (std_minutes != 0 or std_seconds != 0) { 406 | try std.fmt.format(writer, ":{}", .{if (std_minutes < 0) -std_minutes else std_minutes}); 407 | } 408 | if (std_seconds != 0) { 409 | try std.fmt.format(writer, ":{}", .{if (std_seconds < 0) -std_seconds else std_seconds}); 410 | } 411 | 412 | if (this.dst_designation) |dst_designation| { 413 | const should_quote_dst_designation = for (dst_designation) |character| { 414 | if (!std.ascii.isAlphabetic(character)) { 415 | break true; 416 | } 417 | } else false; 418 | 419 | if (should_quote_dst_designation) { 420 | try writer.writeAll("<"); 421 | try writer.writeAll(dst_designation); 422 | try writer.writeAll(">"); 423 | } else { 424 | try writer.writeAll(dst_designation); 425 | } 426 | 427 | // Only write out the DST offset if it is not just the standard offset plus an hour 428 | if (this.dst_offset != this.std_offset + std.time.s_per_hour) { 429 | const dst_offset_west = -this.dst_offset; 430 | const dst_seconds = @rem(dst_offset_west, std.time.s_per_min); 431 | const dst_minutes = @rem(@divTrunc(dst_offset_west, std.time.s_per_min), 60); 432 | const dst_hours = @divTrunc(@divTrunc(dst_offset_west, std.time.s_per_min), 60); 433 | 434 | try std.fmt.format(writer, "{}", .{dst_hours}); 435 | if (dst_minutes != 0 or dst_seconds != 0) { 436 | try std.fmt.format(writer, ":{}", .{if (dst_minutes < 0) -dst_minutes else dst_minutes}); 437 | } 438 | if (dst_seconds != 0) { 439 | try std.fmt.format(writer, ":{}", .{if (dst_seconds < 0) -dst_seconds else dst_seconds}); 440 | } 441 | } 442 | } 443 | 444 | if (this.dst_range) |dst_range| { 445 | try std.fmt.format(writer, ",{},{}", .{ dst_range.start, dst_range.end }); 446 | } 447 | } 448 | 449 | test format { 450 | const america_denver = PosixTZ{ 451 | .std_designation = "MST", 452 | .std_offset = -25200, 453 | .dst_designation = "MDT", 454 | .dst_offset = -21600, 455 | .dst_range = .{ 456 | .start = .{ 457 | .MonthNthWeekDay = .{ 458 | .month = 3, 459 | .n = 2, 460 | .day = 0, 461 | .time = 2 * std.time.s_per_hour, 462 | }, 463 | }, 464 | .end = .{ 465 | .MonthNthWeekDay = .{ 466 | .month = 11, 467 | .n = 1, 468 | .day = 0, 469 | .time = 2 * std.time.s_per_hour, 470 | }, 471 | }, 472 | }, 473 | }; 474 | 475 | try std.testing.expectFmt("MST7MDT,M3.2.0,M11.1.0", "{}", .{america_denver}); 476 | 477 | const europe_berlin = PosixTZ{ 478 | .std_designation = "CET", 479 | .std_offset = 3600, 480 | .dst_designation = "CEST", 481 | .dst_offset = 7200, 482 | .dst_range = .{ 483 | .start = .{ 484 | .MonthNthWeekDay = .{ 485 | .month = 3, 486 | .n = 5, 487 | .day = 0, 488 | .time = 2 * std.time.s_per_hour, 489 | }, 490 | }, 491 | .end = .{ 492 | .MonthNthWeekDay = .{ 493 | .month = 10, 494 | .n = 5, 495 | .day = 0, 496 | .time = 3 * std.time.s_per_hour, 497 | }, 498 | }, 499 | }, 500 | }; 501 | try std.testing.expectFmt("CET-1CEST,M3.5.0,M10.5.0/3", "{}", .{europe_berlin}); 502 | 503 | const antarctica_syowa = PosixTZ{ 504 | .std_designation = "+03", 505 | .std_offset = 3 * std.time.s_per_hour, 506 | .dst_designation = null, 507 | .dst_offset = undefined, 508 | .dst_range = null, 509 | }; 510 | try std.testing.expectFmt("<+03>-3", "{}", .{antarctica_syowa}); 511 | 512 | const pacific_chatham = PosixTZ{ 513 | .std_designation = "+1245", 514 | .std_offset = 12 * std.time.s_per_hour + 45 * std.time.s_per_min, 515 | .dst_designation = "+1345", 516 | .dst_offset = 13 * std.time.s_per_hour + 45 * std.time.s_per_min, 517 | .dst_range = .{ 518 | .start = .{ 519 | .MonthNthWeekDay = .{ 520 | .month = 9, 521 | .n = 5, 522 | .day = 0, 523 | .time = 2 * std.time.s_per_hour + 45 * std.time.s_per_min, 524 | }, 525 | }, 526 | .end = .{ 527 | .MonthNthWeekDay = .{ 528 | .month = 4, 529 | .n = 1, 530 | .day = 0, 531 | .time = 3 * std.time.s_per_hour + 45 * std.time.s_per_min, 532 | }, 533 | }, 534 | }, 535 | }; 536 | try std.testing.expectFmt("<+1245>-12:45<+1345>,M9.5.0/2:45,M4.1.0/3:45", "{}", .{pacific_chatham}); 537 | } 538 | }; 539 | 540 | fn days_in_month(m: u8, is_leap: bool) i32 { 541 | if (m == 2) { 542 | return 28 + @as(i32, @intFromBool(is_leap)); 543 | } else { 544 | return 30 + ((@as(i32, 0xad5) >> @as(u5, @intCast(m - 1))) & 1); 545 | } 546 | } 547 | 548 | fn month_to_secs(m: u8, is_leap: bool) i32 { 549 | const d = std.time.s_per_day; 550 | const secs_though_month = [12]i32{ 551 | 0 * d, 31 * d, 59 * d, 90 * d, 552 | 120 * d, 151 * d, 181 * d, 212 * d, 553 | 243 * d, 273 * d, 304 * d, 334 * d, 554 | }; 555 | var t = secs_though_month[m]; 556 | if (is_leap and m >= 2) t += d; 557 | return t; 558 | } 559 | 560 | fn secs_to_year(secs: i64) i32 { 561 | // Copied from MUSL 562 | // TODO: make more efficient? 563 | var y = @as(i32, @intCast(@divFloor(secs, std.time.s_per_day * 365) + 1970)); 564 | while (year_to_secs(y) > secs) y -= 1; 565 | while (year_to_secs(y + 1) < secs) y += 1; 566 | return y; 567 | } 568 | 569 | test secs_to_year { 570 | try std.testing.expectEqual(@as(i32, 1970), secs_to_year(0)); 571 | try std.testing.expectEqual(@as(i32, 2023), secs_to_year(1672531200)); 572 | } 573 | 574 | fn isLeapYear(year: i32) bool { 575 | return @mod(year, 4) == 0 and (@mod(year, 100) != 0 or @mod(year, 400) == 0); 576 | } 577 | 578 | test isLeapYear { 579 | const leap_years_1800_to_2400 = [_]i32{ 580 | 1804, 1808, 1812, 1816, 1820, 1824, 1828, 581 | 1832, 1836, 1840, 1844, 1848, 1852, 1856, 582 | 1860, 1864, 1868, 1872, 1876, 1880, 1884, 583 | 1888, 1892, 1896, 1904, 1908, 1912, 1916, 584 | 1920, 1924, 1928, 1932, 1936, 1940, 1944, 585 | 1948, 1952, 1956, 1960, 1964, 1968, 1972, 586 | 1976, 1980, 1984, 1988, 1992, 1996, 2000, 587 | 2004, 2008, 2012, 2016, 2020, 2024, 2028, 588 | 2032, 2036, 2040, 2044, 2048, 2052, 2056, 589 | 2060, 2064, 2068, 2072, 2076, 2080, 2084, 590 | 2088, 2092, 2096, 2104, 2108, 2112, 2116, 591 | 2120, 2124, 2128, 2132, 2136, 2140, 2144, 592 | 2148, 2152, 2156, 2160, 2164, 2168, 2172, 593 | 2176, 2180, 2184, 2188, 2192, 2196, 2204, 594 | 2208, 2212, 2216, 2220, 2224, 2228, 2232, 595 | 2236, 2240, 2244, 2248, 2252, 2256, 2260, 596 | 2264, 2268, 2272, 2276, 2280, 2284, 2288, 597 | 2292, 2296, 2304, 2308, 2312, 2316, 2320, 598 | 2324, 2328, 2332, 2336, 2340, 2344, 2348, 599 | 2352, 2356, 2360, 2364, 2368, 2372, 2376, 600 | 2380, 2384, 2388, 2392, 2396, 2400, 601 | }; 602 | 603 | for (leap_years_1800_to_2400) |leap_year| { 604 | errdefer std.debug.print("year = {}\n", .{leap_year}); 605 | try std.testing.expect(isLeapYear(leap_year)); 606 | } 607 | try std.testing.expect(!isLeapYear(2021)); 608 | try std.testing.expect(!isLeapYear(2023)); 609 | } 610 | 611 | const UNIX_EPOCH_YEAR = 1970; 612 | const UNIX_EPOCH_NUMBER_OF_4_YEAR_PERIODS = UNIX_EPOCH_YEAR / 4; 613 | const UNIX_EPOCH_CENTURIES = UNIX_EPOCH_YEAR / 100; 614 | /// Number of 400 year periods before the unix epoch 615 | const UNIX_EPOCH_CYCLES = UNIX_EPOCH_YEAR / 400; 616 | 617 | /// Takes in year number, returns the unix timestamp for the start of the year. 618 | fn year_to_secs(year: i32) i64 { 619 | const number_of_four_year_periods = @divFloor(year - 1, 4); 620 | const centuries = @divFloor(year - 1, 100); 621 | const cycles = @divFloor(year - 1, 400); 622 | 623 | const years_since_epoch = year - UNIX_EPOCH_YEAR; 624 | const number_of_four_year_periods_since_epoch = number_of_four_year_periods - UNIX_EPOCH_NUMBER_OF_4_YEAR_PERIODS; 625 | const centuries_since_epoch = centuries - UNIX_EPOCH_CENTURIES; 626 | const cycles_since_epoch = cycles - UNIX_EPOCH_CYCLES; 627 | 628 | const number_of_leap_days_since_epoch = 629 | number_of_four_year_periods_since_epoch - 630 | centuries_since_epoch + 631 | cycles_since_epoch; 632 | 633 | const SECONDS_PER_REGULAR_YEAR = 365 * std.time.s_per_day; 634 | return @as(i64, years_since_epoch) * SECONDS_PER_REGULAR_YEAR + number_of_leap_days_since_epoch * std.time.s_per_day; 635 | } 636 | 637 | test year_to_secs { 638 | try std.testing.expectEqual(@as(i64, 0), year_to_secs(1970)); 639 | try std.testing.expectEqual(@as(i64, 1577836800), year_to_secs(2020)); 640 | try std.testing.expectEqual(@as(i64, 1609459200), year_to_secs(2021)); 641 | try std.testing.expectEqual(@as(i64, 1640995200), year_to_secs(2022)); 642 | try std.testing.expectEqual(@as(i64, 1672531200), year_to_secs(2023)); 643 | } 644 | 645 | const TIME_TYPE_SIZE = 6; 646 | 647 | pub const TZifHeader = struct { 648 | version: Version, 649 | isutcnt: u32, 650 | isstdcnt: u32, 651 | leapcnt: u32, 652 | timecnt: u32, 653 | typecnt: u32, 654 | charcnt: u32, 655 | 656 | pub fn dataSize(this: @This(), dataBlockVersion: Version) u32 { 657 | return this.timecnt * dataBlockVersion.timeSize() + 658 | this.timecnt + 659 | this.typecnt * TIME_TYPE_SIZE + 660 | this.charcnt + 661 | this.leapcnt * dataBlockVersion.leapSize() + 662 | this.isstdcnt + 663 | this.isutcnt; 664 | } 665 | }; 666 | 667 | pub fn parseHeader(reader: anytype, seekableStream: anytype) !TZifHeader { 668 | var magic_buf: [4]u8 = undefined; 669 | try reader.readNoEof(&magic_buf); 670 | if (!std.mem.eql(u8, "TZif", &magic_buf)) { 671 | return error.InvalidFormat; // Magic number "TZif" is missing 672 | } 673 | 674 | // Check verison 675 | const version = reader.readEnum(Version, .little) catch |err| switch (err) { 676 | error.InvalidValue => return error.UnsupportedVersion, 677 | else => |e| return e, 678 | }; 679 | if (version == .V1) { 680 | return error.UnsupportedVersion; 681 | } 682 | 683 | // Seek past reserved bytes 684 | try seekableStream.seekBy(15); 685 | 686 | return TZifHeader{ 687 | .version = version, 688 | .isutcnt = try reader.readInt(u32, .big), 689 | .isstdcnt = try reader.readInt(u32, .big), 690 | .leapcnt = try reader.readInt(u32, .big), 691 | .timecnt = try reader.readInt(u32, .big), 692 | .typecnt = try reader.readInt(u32, .big), 693 | .charcnt = try reader.readInt(u32, .big), 694 | }; 695 | } 696 | 697 | /// Parses hh[:mm[:ss]] to a number of seconds. Hours may be one digit long. Minutes and seconds must be two digits. 698 | fn hhmmss_offset_to_s(_string: []const u8, idx: *usize) !i32 { 699 | var string = _string; 700 | var sign: i2 = 1; 701 | if (string[0] == '+') { 702 | sign = 1; 703 | string = string[1..]; 704 | idx.* += 1; 705 | } else if (string[0] == '-') { 706 | sign = -1; 707 | string = string[1..]; 708 | idx.* += 1; 709 | } 710 | 711 | for (string, 0..) |c, i| { 712 | if (!(std.ascii.isDigit(c) or c == ':')) { 713 | string = string[0..i]; 714 | break; 715 | } 716 | idx.* += 1; 717 | } 718 | 719 | var result: i32 = 0; 720 | 721 | var segment_iter = std.mem.split(u8, string, ":"); 722 | const hour_string = segment_iter.next() orelse return error.EmptyString; 723 | const hours = std.fmt.parseInt(u32, hour_string, 10) catch |err| switch (err) { 724 | error.InvalidCharacter => return error.InvalidFormat, 725 | error.Overflow => return error.InvalidFormat, 726 | }; 727 | if (hours > 167) { 728 | // TODO: use diagnostics mechanism instead of logging 729 | log.warn("too many hours! {}", .{hours}); 730 | return error.InvalidFormat; 731 | } 732 | result += std.time.s_per_hour * @as(i32, @intCast(hours)); 733 | 734 | if (segment_iter.next()) |minute_string| { 735 | if (minute_string.len != 2) { 736 | // TODO: Add diagnostics when returning an error. 737 | return error.InvalidFormat; 738 | } 739 | const minutes = try std.fmt.parseInt(u32, minute_string, 10); 740 | if (minutes > 59) return error.InvalidFormat; 741 | result += std.time.s_per_min * @as(i32, @intCast(minutes)); 742 | } 743 | 744 | if (segment_iter.next()) |second_string| { 745 | if (second_string.len != 2) { 746 | // TODO: Add diagnostics when returning an error. 747 | return error.InvalidFormat; 748 | } 749 | const seconds = try std.fmt.parseInt(u8, second_string, 10); 750 | if (seconds > 59) return error.InvalidFormat; 751 | result += seconds; 752 | } 753 | 754 | return result * sign; 755 | } 756 | 757 | fn parsePosixTZ_rule(_string: []const u8) !PosixTZ.Rule { 758 | var string = _string; 759 | if (string.len < 2) return error.InvalidFormat; 760 | 761 | const time: i32 = if (std.mem.indexOf(u8, string, "/")) |start_of_time| parse_time: { 762 | const time_string = string[start_of_time + 1 ..]; 763 | 764 | var i: usize = 0; 765 | const time = try hhmmss_offset_to_s(time_string, &i); 766 | 767 | // The time at the end of the rule should be the last thing in the string. Fixes the parsing to return 768 | // an error in cases like "/2/3", where they have some extra characters. 769 | if (i != time_string.len) { 770 | return error.InvalidFormat; 771 | } 772 | 773 | string = string[0..start_of_time]; 774 | 775 | break :parse_time time; 776 | } else 2 * std.time.s_per_hour; 777 | 778 | if (string[0] == 'J') { 779 | const julian_day1 = std.fmt.parseInt(u16, string[1..], 10) catch |err| switch (err) { 780 | error.InvalidCharacter => return error.InvalidFormat, 781 | error.Overflow => return error.InvalidFormat, 782 | }; 783 | 784 | if (julian_day1 < 1 or julian_day1 > 365) return error.InvalidFormat; 785 | return PosixTZ.Rule{ .JulianDay = .{ .day = julian_day1, .time = time } }; 786 | } else if (std.ascii.isDigit(string[0])) { 787 | const julian_day0 = std.fmt.parseInt(u16, string[0..], 10) catch |err| switch (err) { 788 | error.InvalidCharacter => return error.InvalidFormat, 789 | error.Overflow => return error.InvalidFormat, 790 | }; 791 | 792 | if (julian_day0 > 365) return error.InvalidFormat; 793 | return PosixTZ.Rule{ .JulianDayZero = .{ .day = julian_day0, .time = time } }; 794 | } else if (string[0] == 'M') { 795 | var split_iter = std.mem.split(u8, string[1..], "."); 796 | const m_str = split_iter.next() orelse return error.InvalidFormat; 797 | const n_str = split_iter.next() orelse return error.InvalidFormat; 798 | const d_str = split_iter.next() orelse return error.InvalidFormat; 799 | 800 | const m = std.fmt.parseInt(u8, m_str, 10) catch |err| switch (err) { 801 | error.InvalidCharacter => return error.InvalidFormat, 802 | error.Overflow => return error.InvalidFormat, 803 | }; 804 | const n = std.fmt.parseInt(u8, n_str, 10) catch |err| switch (err) { 805 | error.InvalidCharacter => return error.InvalidFormat, 806 | error.Overflow => return error.InvalidFormat, 807 | }; 808 | const d = std.fmt.parseInt(u8, d_str, 10) catch |err| switch (err) { 809 | error.InvalidCharacter => return error.InvalidFormat, 810 | error.Overflow => return error.InvalidFormat, 811 | }; 812 | 813 | if (m < 1 or m > 12) return error.InvalidFormat; 814 | if (n < 1 or n > 5) return error.InvalidFormat; 815 | if (d > 6) return error.InvalidFormat; 816 | 817 | return PosixTZ.Rule{ .MonthNthWeekDay = .{ .month = m, .n = n, .day = d, .time = time } }; 818 | } else { 819 | return error.InvalidFormat; 820 | } 821 | } 822 | 823 | fn parsePosixTZ_designation(string: []const u8, idx: *usize) ![]const u8 { 824 | const quoted = string[idx.*] == '<'; 825 | if (quoted) idx.* += 1; 826 | const start = idx.*; 827 | while (idx.* < string.len) : (idx.* += 1) { 828 | if ((quoted and string[idx.*] == '>') or 829 | (!quoted and !std.ascii.isAlphabetic(string[idx.*]))) 830 | { 831 | const designation = string[start..idx.*]; 832 | 833 | // The designation must be at least one character long! 834 | if (designation.len == 0) return error.InvalidFormat; 835 | 836 | if (quoted) idx.* += 1; 837 | return designation; 838 | } 839 | } 840 | return error.InvalidFormat; 841 | } 842 | 843 | pub fn parsePosixTZ(string: []const u8) !PosixTZ { 844 | var result = PosixTZ{ .std_designation = undefined, .std_offset = undefined }; 845 | var idx: usize = 0; 846 | 847 | result.std_designation = try parsePosixTZ_designation(string, &idx); 848 | 849 | // multiply by -1 to get offset as seconds East of Greenwich as TZif specifies it: 850 | result.std_offset = try hhmmss_offset_to_s(string[idx..], &idx) * -1; 851 | if (idx >= string.len) { 852 | return result; 853 | } 854 | 855 | if (string[idx] != ',') { 856 | result.dst_designation = try parsePosixTZ_designation(string, &idx); 857 | 858 | if (idx < string.len and string[idx] != ',') { 859 | // multiply by -1 to get offset as seconds East of Greenwich as TZif specifies it: 860 | result.dst_offset = try hhmmss_offset_to_s(string[idx..], &idx) * -1; 861 | } else { 862 | result.dst_offset = result.std_offset + std.time.s_per_hour; 863 | } 864 | 865 | if (idx >= string.len) { 866 | return result; 867 | } 868 | } 869 | 870 | std.debug.assert(string[idx] == ','); 871 | idx += 1; 872 | 873 | if (std.mem.indexOf(u8, string[idx..], ",")) |_end_of_start_rule| { 874 | const end_of_start_rule = idx + _end_of_start_rule; 875 | result.dst_range = .{ 876 | .start = try parsePosixTZ_rule(string[idx..end_of_start_rule]), 877 | .end = try parsePosixTZ_rule(string[end_of_start_rule + 1 ..]), 878 | }; 879 | } else { 880 | return error.InvalidFormat; 881 | } 882 | 883 | return result; 884 | } 885 | 886 | pub fn parse(allocator: std.mem.Allocator, reader: anytype, seekableStream: anytype) !TimeZone { 887 | const v1_header = try parseHeader(reader, seekableStream); 888 | try seekableStream.seekBy(v1_header.dataSize(.V1)); 889 | 890 | const v2_header = try parseHeader(reader, seekableStream); 891 | 892 | // Parse transition times 893 | var transition_times = try allocator.alloc(i64, v2_header.timecnt); 894 | errdefer allocator.free(transition_times); 895 | { 896 | var prev: i64 = -(2 << 59); // Earliest time supported, this is earlier than the big bang 897 | var i: usize = 0; 898 | while (i < transition_times.len) : (i += 1) { 899 | transition_times[i] = try reader.readInt(i64, .big); 900 | if (transition_times[i] <= prev) { 901 | return error.InvalidFormat; 902 | } 903 | prev = transition_times[i]; 904 | } 905 | } 906 | 907 | // Parse transition types 908 | const transition_types = try allocator.alloc(u8, v2_header.timecnt); 909 | errdefer allocator.free(transition_types); 910 | try reader.readNoEof(transition_types); 911 | for (transition_types) |transition_type| { 912 | if (transition_type >= v2_header.typecnt) { 913 | return error.InvalidFormat; // a transition type index is out of bounds 914 | } 915 | } 916 | 917 | // Parse local time type records 918 | var local_time_types = try allocator.alloc(LocalTimeType, v2_header.typecnt); 919 | errdefer allocator.free(local_time_types); 920 | { 921 | var i: usize = 0; 922 | while (i < local_time_types.len) : (i += 1) { 923 | local_time_types[i].ut_offset = try reader.readInt(i32, .big); 924 | local_time_types[i].is_daylight_saving_time = switch (try reader.readByte()) { 925 | 0 => false, 926 | 1 => true, 927 | else => return error.InvalidFormat, 928 | }; 929 | 930 | local_time_types[i].designation_index = try reader.readByte(); 931 | if (local_time_types[i].designation_index >= v2_header.charcnt) { 932 | return error.InvalidFormat; 933 | } 934 | } 935 | } 936 | 937 | // Read designations 938 | const time_zone_designations = try allocator.alloc(u8, v2_header.charcnt); 939 | errdefer allocator.free(time_zone_designations); 940 | try reader.readNoEof(time_zone_designations); 941 | 942 | // Parse leap seconds records 943 | var leap_seconds = try allocator.alloc(LeapSecond, v2_header.leapcnt); 944 | errdefer allocator.free(leap_seconds); 945 | { 946 | var i: usize = 0; 947 | while (i < leap_seconds.len) : (i += 1) { 948 | leap_seconds[i].occur = try reader.readInt(i64, .big); 949 | if (i == 0 and leap_seconds[i].occur < 0) { 950 | return error.InvalidFormat; 951 | } else if (i != 0 and leap_seconds[i].occur - leap_seconds[i - 1].occur < 2419199) { 952 | return error.InvalidFormat; // There must be at least 28 days worth of seconds between leap seconds 953 | } 954 | 955 | leap_seconds[i].corr = try reader.readInt(i32, .big); 956 | if (i == 0 and (leap_seconds[0].corr != 1 and leap_seconds[0].corr != -1)) { 957 | log.warn("First leap second correction is not 1 or -1: {}", .{leap_seconds[0]}); 958 | return error.InvalidFormat; 959 | } else if (i != 0) { 960 | const diff = leap_seconds[i].corr - leap_seconds[i - 1].corr; 961 | if (diff != 1 and diff != -1) { 962 | log.warn("Too large of a difference between leap seconds: {}", .{diff}); 963 | return error.InvalidFormat; 964 | } 965 | } 966 | } 967 | } 968 | 969 | // Parse standard/wall indicators 970 | var transition_is_std = try allocator.alloc(bool, v2_header.isstdcnt); 971 | errdefer allocator.free(transition_is_std); 972 | { 973 | var i: usize = 0; 974 | while (i < transition_is_std.len) : (i += 1) { 975 | transition_is_std[i] = switch (try reader.readByte()) { 976 | 1 => true, 977 | 0 => false, 978 | else => return error.InvalidFormat, 979 | }; 980 | } 981 | } 982 | 983 | // Parse UT/local indicators 984 | var transition_is_ut = try allocator.alloc(bool, v2_header.isutcnt); 985 | errdefer allocator.free(transition_is_ut); 986 | { 987 | var i: usize = 0; 988 | while (i < transition_is_ut.len) : (i += 1) { 989 | transition_is_ut[i] = switch (try reader.readByte()) { 990 | 1 => true, 991 | 0 => false, 992 | else => return error.InvalidFormat, 993 | }; 994 | } 995 | } 996 | 997 | // Parse TZ string from footer 998 | if ((try reader.readByte()) != '\n') return error.InvalidFormat; 999 | const tz_string = try reader.readUntilDelimiterAlloc(allocator, '\n', 60); 1000 | errdefer allocator.free(tz_string); 1001 | 1002 | const posixTZ: ?PosixTZ = if (tz_string.len > 0) 1003 | try parsePosixTZ(tz_string) 1004 | else 1005 | null; 1006 | 1007 | return TimeZone{ 1008 | .allocator = allocator, 1009 | .version = v2_header.version, 1010 | .transitionTimes = transition_times, 1011 | .transitionTypes = transition_types, 1012 | .localTimeTypes = local_time_types, 1013 | .designations = time_zone_designations, 1014 | .leapSeconds = leap_seconds, 1015 | .transitionIsStd = transition_is_std, 1016 | .transitionIsUT = transition_is_ut, 1017 | .string = tz_string, 1018 | .posixTZ = posixTZ, 1019 | }; 1020 | } 1021 | 1022 | pub fn parseFile(allocator: std.mem.Allocator, path: []const u8) !TimeZone { 1023 | const cwd = std.fs.cwd(); 1024 | 1025 | const file = try cwd.openFile(path, .{}); 1026 | defer file.close(); 1027 | 1028 | return parse(allocator, file.reader(), file.seekableStream()); 1029 | } 1030 | 1031 | const TransitionType = union(enum) { 1032 | first_local_time_type, 1033 | transition_index: usize, 1034 | specified_by_posix_tz, 1035 | specified_by_posix_tz_or_index_0, 1036 | }; 1037 | 1038 | /// Get the transition type of the first element in the `transition_times` array which is less than or equal to `timestamp_utc`. 1039 | /// 1040 | /// Returns `.transition_index` if the timestamp is contained inside the `transition_times` array. 1041 | /// 1042 | /// Returns `.specified_by_posix_tz_or_index_0` if the `transition_times` list is empty. 1043 | /// 1044 | /// Returns `.first_local_time_type` if `timestamp_utc` is before the first transition time. 1045 | /// 1046 | /// Returns `.specified_by_posix_tz` if `timestamp_utc` is after or on the last transition time. 1047 | fn getTransitionTypeByTimestamp(transition_times: []const i64, timestamp_utc: i64) TransitionType { 1048 | if (transition_times.len == 0) return .specified_by_posix_tz_or_index_0; 1049 | if (timestamp_utc < transition_times[0]) return .first_local_time_type; 1050 | if (timestamp_utc >= transition_times[transition_times.len - 1]) return .specified_by_posix_tz; 1051 | 1052 | var left: usize = 0; 1053 | var right: usize = transition_times.len; 1054 | 1055 | while (left < right) { 1056 | // Avoid overflowing in the midpoint calculation 1057 | const mid = left + (right - left) / 2; 1058 | // Compare the key with the midpoint element 1059 | if (transition_times[mid] == timestamp_utc) { 1060 | if (mid + 1 < transition_times.len) { 1061 | return .{ .transition_index = mid }; 1062 | } else { 1063 | return .{ .transition_index = mid }; 1064 | } 1065 | } else if (transition_times[mid] > timestamp_utc) { 1066 | right = mid; 1067 | } else if (transition_times[mid] < timestamp_utc) { 1068 | left = mid + 1; 1069 | } 1070 | } 1071 | 1072 | if (right >= transition_times.len) { 1073 | return .specified_by_posix_tz; 1074 | } else if (right > 0) { 1075 | return .{ .transition_index = right - 1 }; 1076 | } else { 1077 | return .first_local_time_type; 1078 | } 1079 | } 1080 | 1081 | test getTransitionTypeByTimestamp { 1082 | const transition_times = [7]i64{ -2334101314, -1157283000, -1155436200, -880198200, -769395600, -765376200, -712150200 }; 1083 | 1084 | try testing.expectEqual(TransitionType.first_local_time_type, getTransitionTypeByTimestamp(&transition_times, -2334101315)); 1085 | try testing.expectEqual(TransitionType{ .transition_index = 0 }, getTransitionTypeByTimestamp(&transition_times, -2334101314)); 1086 | try testing.expectEqual(TransitionType{ .transition_index = 0 }, getTransitionTypeByTimestamp(&transition_times, -2334101313)); 1087 | 1088 | try testing.expectEqual(TransitionType{ .transition_index = 0 }, getTransitionTypeByTimestamp(&transition_times, -1157283001)); 1089 | try testing.expectEqual(TransitionType{ .transition_index = 1 }, getTransitionTypeByTimestamp(&transition_times, -1157283000)); 1090 | try testing.expectEqual(TransitionType{ .transition_index = 1 }, getTransitionTypeByTimestamp(&transition_times, -1157282999)); 1091 | 1092 | try testing.expectEqual(TransitionType{ .transition_index = 1 }, getTransitionTypeByTimestamp(&transition_times, -1155436201)); 1093 | try testing.expectEqual(TransitionType{ .transition_index = 2 }, getTransitionTypeByTimestamp(&transition_times, -1155436200)); 1094 | try testing.expectEqual(TransitionType{ .transition_index = 2 }, getTransitionTypeByTimestamp(&transition_times, -1155436199)); 1095 | 1096 | try testing.expectEqual(TransitionType{ .transition_index = 2 }, getTransitionTypeByTimestamp(&transition_times, -880198201)); 1097 | try testing.expectEqual(TransitionType{ .transition_index = 3 }, getTransitionTypeByTimestamp(&transition_times, -880198200)); 1098 | try testing.expectEqual(TransitionType{ .transition_index = 3 }, getTransitionTypeByTimestamp(&transition_times, -880198199)); 1099 | 1100 | try testing.expectEqual(TransitionType{ .transition_index = 3 }, getTransitionTypeByTimestamp(&transition_times, -769395601)); 1101 | try testing.expectEqual(TransitionType{ .transition_index = 4 }, getTransitionTypeByTimestamp(&transition_times, -769395600)); 1102 | try testing.expectEqual(TransitionType{ .transition_index = 4 }, getTransitionTypeByTimestamp(&transition_times, -769395599)); 1103 | 1104 | try testing.expectEqual(TransitionType{ .transition_index = 4 }, getTransitionTypeByTimestamp(&transition_times, -765376201)); 1105 | try testing.expectEqual(TransitionType{ .transition_index = 5 }, getTransitionTypeByTimestamp(&transition_times, -765376200)); 1106 | try testing.expectEqual(TransitionType{ .transition_index = 5 }, getTransitionTypeByTimestamp(&transition_times, -765376199)); 1107 | 1108 | // Why is there 7 transition types if the last type is not used? 1109 | try testing.expectEqual(TransitionType{ .transition_index = 5 }, getTransitionTypeByTimestamp(&transition_times, -712150201)); 1110 | try testing.expectEqual(TransitionType.specified_by_posix_tz, getTransitionTypeByTimestamp(&transition_times, -712150200)); 1111 | try testing.expectEqual(TransitionType.specified_by_posix_tz, getTransitionTypeByTimestamp(&transition_times, -712150199)); 1112 | } 1113 | 1114 | test "parse invalid bytes" { 1115 | var fbs = std.io.fixedBufferStream("dflkasjreklnlkvnalkfek"); 1116 | try testing.expectError(error.InvalidFormat, parse(std.testing.allocator, fbs.reader(), fbs.seekableStream())); 1117 | } 1118 | 1119 | test "parse UTC zoneinfo" { 1120 | var fbs = std.io.fixedBufferStream(@embedFile("zoneinfo/UTC")); 1121 | 1122 | const res = try parse(std.testing.allocator, fbs.reader(), fbs.seekableStream()); 1123 | defer res.deinit(); 1124 | 1125 | try testing.expectEqual(Version.V2, res.version); 1126 | try testing.expectEqualSlices(i64, &[_]i64{}, res.transitionTimes); 1127 | try testing.expectEqualSlices(u8, &[_]u8{}, res.transitionTypes); 1128 | try testing.expectEqualSlices(LocalTimeType, &[_]LocalTimeType{.{ .ut_offset = 0, .is_daylight_saving_time = false, .designation_index = 0 }}, res.localTimeTypes); 1129 | try testing.expectEqualSlices(u8, "UTC\x00", res.designations); 1130 | } 1131 | 1132 | test "parse Pacific/Honolulu zoneinfo and calculate local times" { 1133 | const transition_times = [7]i64{ -2334101314, -1157283000, -1155436200, -880198200, -769395600, -765376200, -712150200 }; 1134 | const transition_types = [7]u8{ 1, 2, 1, 3, 4, 1, 5 }; 1135 | const local_time_types = [6]LocalTimeType{ 1136 | .{ .ut_offset = -37886, .is_daylight_saving_time = false, .designation_index = 0 }, 1137 | .{ .ut_offset = -37800, .is_daylight_saving_time = false, .designation_index = 4 }, 1138 | .{ .ut_offset = -34200, .is_daylight_saving_time = true, .designation_index = 8 }, 1139 | .{ .ut_offset = -34200, .is_daylight_saving_time = true, .designation_index = 12 }, 1140 | .{ .ut_offset = -34200, .is_daylight_saving_time = true, .designation_index = 16 }, 1141 | .{ .ut_offset = -36000, .is_daylight_saving_time = false, .designation_index = 4 }, 1142 | }; 1143 | const designations = "LMT\x00HST\x00HDT\x00HWT\x00HPT\x00"; 1144 | const is_std = &[6]bool{ false, false, false, false, true, false }; 1145 | const is_ut = &[6]bool{ false, false, false, false, true, false }; 1146 | const string = "HST10"; 1147 | 1148 | var fbs = std.io.fixedBufferStream(@embedFile("zoneinfo/Pacific/Honolulu")); 1149 | 1150 | const res = try parse(std.testing.allocator, fbs.reader(), fbs.seekableStream()); 1151 | defer res.deinit(); 1152 | 1153 | try testing.expectEqual(Version.V2, res.version); 1154 | try testing.expectEqualSlices(i64, &transition_times, res.transitionTimes); 1155 | try testing.expectEqualSlices(u8, &transition_types, res.transitionTypes); 1156 | try testing.expectEqualSlices(LocalTimeType, &local_time_types, res.localTimeTypes); 1157 | try testing.expectEqualSlices(u8, designations, res.designations); 1158 | try testing.expectEqualSlices(bool, is_std, res.transitionIsStd); 1159 | try testing.expectEqualSlices(bool, is_ut, res.transitionIsUT); 1160 | try testing.expectEqualSlices(u8, string, res.string); 1161 | 1162 | { 1163 | const conversion = res.localTimeFromUTC(-1156939200).?; 1164 | try testing.expectEqual(@as(i64, -1156973400), conversion.timestamp); 1165 | try testing.expectEqual(true, conversion.is_daylight_saving_time); 1166 | try testing.expectEqualSlices(u8, "HDT", conversion.designation); 1167 | } 1168 | { 1169 | // A second before the first timezone transition 1170 | const conversion = res.localTimeFromUTC(-2334101315).?; 1171 | try testing.expectEqual(@as(i64, -2334101315 - 37886), conversion.timestamp); 1172 | try testing.expectEqual(false, conversion.is_daylight_saving_time); 1173 | try testing.expectEqualSlices(u8, "LMT", conversion.designation); 1174 | } 1175 | { 1176 | // At the first timezone transition 1177 | const conversion = res.localTimeFromUTC(-2334101314).?; 1178 | try testing.expectEqual(@as(i64, -2334101314 - 37800), conversion.timestamp); 1179 | try testing.expectEqual(false, conversion.is_daylight_saving_time); 1180 | try testing.expectEqualSlices(u8, "HST", conversion.designation); 1181 | } 1182 | { 1183 | // After the first timezone transition 1184 | const conversion = res.localTimeFromUTC(-2334101313).?; 1185 | try testing.expectEqual(@as(i64, -2334101313 - 37800), conversion.timestamp); 1186 | try testing.expectEqual(false, conversion.is_daylight_saving_time); 1187 | try testing.expectEqualSlices(u8, "HST", conversion.designation); 1188 | } 1189 | { 1190 | // After the last timezone transition; conversion should be performed using the Posix TZ footer. 1191 | // Taken from RFC8536 Appendix B.2 1192 | const conversion = res.localTimeFromUTC(1546300800).?; 1193 | try testing.expectEqual(@as(i64, 1546300800) - 10 * std.time.s_per_hour, conversion.timestamp); 1194 | try testing.expectEqual(false, conversion.is_daylight_saving_time); 1195 | try testing.expectEqualSlices(u8, "HST", conversion.designation); 1196 | } 1197 | } 1198 | 1199 | test "posix TZ string, regular year" { 1200 | // IANA identifier America/Denver; default DST transition time at 2 am 1201 | var result = try parsePosixTZ("MST7MDT,M3.2.0,M11.1.0"); 1202 | var stdoff: i32 = -25200; 1203 | var dstoff: i32 = -21600; 1204 | try testing.expectEqualSlices(u8, "MST", result.std_designation); 1205 | try testing.expectEqual(stdoff, result.std_offset); 1206 | try testing.expectEqualSlices(u8, "MDT", result.dst_designation.?); 1207 | try testing.expectEqual(dstoff, result.dst_offset); 1208 | try testing.expectEqual(PosixTZ.Rule{ .MonthNthWeekDay = .{ .month = 3, .n = 2, .day = 0, .time = 2 * std.time.s_per_hour } }, result.dst_range.?.start); 1209 | try testing.expectEqual(PosixTZ.Rule{ .MonthNthWeekDay = .{ .month = 11, .n = 1, .day = 0, .time = 2 * std.time.s_per_hour } }, result.dst_range.?.end); 1210 | try testing.expectEqual(stdoff, result.offset(1612734960).offset); 1211 | // 2021-03-14T01:59:59-07:00 (2nd Sunday of the 3rd month, MST) 1212 | try testing.expectEqual(stdoff, result.offset(1615712399).offset); 1213 | // 2021-03-14T02:00:00-07:00 (2nd Sunday of the 3rd month, MST) 1214 | try testing.expectEqual(dstoff, result.offset(1615712400).offset); 1215 | try testing.expectEqual(dstoff, result.offset(1620453601).offset); 1216 | // 2021-11-07T01:59:59-06:00 (1st Sunday of the 11th month, MDT) 1217 | try testing.expectEqual(dstoff, result.offset(1636271999).offset); 1218 | // 2021-11-07T02:00:00-06:00 (1st Sunday of the 11th month, MDT) 1219 | try testing.expectEqual(stdoff, result.offset(1636272000).offset); 1220 | 1221 | // IANA identifier: Europe/Berlin 1222 | result = try parsePosixTZ("CET-1CEST,M3.5.0,M10.5.0/3"); 1223 | stdoff = 3600; 1224 | dstoff = 7200; 1225 | try testing.expectEqualSlices(u8, "CET", result.std_designation); 1226 | try testing.expectEqual(stdoff, result.std_offset); 1227 | try testing.expectEqualSlices(u8, "CEST", result.dst_designation.?); 1228 | try testing.expectEqual(dstoff, result.dst_offset); 1229 | try testing.expectEqual(PosixTZ.Rule{ .MonthNthWeekDay = .{ .month = 3, .n = 5, .day = 0, .time = 2 * std.time.s_per_hour } }, result.dst_range.?.start); 1230 | try testing.expectEqual(PosixTZ.Rule{ .MonthNthWeekDay = .{ .month = 10, .n = 5, .day = 0, .time = 3 * std.time.s_per_hour } }, result.dst_range.?.end); 1231 | // 2023-10-29T00:59:59Z, or 2023-10-29 01:59:59 CEST. Offset should still be CEST. 1232 | try testing.expectEqual(dstoff, result.offset(1698541199).offset); 1233 | // 2023-10-29T01:00:00Z, or 2023-10-29 03:00:00 CEST. Offset should now be CET. 1234 | try testing.expectEqual(stdoff, result.offset(1698541200).offset); 1235 | 1236 | // IANA identifier: America/New_York 1237 | result = try parsePosixTZ("EST5EDT,M3.2.0/02:00:00,M11.1.0"); 1238 | stdoff = -18000; 1239 | dstoff = -14400; 1240 | try testing.expectEqualSlices(u8, "EST", result.std_designation); 1241 | try testing.expectEqual(stdoff, result.std_offset); 1242 | try testing.expectEqualSlices(u8, "EDT", result.dst_designation.?); 1243 | try testing.expectEqual(dstoff, result.dst_offset); 1244 | // transition std 2023-03-12T01:59:59-05:00 --> dst 2023-03-12T03:00:00-04:00 1245 | try testing.expectEqual(stdoff, result.offset(1678604399).offset); 1246 | try testing.expectEqual(dstoff, result.offset(1678604400).offset); 1247 | // transition dst 2023-11-05T01:59:59-04:00 --> std 2023-11-05T01:00:00-05:00 1248 | try testing.expectEqual(dstoff, result.offset(1699163999).offset); 1249 | try testing.expectEqual(stdoff, result.offset(1699164000).offset); 1250 | 1251 | // IANA identifier: America/New_York 1252 | result = try parsePosixTZ("EST5EDT,M3.2.0/02:00:00,M11.1.0/02:00:00"); 1253 | stdoff = -18000; 1254 | dstoff = -14400; 1255 | try testing.expectEqualSlices(u8, "EST", result.std_designation); 1256 | try testing.expectEqual(stdoff, result.std_offset); 1257 | try testing.expectEqualSlices(u8, "EDT", result.dst_designation.?); 1258 | try testing.expectEqual(dstoff, result.dst_offset); 1259 | // transition std 2023-03-12T01:59:59-05:00 --> dst 2023-03-12T03:00:00-04:00 1260 | try testing.expectEqual(stdoff, result.offset(1678604399).offset); 1261 | try testing.expectEqual(dstoff, result.offset(1678604400).offset); 1262 | // transition dst 2023-11-05T01:59:59-04:00 --> std 2023-11-05T01:00:00-05:00 1263 | try testing.expectEqual(dstoff, result.offset(1699163999).offset); 1264 | try testing.expectEqual(stdoff, result.offset(1699164000).offset); 1265 | 1266 | // IANA identifier: America/New_York 1267 | result = try parsePosixTZ("EST5EDT,M3.2.0,M11.1.0/02:00:00"); 1268 | stdoff = -18000; 1269 | dstoff = -14400; 1270 | try testing.expectEqualSlices(u8, "EST", result.std_designation); 1271 | try testing.expectEqual(stdoff, result.std_offset); 1272 | try testing.expectEqualSlices(u8, "EDT", result.dst_designation.?); 1273 | try testing.expectEqual(dstoff, result.dst_offset); 1274 | // transition std 2023-03-12T01:59:59-05:00 --> dst 2023-03-12T03:00:00-04:00 1275 | try testing.expectEqual(stdoff, result.offset(1678604399).offset); 1276 | try testing.expectEqual(dstoff, result.offset(1678604400).offset); 1277 | // transition dst 2023-11-05T01:59:59-04:00 --> std 2023-11-05T01:00:00-05:00 1278 | try testing.expectEqual(dstoff, result.offset(1699163999).offset); 1279 | try testing.expectEqual(stdoff, result.offset(1699164000).offset); 1280 | 1281 | // IANA identifier: America/Chicago 1282 | result = try parsePosixTZ("CST6CDT,M3.2.0/2:00:00,M11.1.0/2:00:00"); 1283 | stdoff = -21600; 1284 | dstoff = -18000; 1285 | try testing.expectEqualSlices(u8, "CST", result.std_designation); 1286 | try testing.expectEqual(stdoff, result.std_offset); 1287 | try testing.expectEqualSlices(u8, "CDT", result.dst_designation.?); 1288 | try testing.expectEqual(dstoff, result.dst_offset); 1289 | // transition std 2023-03-12T01:59:59-06:00 --> dst 2023-03-12T03:00:00-05:00 1290 | try testing.expectEqual(stdoff, result.offset(1678607999).offset); 1291 | try testing.expectEqual(dstoff, result.offset(1678608000).offset); 1292 | // transition dst 2023-11-05T01:59:59-05:00 --> std 2023-11-05T01:00:00-06:00 1293 | try testing.expectEqual(dstoff, result.offset(1699167599).offset); 1294 | try testing.expectEqual(stdoff, result.offset(1699167600).offset); 1295 | 1296 | // IANA identifier: America/Denver 1297 | result = try parsePosixTZ("MST7MDT,M3.2.0/2:00:00,M11.1.0/2:00:00"); 1298 | stdoff = -25200; 1299 | dstoff = -21600; 1300 | try testing.expectEqualSlices(u8, "MST", result.std_designation); 1301 | try testing.expectEqual(stdoff, result.std_offset); 1302 | try testing.expectEqualSlices(u8, "MDT", result.dst_designation.?); 1303 | try testing.expectEqual(dstoff, result.dst_offset); 1304 | // transition std 2023-03-12T01:59:59-07:00 --> dst 2023-03-12T03:00:00-06:00 1305 | try testing.expectEqual(stdoff, result.offset(1678611599).offset); 1306 | try testing.expectEqual(dstoff, result.offset(1678611600).offset); 1307 | // transition dst 2023-11-05T01:59:59-06:00 --> std 2023-11-05T01:00:00-07:00 1308 | try testing.expectEqual(dstoff, result.offset(1699171199).offset); 1309 | try testing.expectEqual(stdoff, result.offset(1699171200).offset); 1310 | 1311 | // IANA identifier: America/Los_Angeles 1312 | result = try parsePosixTZ("PST8PDT,M3.2.0/2:00:00,M11.1.0/2:00:00"); 1313 | stdoff = -28800; 1314 | dstoff = -25200; 1315 | try testing.expectEqualSlices(u8, "PST", result.std_designation); 1316 | try testing.expectEqual(stdoff, result.std_offset); 1317 | try testing.expectEqualSlices(u8, "PDT", result.dst_designation.?); 1318 | try testing.expectEqual(dstoff, result.dst_offset); 1319 | // transition std 2023-03-12T01:59:59-08:00 --> dst 2023-03-12T03:00:00-07:00 1320 | try testing.expectEqual(stdoff, result.offset(1678615199).offset); 1321 | try testing.expectEqual(dstoff, result.offset(1678615200).offset); 1322 | // transition dst 2023-11-05T01:59:59-07:00 --> std 2023-11-05T01:00:00-08:00 1323 | try testing.expectEqual(dstoff, result.offset(1699174799).offset); 1324 | try testing.expectEqual(stdoff, result.offset(1699174800).offset); 1325 | 1326 | // IANA identifier: America/Sitka 1327 | result = try parsePosixTZ("AKST9AKDT,M3.2.0,M11.1.0"); 1328 | stdoff = -32400; 1329 | dstoff = -28800; 1330 | try testing.expectEqualSlices(u8, "AKST", result.std_designation); 1331 | try testing.expectEqual(stdoff, result.std_offset); 1332 | try testing.expectEqualSlices(u8, "AKDT", result.dst_designation.?); 1333 | try testing.expectEqual(dstoff, result.dst_offset); 1334 | // transition std 2023-03-12T01:59:59-09:00 --> dst 2023-03-12T03:00:00-08:00 1335 | try testing.expectEqual(stdoff, result.offset(1678618799).offset); 1336 | try testing.expectEqual(dstoff, result.offset(1678618800).offset); 1337 | // transition dst 2023-11-05T01:59:59-08:00 --> std 2023-11-05T01:00:00-09:00 1338 | try testing.expectEqual(dstoff, result.offset(1699178399).offset); 1339 | try testing.expectEqual(stdoff, result.offset(1699178400).offset); 1340 | 1341 | // IANA identifier: Asia/Jerusalem 1342 | result = try parsePosixTZ("IST-2IDT,M3.4.4/26,M10.5.0"); 1343 | stdoff = 7200; 1344 | dstoff = 10800; 1345 | try testing.expectEqualSlices(u8, "IST", result.std_designation); 1346 | try testing.expectEqual(stdoff, result.std_offset); 1347 | try testing.expectEqualSlices(u8, "IDT", result.dst_designation.?); 1348 | try testing.expectEqual(dstoff, result.dst_offset); 1349 | // transition std 2023-03-24T01:59:59+02:00 --> dst 2023-03-24T03:00:00+03:00 1350 | try testing.expectEqual(stdoff, result.offset(1679615999).offset); 1351 | try testing.expectEqual(dstoff, result.offset(1679616000).offset); 1352 | // transition dst 2023-10-29T01:59:59+03:00 --> std 2023-10-29T01:00:00+02:00 1353 | try testing.expectEqual(dstoff, result.offset(1698533999).offset); 1354 | try testing.expectEqual(stdoff, result.offset(1698534000).offset); 1355 | 1356 | // IANA identifier: America/Argentina/Buenos_Aires 1357 | result = try parsePosixTZ("WART4WARST,J1/0,J365/25"); // TODO : separate tests for jday ? 1358 | stdoff = -10800; 1359 | dstoff = -10800; 1360 | try testing.expectEqualSlices(u8, "WART", result.std_designation); 1361 | try testing.expectEqualSlices(u8, "WARST", result.dst_designation.?); 1362 | // transition std 2023-03-24T01:59:59-03:00 --> dst 2023-03-24T03:00:00-03:00 1363 | try testing.expectEqual(stdoff, result.offset(1679633999).offset); 1364 | try testing.expectEqual(dstoff, result.offset(1679637600).offset); 1365 | // transition dst 2023-10-29T01:59:59-03:00 --> std 2023-10-29T01:00:00-03:00 1366 | try testing.expectEqual(dstoff, result.offset(1698555599).offset); 1367 | try testing.expectEqual(stdoff, result.offset(1698552000).offset); 1368 | 1369 | // IANA identifier: America/Nuuk 1370 | result = try parsePosixTZ("WGT3WGST,M3.5.0/-2,M10.5.0/-1"); 1371 | stdoff = -10800; 1372 | dstoff = -7200; 1373 | try testing.expectEqualSlices(u8, "WGT", result.std_designation); 1374 | try testing.expectEqual(stdoff, result.std_offset); 1375 | try testing.expectEqualSlices(u8, "WGST", result.dst_designation.?); 1376 | try testing.expectEqual(dstoff, result.dst_offset); 1377 | // transition std 2021-03-27T21:59:59-03:00 --> dst 2021-03-27T23:00:00-02:00 1378 | try testing.expectEqual(stdoff, result.offset(1616893199).offset); 1379 | try testing.expectEqual(dstoff, result.offset(1616893200).offset); 1380 | // transition dst 2021-10-30T22:59:59-02:00 --> std 2021-10-30T22:00:00-03:00 1381 | try testing.expectEqual(dstoff, result.offset(1635641999).offset); 1382 | try testing.expectEqual(stdoff, result.offset(1635642000).offset); 1383 | } 1384 | 1385 | test "posix TZ string, leap year, America/New_York, start transition time specified" { 1386 | // IANA identifier: America/New_York 1387 | const result = try parsePosixTZ("EST5EDT,M3.2.0/02:00:00,M11.1.0"); 1388 | const stdoff: i32 = -18000; 1389 | const dstoff: i32 = -14400; 1390 | try testing.expectEqualSlices(u8, "EST", result.std_designation); 1391 | try testing.expectEqual(stdoff, result.std_offset); 1392 | try testing.expectEqualSlices(u8, "EDT", result.dst_designation.?); 1393 | try testing.expectEqual(dstoff, result.dst_offset); 1394 | // transition std 2020-03-08T01:59:59-05:00 --> dst 2020-03-08T03:00:00-04:00 1395 | try testing.expectEqual(stdoff, result.offset(1583650799).offset); 1396 | try testing.expectEqual(dstoff, result.offset(1583650800).offset); 1397 | // transition dst 2020-11-01T01:59:59-04:00 --> std 2020-11-01T01:00:00-05:00 1398 | try testing.expectEqual(dstoff, result.offset(1604210399).offset); 1399 | try testing.expectEqual(stdoff, result.offset(1604210400).offset); 1400 | } 1401 | 1402 | test "posix TZ string, leap year, America/New_York, both transition times specified" { 1403 | // IANA identifier: America/New_York 1404 | const result = try parsePosixTZ("EST5EDT,M3.2.0/02:00:00,M11.1.0/02:00:00"); 1405 | const stdoff: i32 = -18000; 1406 | const dstoff: i32 = -14400; 1407 | try testing.expectEqualSlices(u8, "EST", result.std_designation); 1408 | try testing.expectEqual(stdoff, result.std_offset); 1409 | try testing.expectEqualSlices(u8, "EDT", result.dst_designation.?); 1410 | try testing.expectEqual(dstoff, result.dst_offset); 1411 | // transition std 2020-03-08T01:59:59-05:00 --> dst 2020-03-08T03:00:00-04:00 1412 | try testing.expectEqual(stdoff, result.offset(1583650799).offset); 1413 | try testing.expectEqual(dstoff, result.offset(1583650800).offset); 1414 | // transtion dst 2020-11-01T01:59:59-04:00 --> std 2020-11-01T01:00:00-05:00 1415 | try testing.expectEqual(dstoff, result.offset(1604210399).offset); 1416 | try testing.expectEqual(stdoff, result.offset(1604210400).offset); 1417 | } 1418 | 1419 | test "posix TZ string, leap year, America/New_York, end transition time specified" { 1420 | // IANA identifier: America/New_York 1421 | const result = try parsePosixTZ("EST5EDT,M3.2.0,M11.1.0/02:00:00"); 1422 | const stdoff: i32 = -18000; 1423 | const dstoff: i32 = -14400; 1424 | try testing.expectEqualSlices(u8, "EST", result.std_designation); 1425 | try testing.expectEqual(stdoff, result.std_offset); 1426 | try testing.expectEqualSlices(u8, "EDT", result.dst_designation.?); 1427 | try testing.expectEqual(dstoff, result.dst_offset); 1428 | // transition std 2020-03-08T01:59:59-05:00 --> dst 2020-03-08T03:00:00-04:00 1429 | try testing.expectEqual(stdoff, result.offset(1583650799).offset); 1430 | try testing.expectEqual(dstoff, result.offset(1583650800).offset); 1431 | // transtion dst 2020-11-01T01:59:59-04:00 --> std 2020-11-01T01:00:00-05:00 1432 | try testing.expectEqual(dstoff, result.offset(1604210399).offset); 1433 | try testing.expectEqual(stdoff, result.offset(1604210400).offset); 1434 | } 1435 | 1436 | test "posix TZ string, leap year, America/Chicago, both transition times specified" { 1437 | // IANA identifier: America/Chicago 1438 | const result = try parsePosixTZ("CST6CDT,M3.2.0/2:00:00,M11.1.0/2:00:00"); 1439 | const stdoff: i32 = -21600; 1440 | const dstoff: i32 = -18000; 1441 | try testing.expectEqualSlices(u8, "CST", result.std_designation); 1442 | try testing.expectEqual(stdoff, result.std_offset); 1443 | try testing.expectEqualSlices(u8, "CDT", result.dst_designation.?); 1444 | try testing.expectEqual(dstoff, result.dst_offset); 1445 | // transition std 2020-03-08T01:59:59-06:00 --> dst 2020-03-08T03:00:00-05:00 1446 | try testing.expectEqual(stdoff, result.offset(1583654399).offset); 1447 | try testing.expectEqual(dstoff, result.offset(1583654400).offset); 1448 | // transtion dst 2020-11-01T01:59:59-05:00 --> std 2020-11-01T01:00:00-06:00 1449 | try testing.expectEqual(dstoff, result.offset(1604213999).offset); 1450 | try testing.expectEqual(stdoff, result.offset(1604214000).offset); 1451 | } 1452 | 1453 | test "posix TZ string, leap year, America/Denver, both transition times specified" { 1454 | // IANA identifier: America/Denver 1455 | const result = try parsePosixTZ("MST7MDT,M3.2.0/2:00:00,M11.1.0/2:00:00"); 1456 | const stdoff: i32 = -25200; 1457 | const dstoff: i32 = -21600; 1458 | try testing.expectEqualSlices(u8, "MST", result.std_designation); 1459 | try testing.expectEqual(stdoff, result.std_offset); 1460 | try testing.expectEqualSlices(u8, "MDT", result.dst_designation.?); 1461 | try testing.expectEqual(dstoff, result.dst_offset); 1462 | // transition std 2020-03-08T01:59:59-07:00 --> dst 2020-03-08T03:00:00-06:00 1463 | try testing.expectEqual(stdoff, result.offset(1583657999).offset); 1464 | try testing.expectEqual(dstoff, result.offset(1583658000).offset); 1465 | // transtion dst 2020-11-01T01:59:59-06:00 --> std 2020-11-01T01:00:00-07:00 1466 | try testing.expectEqual(dstoff, result.offset(1604217599).offset); 1467 | try testing.expectEqual(stdoff, result.offset(1604217600).offset); 1468 | } 1469 | 1470 | test "posix TZ string, leap year, America/Los_Angeles, both transition times specified" { 1471 | // IANA identifier: America/Los_Angeles 1472 | const result = try parsePosixTZ("PST8PDT,M3.2.0/2:00:00,M11.1.0/2:00:00"); 1473 | const stdoff: i32 = -28800; 1474 | const dstoff: i32 = -25200; 1475 | try testing.expectEqualSlices(u8, "PST", result.std_designation); 1476 | try testing.expectEqual(stdoff, result.std_offset); 1477 | try testing.expectEqualSlices(u8, "PDT", result.dst_designation.?); 1478 | try testing.expectEqual(dstoff, result.dst_offset); 1479 | // transition std 2020-03-08T01:59:59-08:00 --> dst 2020-03-08T03:00:00-07:00 1480 | try testing.expectEqual(stdoff, result.offset(1583661599).offset); 1481 | try testing.expectEqual(dstoff, result.offset(1583661600).offset); 1482 | // transtion dst 2020-11-01T01:59:59-07:00 --> std 2020-11-01T01:00:00-08:00 1483 | try testing.expectEqual(dstoff, result.offset(1604221199).offset); 1484 | try testing.expectEqual(stdoff, result.offset(1604221200).offset); 1485 | } 1486 | 1487 | test "posix TZ string, leap year, America/Sitka" { 1488 | // IANA identifier: America/Sitka 1489 | const result = try parsePosixTZ("AKST9AKDT,M3.2.0,M11.1.0"); 1490 | const stdoff: i32 = -32400; 1491 | const dstoff: i32 = -28800; 1492 | try testing.expectEqualSlices(u8, "AKST", result.std_designation); 1493 | try testing.expectEqual(stdoff, result.std_offset); 1494 | try testing.expectEqualSlices(u8, "AKDT", result.dst_designation.?); 1495 | try testing.expectEqual(dstoff, result.dst_offset); 1496 | // transition std 2020-03-08T01:59:59-09:00 --> dst 2020-03-08T03:00:00-08:00 1497 | try testing.expectEqual(stdoff, result.offset(1583665199).offset); 1498 | try testing.expectEqual(dstoff, result.offset(1583665200).offset); 1499 | // transtion dst 2020-11-01T01:59:59-08:00 --> std 2020-11-01T01:00:00-09:00 1500 | try testing.expectEqual(dstoff, result.offset(1604224799).offset); 1501 | try testing.expectEqual(stdoff, result.offset(1604224800).offset); 1502 | } 1503 | 1504 | test "posix TZ string, leap year, Asia/Jerusalem" { 1505 | // IANA identifier: Asia/Jerusalem 1506 | const result = try parsePosixTZ("IST-2IDT,M3.4.4/26,M10.5.0"); 1507 | const stdoff: i32 = 7200; 1508 | const dstoff: i32 = 10800; 1509 | try testing.expectEqualSlices(u8, "IST", result.std_designation); 1510 | try testing.expectEqual(stdoff, result.std_offset); 1511 | try testing.expectEqualSlices(u8, "IDT", result.dst_designation.?); 1512 | try testing.expectEqual(dstoff, result.dst_offset); 1513 | // transition std 2020-03-27T01:59:59+02:00 --> dst 2020-03-27T03:00:00+03:00 1514 | try testing.expectEqual(stdoff, result.offset(1585267199).offset); 1515 | try testing.expectEqual(dstoff, result.offset(1585267200).offset); 1516 | // transtion dst 2020-10-25T01:59:59+03:00 --> std 2020-10-25T01:00:00+02:00 1517 | try testing.expectEqual(dstoff, result.offset(1603580399).offset); 1518 | try testing.expectEqual(stdoff, result.offset(1603580400).offset); 1519 | } 1520 | 1521 | // Buenos Aires has DST all year long, make sure that it never returns the STD offset 1522 | test "posix TZ string, leap year, America/Argentina/Buenos_Aires" { 1523 | // IANA identifier: America/Argentina/Buenos_Aires 1524 | const result = try parsePosixTZ("WART4WARST,J1/0,J365/25"); 1525 | const stdoff: i32 = -4 * std.time.s_per_hour; 1526 | const dstoff: i32 = -3 * std.time.s_per_hour; 1527 | try testing.expectEqualSlices(u8, "WART", result.std_designation); 1528 | try testing.expectEqualSlices(u8, "WARST", result.dst_designation.?); 1529 | _ = stdoff; 1530 | 1531 | // transition std 2020-03-27T01:59:59-03:00 --> dst 2020-03-27T03:00:00-03:00 1532 | try testing.expectEqual(dstoff, result.offset(1585285199).offset); 1533 | try testing.expectEqual(dstoff, result.offset(1585288800).offset); 1534 | // transtion dst 2020-10-25T01:59:59-03:00 --> std 2020-10-25T01:00:00-03:00 1535 | try testing.expectEqual(dstoff, result.offset(1603601999).offset); 1536 | try testing.expectEqual(dstoff, result.offset(1603598400).offset); 1537 | 1538 | // Make sure it returns dstoff at the start of the year 1539 | try testing.expectEqual(dstoff, result.offset(1577836800).offset); // 2020 1540 | try testing.expectEqual(dstoff, result.offset(1609459200).offset); // 2021 1541 | 1542 | // Make sure it returns dstoff at the end of the year 1543 | try testing.expectEqual(dstoff, result.offset(1609459199).offset); 1544 | } 1545 | 1546 | test "posix TZ string, leap year, America/Nuuk" { 1547 | // IANA identifier: America/Nuuk 1548 | const result = try parsePosixTZ("WGT3WGST,M3.5.0/-2,M10.5.0/-1"); 1549 | const stdoff: i32 = -10800; 1550 | const dstoff: i32 = -7200; 1551 | try testing.expectEqualSlices(u8, "WGT", result.std_designation); 1552 | try testing.expectEqual(stdoff, result.std_offset); 1553 | try testing.expectEqualSlices(u8, "WGST", result.dst_designation.?); 1554 | try testing.expectEqual(dstoff, result.dst_offset); 1555 | // transition std 2020-03-28T21:59:59-03:00 --> dst 2020-03-28T23:00:00-02:00 1556 | try testing.expectEqual(stdoff, result.offset(1585443599).offset); 1557 | try testing.expectEqual(dstoff, result.offset(1585443600).offset); 1558 | // transtion dst 2020-10-24T22:59:59-02:00 --> std 2020-10-24T22:00:00-03:00 1559 | try testing.expectEqual(dstoff, result.offset(1603587599).offset); 1560 | try testing.expectEqual(stdoff, result.offset(1603587600).offset); 1561 | } 1562 | 1563 | test "posix TZ, valid strings" { 1564 | // from CPython's zoneinfo tests; 1565 | // https://github.com/python/cpython/blob/main/Lib/test/test_zoneinfo/test_zoneinfo.py 1566 | const tzstrs = [_][]const u8{ 1567 | // Extreme offset hour 1568 | "AAA24", 1569 | "AAA+24", 1570 | "AAA-24", 1571 | "AAA24BBB,J60/2,J300/2", 1572 | "AAA+24BBB,J60/2,J300/2", 1573 | "AAA-24BBB,J60/2,J300/2", 1574 | "AAA4BBB24,J60/2,J300/2", 1575 | "AAA4BBB+24,J60/2,J300/2", 1576 | "AAA4BBB-24,J60/2,J300/2", 1577 | // Extreme offset minutes 1578 | "AAA4:00BBB,J60/2,J300/2", 1579 | "AAA4:59BBB,J60/2,J300/2", 1580 | "AAA4BBB5:00,J60/2,J300/2", 1581 | "AAA4BBB5:59,J60/2,J300/2", 1582 | // Extreme offset seconds 1583 | "AAA4:00:00BBB,J60/2,J300/2", 1584 | "AAA4:00:59BBB,J60/2,J300/2", 1585 | "AAA4BBB5:00:00,J60/2,J300/2", 1586 | "AAA4BBB5:00:59,J60/2,J300/2", 1587 | // Extreme total offset 1588 | "AAA24:59:59BBB5,J60/2,J300/2", 1589 | "AAA-24:59:59BBB5,J60/2,J300/2", 1590 | "AAA4BBB24:59:59,J60/2,J300/2", 1591 | "AAA4BBB-24:59:59,J60/2,J300/2", 1592 | // Extreme months 1593 | "AAA4BBB,M12.1.1/2,M1.1.1/2", 1594 | "AAA4BBB,M1.1.1/2,M12.1.1/2", 1595 | // Extreme weeks 1596 | "AAA4BBB,M1.5.1/2,M1.1.1/2", 1597 | "AAA4BBB,M1.1.1/2,M1.5.1/2", 1598 | // Extreme weekday 1599 | "AAA4BBB,M1.1.6/2,M2.1.1/2", 1600 | "AAA4BBB,M1.1.1/2,M2.1.6/2", 1601 | // Extreme numeric offset 1602 | "AAA4BBB,0/2,20/2", 1603 | "AAA4BBB,0/2,0/14", 1604 | "AAA4BBB,20/2,365/2", 1605 | "AAA4BBB,365/2,365/14", 1606 | // Extreme julian offset 1607 | "AAA4BBB,J1/2,J20/2", 1608 | "AAA4BBB,J1/2,J1/14", 1609 | "AAA4BBB,J20/2,J365/2", 1610 | "AAA4BBB,J365/2,J365/14", 1611 | // Extreme transition hour 1612 | "AAA4BBB,J60/167,J300/2", 1613 | "AAA4BBB,J60/+167,J300/2", 1614 | "AAA4BBB,J60/-167,J300/2", 1615 | "AAA4BBB,J60/2,J300/167", 1616 | "AAA4BBB,J60/2,J300/+167", 1617 | "AAA4BBB,J60/2,J300/-167", 1618 | // Extreme transition minutes 1619 | "AAA4BBB,J60/2:00,J300/2", 1620 | "AAA4BBB,J60/2:59,J300/2", 1621 | "AAA4BBB,J60/2,J300/2:00", 1622 | "AAA4BBB,J60/2,J300/2:59", 1623 | // Extreme transition seconds 1624 | "AAA4BBB,J60/2:00:00,J300/2", 1625 | "AAA4BBB,J60/2:00:59,J300/2", 1626 | "AAA4BBB,J60/2,J300/2:00:00", 1627 | "AAA4BBB,J60/2,J300/2:00:59", 1628 | // Extreme total transition time 1629 | "AAA4BBB,J60/167:59:59,J300/2", 1630 | "AAA4BBB,J60/-167:59:59,J300/2", 1631 | "AAA4BBB,J60/2,J300/167:59:59", 1632 | "AAA4BBB,J60/2,J300/-167:59:59", 1633 | }; 1634 | for (tzstrs) |valid_str| { 1635 | _ = try parsePosixTZ(valid_str); 1636 | } 1637 | } 1638 | 1639 | // The following tests are from CPython's zoneinfo tests; 1640 | // https://github.com/python/cpython/blob/main/Lib/test/test_zoneinfo/test_zoneinfo.py 1641 | test "posix TZ invalid string, unquoted alphanumeric" { 1642 | try std.testing.expectError(error.InvalidFormat, parsePosixTZ("+11")); 1643 | } 1644 | 1645 | test "posix TZ invalid string, unquoted alphanumeric in DST" { 1646 | try std.testing.expectError(error.InvalidFormat, parsePosixTZ("GMT0+11,M3.2.0/2,M11.1.0/3")); 1647 | } 1648 | 1649 | test "posix TZ invalid string, DST but no transition specified" { 1650 | try std.testing.expectError(error.InvalidFormat, parsePosixTZ("PST8PDT")); 1651 | } 1652 | 1653 | test "posix TZ invalid string, only one transition rule" { 1654 | try std.testing.expectError(error.InvalidFormat, parsePosixTZ("PST8PDT,M3.2.0/2")); 1655 | } 1656 | 1657 | test "posix TZ invalid string, transition rule but no DST" { 1658 | try std.testing.expectError(error.InvalidFormat, parsePosixTZ("GMT,M3.2.0/2,M11.1.0/3")); 1659 | } 1660 | 1661 | test "posix TZ invalid offset hours" { 1662 | try std.testing.expectError(error.InvalidFormat, parsePosixTZ("AAA168")); 1663 | try std.testing.expectError(error.InvalidFormat, parsePosixTZ("AAA+168")); 1664 | try std.testing.expectError(error.InvalidFormat, parsePosixTZ("AAA-168")); 1665 | try std.testing.expectError(error.InvalidFormat, parsePosixTZ("AAA168BBB,J60/2,J300/2")); 1666 | try std.testing.expectError(error.InvalidFormat, parsePosixTZ("AAA+168BBB,J60/2,J300/2")); 1667 | try std.testing.expectError(error.InvalidFormat, parsePosixTZ("AAA-168BBB,J60/2,J300/2")); 1668 | try std.testing.expectError(error.InvalidFormat, parsePosixTZ("AAA4BBB168,J60/2,J300/2")); 1669 | try std.testing.expectError(error.InvalidFormat, parsePosixTZ("AAA4BBB+168,J60/2,J300/2")); 1670 | try std.testing.expectError(error.InvalidFormat, parsePosixTZ("AAA4BBB-168,J60/2,J300/2")); 1671 | } 1672 | 1673 | test "posix TZ invalid offset minutes" { 1674 | try std.testing.expectError(error.InvalidFormat, parsePosixTZ("AAA4:0BBB,J60/2,J300/2")); 1675 | try std.testing.expectError(error.InvalidFormat, parsePosixTZ("AAA4:100BBB,J60/2,J300/2")); 1676 | try std.testing.expectError(error.InvalidFormat, parsePosixTZ("AAA4BBB5:0,J60/2,J300/2")); 1677 | try std.testing.expectError(error.InvalidFormat, parsePosixTZ("AAA4BBB5:100,J60/2,J300/2")); 1678 | } 1679 | 1680 | test "posix TZ invalid offset seconds" { 1681 | try std.testing.expectError(error.InvalidFormat, parsePosixTZ("AAA4:00:0BBB,J60/2,J300/2")); 1682 | try std.testing.expectError(error.InvalidFormat, parsePosixTZ("AAA4:00:100BBB,J60/2,J300/2")); 1683 | try std.testing.expectError(error.InvalidFormat, parsePosixTZ("AAA4BBB5:00:0,J60/2,J300/2")); 1684 | try std.testing.expectError(error.InvalidFormat, parsePosixTZ("AAA4BBB5:00:100,J60/2,J300/2")); 1685 | } 1686 | 1687 | test "posix TZ completely invalid dates" { 1688 | try std.testing.expectError(error.InvalidFormat, parsePosixTZ("AAA4BBB,M1443339,M11.1.0/3")); 1689 | try std.testing.expectError(error.InvalidFormat, parsePosixTZ("AAA4BBB,M3.2.0/2,0349309483959c")); 1690 | try std.testing.expectError(error.InvalidFormat, parsePosixTZ("AAA4BBB,,J300/2")); 1691 | try std.testing.expectError(error.InvalidFormat, parsePosixTZ("AAA4BBB,z,J300/2")); 1692 | try std.testing.expectError(error.InvalidFormat, parsePosixTZ("AAA4BBB,J60/2,")); 1693 | try std.testing.expectError(error.InvalidFormat, parsePosixTZ("AAA4BBB,J60/2,z")); 1694 | } 1695 | 1696 | test "posix TZ invalid months" { 1697 | try std.testing.expectError(error.InvalidFormat, parsePosixTZ("AAA4BBB,M13.1.1/2,M1.1.1/2")); 1698 | try std.testing.expectError(error.InvalidFormat, parsePosixTZ("AAA4BBB,M1.1.1/2,M13.1.1/2")); 1699 | try std.testing.expectError(error.InvalidFormat, parsePosixTZ("AAA4BBB,M0.1.1/2,M1.1.1/2")); 1700 | try std.testing.expectError(error.InvalidFormat, parsePosixTZ("AAA4BBB,M1.1.1/2,M0.1.1/2")); 1701 | } 1702 | 1703 | test "posix TZ invalid weeks" { 1704 | try std.testing.expectError(error.InvalidFormat, parsePosixTZ("AAA4BBB,M1.6.1/2,M1.1.1/2")); 1705 | try std.testing.expectError(error.InvalidFormat, parsePosixTZ("AAA4BBB,M1.1.1/2,M1.6.1/2")); 1706 | } 1707 | 1708 | test "posix TZ invalid weekday" { 1709 | try std.testing.expectError(error.InvalidFormat, parsePosixTZ("AAA4BBB,M1.1.7/2,M2.1.1/2")); 1710 | try std.testing.expectError(error.InvalidFormat, parsePosixTZ("AAA4BBB,M1.1.1/2,M2.1.7/2")); 1711 | } 1712 | 1713 | test "posix TZ invalid numeric offset" { 1714 | try std.testing.expectError(error.InvalidFormat, parsePosixTZ("AAA4BBB,-1/2,20/2")); 1715 | try std.testing.expectError(error.InvalidFormat, parsePosixTZ("AAA4BBB,1/2,-1/2")); 1716 | try std.testing.expectError(error.InvalidFormat, parsePosixTZ("AAA4BBB,367,20/2")); 1717 | try std.testing.expectError(error.InvalidFormat, parsePosixTZ("AAA4BBB,1/2,367/2")); 1718 | } 1719 | 1720 | test "posix TZ invalid julian offset" { 1721 | try std.testing.expectError(error.InvalidFormat, parsePosixTZ("AAA4BBB,J0/2,J20/2")); 1722 | try std.testing.expectError(error.InvalidFormat, parsePosixTZ("AAA4BBB,J20/2,J366/2")); 1723 | } 1724 | 1725 | test "posix TZ invalid transition time" { 1726 | try std.testing.expectError(error.InvalidFormat, parsePosixTZ("AAA4BBB,J60/2/3,J300/2")); 1727 | try std.testing.expectError(error.InvalidFormat, parsePosixTZ("AAA4BBB,J60/2,J300/2/3")); 1728 | } 1729 | 1730 | test "posix TZ invalid transition hour" { 1731 | try std.testing.expectError(error.InvalidFormat, parsePosixTZ("AAA4BBB,J60/168,J300/2")); 1732 | try std.testing.expectError(error.InvalidFormat, parsePosixTZ("AAA4BBB,J60/+168,J300/2")); 1733 | try std.testing.expectError(error.InvalidFormat, parsePosixTZ("AAA4BBB,J60/-168,J300/2")); 1734 | try std.testing.expectError(error.InvalidFormat, parsePosixTZ("AAA4BBB,J60/2,J300/168")); 1735 | try std.testing.expectError(error.InvalidFormat, parsePosixTZ("AAA4BBB,J60/2,J300/+168")); 1736 | try std.testing.expectError(error.InvalidFormat, parsePosixTZ("AAA4BBB,J60/2,J300/-168")); 1737 | } 1738 | 1739 | test "posix TZ invalid transition minutes" { 1740 | try std.testing.expectError(error.InvalidFormat, parsePosixTZ("AAA4BBB,J60/2:0,J300/2")); 1741 | try std.testing.expectError(error.InvalidFormat, parsePosixTZ("AAA4BBB,J60/2:100,J300/2")); 1742 | try std.testing.expectError(error.InvalidFormat, parsePosixTZ("AAA4BBB,J60/2,J300/2:0")); 1743 | try std.testing.expectError(error.InvalidFormat, parsePosixTZ("AAA4BBB,J60/2,J300/2:100")); 1744 | } 1745 | 1746 | test "posix TZ invalid transition seconds" { 1747 | try std.testing.expectError(error.InvalidFormat, parsePosixTZ("AAA4BBB,J60/2:00:0,J300/2")); 1748 | try std.testing.expectError(error.InvalidFormat, parsePosixTZ("AAA4BBB,J60/2:00:100,J300/2")); 1749 | try std.testing.expectError(error.InvalidFormat, parsePosixTZ("AAA4BBB,J60/2,J300/2:00:0")); 1750 | try std.testing.expectError(error.InvalidFormat, parsePosixTZ("AAA4BBB,J60/2,J300/2:00:100")); 1751 | } 1752 | 1753 | test "posix TZ EST5EDT,M3.2.0/4:00,M11.1.0/3:00 from zoneinfo_test.py" { 1754 | // Transition to EDT on the 2nd Sunday in March at 4 AM, and 1755 | // transition back on the first Sunday in November at 3AM 1756 | const result = try parsePosixTZ("EST5EDT,M3.2.0/4:00,M11.1.0/3:00"); 1757 | try testing.expectEqual(@as(i32, -18000), result.offset(1552107600).offset); // 2019-03-09T00:00:00-05:00 1758 | try testing.expectEqual(@as(i32, -18000), result.offset(1552208340).offset); // 2019-03-10T03:59:00-05:00 1759 | try testing.expectEqual(@as(i32, -14400), result.offset(1572667200).offset); // 2019-11-02T00:00:00-04:00 1760 | try testing.expectEqual(@as(i32, -14400), result.offset(1572760740).offset); // 2019-11-03T01:59:00-04:00 1761 | try testing.expectEqual(@as(i32, -14400), result.offset(1572760800).offset); // 2019-11-03T02:00:00-04:00 1762 | try testing.expectEqual(@as(i32, -18000), result.offset(1572764400).offset); // 2019-11-03T02:00:00-05:00 1763 | try testing.expectEqual(@as(i32, -18000), result.offset(1583657940).offset); // 2020-03-08T03:59:00-05:00 1764 | try testing.expectEqual(@as(i32, -14400), result.offset(1604210340).offset); // 2020-11-01T01:59:00-04:00 1765 | try testing.expectEqual(@as(i32, -14400), result.offset(1604210400).offset); // 2020-11-01T02:00:00-04:00 1766 | try testing.expectEqual(@as(i32, -18000), result.offset(1604214000).offset); // 2020-11-01T02:00:00-05:00 1767 | } 1768 | 1769 | test "posix TZ GMT0BST-1,M3.5.0/1:00,M10.5.0/2:00 from zoneinfo_test.py" { 1770 | // Transition to BST happens on the last Sunday in March at 1 AM GMT 1771 | // and the transition back happens the last Sunday in October at 2AM BST 1772 | const result = try parsePosixTZ("GMT0BST-1,M3.5.0/1:00,M10.5.0/2:00"); 1773 | try testing.expectEqual(@as(i32, 0), result.offset(1553904000).offset); // 2019-03-30T00:00:00+00:00 1774 | try testing.expectEqual(@as(i32, 0), result.offset(1553993940).offset); // 2019-03-31T00:59:00+00:00 1775 | try testing.expectEqual(@as(i32, 3600), result.offset(1553994000).offset); // 2019-03-31T02:00:00+01:00 1776 | try testing.expectEqual(@as(i32, 3600), result.offset(1572044400).offset); // 2019-10-26T00:00:00+01:00 1777 | try testing.expectEqual(@as(i32, 3600), result.offset(1572134340).offset); // 2019-10-27T00:59:00+01:00 1778 | try testing.expectEqual(@as(i32, 0), result.offset(1585443540).offset); // 2020-03-29T00:59:00+00:00 1779 | try testing.expectEqual(@as(i32, 3600), result.offset(1585443600).offset); // 2020-03-29T02:00:00+01:00 1780 | try testing.expectEqual(@as(i32, 3600), result.offset(1603583940).offset); // 2020-10-25T00:59:00+01:00 1781 | try testing.expectEqual(@as(i32, 3600), result.offset(1603584000).offset); // 2020-10-25T01:00:00+01:00 1782 | try testing.expectEqual(@as(i32, 0), result.offset(1603591200).offset); // 2020-10-25T02:00:00+00:00 1783 | } 1784 | 1785 | test "posix TZ AEST-10AEDT,M10.1.0/2,M4.1.0/3 from zoneinfo_test.py" { 1786 | // Austrialian time zone - DST start is chronologically first 1787 | const result = try parsePosixTZ("AEST-10AEDT,M10.1.0/2,M4.1.0/3"); 1788 | try testing.expectEqual(@as(i32, 39600), result.offset(1554469200).offset); // 2019-04-06T00:00:00+11:00 1789 | try testing.expectEqual(@as(i32, 39600), result.offset(1554562740).offset); // 2019-04-07T01:59:00+11:00 1790 | try testing.expectEqual(@as(i32, 39600), result.offset(1554562740).offset); // 2019-04-07T01:59:00+11:00 1791 | try testing.expectEqual(@as(i32, 39600), result.offset(1554562800).offset); // 2019-04-07T02:00:00+11:00 1792 | try testing.expectEqual(@as(i32, 39600), result.offset(1554562860).offset); // 2019-04-07T02:01:00+11:00 1793 | try testing.expectEqual(@as(i32, 36000), result.offset(1554566400).offset); // 2019-04-07T02:00:00+10:00 1794 | try testing.expectEqual(@as(i32, 36000), result.offset(1554566460).offset); // 2019-04-07T02:01:00+10:00 1795 | try testing.expectEqual(@as(i32, 36000), result.offset(1554570000).offset); // 2019-04-07T03:00:00+10:00 1796 | try testing.expectEqual(@as(i32, 36000), result.offset(1554570000).offset); // 2019-04-07T03:00:00+10:00 1797 | try testing.expectEqual(@as(i32, 36000), result.offset(1570197600).offset); // 2019-10-05T00:00:00+10:00 1798 | try testing.expectEqual(@as(i32, 36000), result.offset(1570291140).offset); // 2019-10-06T01:59:00+10:00 1799 | try testing.expectEqual(@as(i32, 39600), result.offset(1570291200).offset); // 2019-10-06T03:00:00+11:00 1800 | } 1801 | 1802 | test "posix TZ IST-1GMT0,M10.5.0,M3.5.0/1 from zoneinfo_test.py" { 1803 | // Irish time zone - negative DST 1804 | const result = try parsePosixTZ("IST-1GMT0,M10.5.0,M3.5.0/1"); 1805 | try testing.expectEqual(@as(i32, 0), result.offset(1553904000).offset); // 2019-03-30T00:00:00+00:00 1806 | try testing.expectEqual(@as(i32, 0), result.offset(1553993940).offset); // 2019-03-31T00:59:00+00:00 1807 | try testing.expectEqual(true, result.offset(1553993940).is_daylight_saving_time); // 2019-03-31T00:59:00+00:00 1808 | try testing.expectEqual(@as(i32, 3600), result.offset(1553994000).offset); // 2019-03-31T02:00:00+01:00 1809 | try testing.expectEqual(false, result.offset(1553994000).is_daylight_saving_time); // 2019-03-31T02:00:00+01:00 1810 | try testing.expectEqual(@as(i32, 3600), result.offset(1572044400).offset); // 2019-10-26T00:00:00+01:00 1811 | try testing.expectEqual(@as(i32, 3600), result.offset(1572134340).offset); // 2019-10-27T00:59:00+01:00 1812 | try testing.expectEqual(@as(i32, 3600), result.offset(1572134400).offset); // 2019-10-27T01:00:00+01:00 1813 | try testing.expectEqual(@as(i32, 0), result.offset(1572138000).offset); // 2019-10-27T01:00:00+00:00 1814 | try testing.expectEqual(@as(i32, 0), result.offset(1572141600).offset); // 2019-10-27T02:00:00+00:00 1815 | try testing.expectEqual(@as(i32, 0), result.offset(1585443540).offset); // 2020-03-29T00:59:00+00:00 1816 | try testing.expectEqual(@as(i32, 3600), result.offset(1585443600).offset); // 2020-03-29T02:00:00+01:00 1817 | try testing.expectEqual(@as(i32, 3600), result.offset(1603583940).offset); // 2020-10-25T00:59:00+01:00 1818 | try testing.expectEqual(@as(i32, 3600), result.offset(1603584000).offset); // 2020-10-25T01:00:00+01:00 1819 | try testing.expectEqual(@as(i32, 0), result.offset(1603591200).offset); // 2020-10-25T02:00:00+00:00 1820 | } 1821 | 1822 | test "posix TZ <+11>-11 from zoneinfo_test.py" { 1823 | // Pacific/Kosrae: Fixed offset zone with a quoted numerical tzname 1824 | const result = try parsePosixTZ("<+11>-11"); 1825 | try testing.expectEqual(@as(i32, 39600), result.offset(1577797200).offset); // 2020-01-01T00:00:00+11:00 1826 | } 1827 | 1828 | test "posix TZ <-04>4<-03>,M9.1.6/24,M4.1.6/24 from zoneinfo_test.py" { 1829 | // Quoted STD and DST, transitions at 24:00 1830 | const result = try parsePosixTZ("<-04>4<-03>,M9.1.6/24,M4.1.6/24"); 1831 | try testing.expectEqual(@as(i32, -14400), result.offset(1588305600).offset); // 2020-05-01T00:00:00-04:00 1832 | try testing.expectEqual(@as(i32, -10800), result.offset(1604199600).offset); // 2020-11-01T00:00:00-03:00 1833 | } 1834 | 1835 | test "posix TZ EST5EDT,0/0,J365/25 from zoneinfo_test.py" { 1836 | // Permanent daylight saving time is modeled with transitions at 0/0 1837 | // and J365/25, as mentioned in RFC 8536 Section 3.3.1 1838 | const result = try parsePosixTZ("EST5EDT,0/0,J365/25"); 1839 | try testing.expectEqual(@as(i32, -14400), result.offset(1546315200).offset); // 2019-01-01T00:00:00-04:00 1840 | try testing.expectEqual(@as(i32, -14400), result.offset(1559361600).offset); // 2019-06-01T00:00:00-04:00 1841 | try testing.expectEqual(@as(i32, -14400), result.offset(1577851199).offset); // 2019-12-31T23:59:59.999999-04:00 1842 | try testing.expectEqual(@as(i32, -14400), result.offset(1577851200).offset); // 2020-01-01T00:00:00-04:00 1843 | try testing.expectEqual(@as(i32, -14400), result.offset(1583035200).offset); // 2020-03-01T00:00:00-04:00 1844 | try testing.expectEqual(@as(i32, -14400), result.offset(1590984000).offset); // 2020-06-01T00:00:00-04:00 1845 | try testing.expectEqual(@as(i32, -14400), result.offset(1609473599).offset); // 2020-12-31T23:59:59.999999-04:00 1846 | try testing.expectEqual(@as(i32, -14400), result.offset(13569480000).offset); // 2400-01-01T00:00:00-04:00 1847 | try testing.expectEqual(@as(i32, -14400), result.offset(13574664000).offset); // 2400-03-01T00:00:00-04:00 1848 | try testing.expectEqual(@as(i32, -14400), result.offset(13601102399).offset); // 2400-12-31T23:59:59.999999-04:00 1849 | } 1850 | 1851 | test "posix TZ AAA3BBB,J60/12,J305/12 from zoneinfo_test.py" { 1852 | // Transitions on March 1st and November 1st of each year 1853 | const result = try parsePosixTZ("AAA3BBB,J60/12,J305/12"); 1854 | try testing.expectEqual(@as(i32, -10800), result.offset(1546311600).offset); // 2019-01-01T00:00:00-03:00 1855 | try testing.expectEqual(@as(i32, -10800), result.offset(1551322800).offset); // 2019-02-28T00:00:00-03:00 1856 | try testing.expectEqual(@as(i32, -10800), result.offset(1551452340).offset); // 2019-03-01T11:59:00-03:00 1857 | try testing.expectEqual(@as(i32, -7200), result.offset(1551452400).offset); // 2019-03-01T13:00:00-02:00 1858 | try testing.expectEqual(@as(i32, -7200), result.offset(1572613140).offset); // 2019-11-01T10:59:00-02:00 1859 | try testing.expectEqual(@as(i32, -7200), result.offset(1572613200).offset); // 2019-11-01T11:00:00-02:00 1860 | try testing.expectEqual(@as(i32, -10800), result.offset(1572616800).offset); // 2019-11-01T11:00:00-03:00 1861 | try testing.expectEqual(@as(i32, -10800), result.offset(1572620400).offset); // 2019-11-01T12:00:00-03:00 1862 | try testing.expectEqual(@as(i32, -10800), result.offset(1577847599).offset); // 2019-12-31T23:59:59.999999-03:00 1863 | try testing.expectEqual(@as(i32, -10800), result.offset(1577847600).offset); // 2020-01-01T00:00:00-03:00 1864 | try testing.expectEqual(@as(i32, -10800), result.offset(1582945200).offset); // 2020-02-29T00:00:00-03:00 1865 | try testing.expectEqual(@as(i32, -10800), result.offset(1583074740).offset); // 2020-03-01T11:59:00-03:00 1866 | try testing.expectEqual(@as(i32, -7200), result.offset(1583074800).offset); // 2020-03-01T13:00:00-02:00 1867 | try testing.expectEqual(@as(i32, -7200), result.offset(1604235540).offset); // 2020-11-01T10:59:00-02:00 1868 | try testing.expectEqual(@as(i32, -7200), result.offset(1604235600).offset); // 2020-11-01T11:00:00-02:00 1869 | try testing.expectEqual(@as(i32, -10800), result.offset(1604239200).offset); // 2020-11-01T11:00:00-03:00 1870 | try testing.expectEqual(@as(i32, -10800), result.offset(1604242800).offset); // 2020-11-01T12:00:00-03:00 1871 | try testing.expectEqual(@as(i32, -10800), result.offset(1609469999).offset); // 2020-12-31T23:59:59.999999-03:00 1872 | } 1873 | 1874 | test "posix TZ <-03>3<-02>,M3.5.0/-2,M10.5.0/-1 from zoneinfo_test.py" { 1875 | // Taken from America/Godthab, this rule has a transition on the 1876 | // Saturday before the last Sunday of March and October, at 22:00 and 23:00, 1877 | // respectively. This is encoded with negative start and end transition times. 1878 | const result = try parsePosixTZ("<-03>3<-02>,M3.5.0/-2,M10.5.0/-1"); 1879 | try testing.expectEqual(@as(i32, -10800), result.offset(1585278000).offset); // 2020-03-27T00:00:00-03:00 1880 | try testing.expectEqual(@as(i32, -10800), result.offset(1585443599).offset); // 2020-03-28T21:59:59-03:00 1881 | try testing.expectEqual(@as(i32, -7200), result.offset(1585443600).offset); // 2020-03-28T23:00:00-02:00 1882 | try testing.expectEqual(@as(i32, -7200), result.offset(1603580400).offset); // 2020-10-24T21:00:00-02:00 1883 | try testing.expectEqual(@as(i32, -7200), result.offset(1603584000).offset); // 2020-10-24T22:00:00-02:00 1884 | try testing.expectEqual(@as(i32, -10800), result.offset(1603587600).offset); // 2020-10-24T22:00:00-03:00 1885 | try testing.expectEqual(@as(i32, -10800), result.offset(1603591200).offset); // 2020-10-24T23:00:00-03:00 1886 | } 1887 | 1888 | test "posix TZ AAA3BBB,M3.2.0/01:30,M11.1.0/02:15:45 from zoneinfo_test.py" { 1889 | // Transition times with minutes and seconds 1890 | const result = try parsePosixTZ("AAA3BBB,M3.2.0/01:30,M11.1.0/02:15:45"); 1891 | try testing.expectEqual(@as(i32, -10800), result.offset(1331438400).offset); // 2012-03-11T01:00:00-03:00 1892 | try testing.expectEqual(@as(i32, -7200), result.offset(1331440200).offset); // 2012-03-11T02:30:00-02:00 1893 | try testing.expectEqual(@as(i32, -7200), result.offset(1351998944).offset); // 2012-11-04T01:15:44.999999-02:00 1894 | try testing.expectEqual(@as(i32, -7200), result.offset(1351998945).offset); // 2012-11-04T01:15:45-02:00 1895 | try testing.expectEqual(@as(i32, -10800), result.offset(1352002545).offset); // 2012-11-04T01:15:45-03:00 1896 | try testing.expectEqual(@as(i32, -10800), result.offset(1352006145).offset); // 2012-11-04T02:15:45-03:00 1897 | } 1898 | -------------------------------------------------------------------------------- /zig.mod: -------------------------------------------------------------------------------- 1 | id: bk4igusnzp7cfi81fbhr5lzu26qcwvzyh1idcrieo5hzp873 2 | name: tzif 3 | main: tzif.zig 4 | license: MIT 5 | dependencies: 6 | -------------------------------------------------------------------------------- /zoneinfo/Pacific/Honolulu: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leroycep/zig-tzif/a9eb2349b7e9f4d576b195e21b3ef4555b930b09/zoneinfo/Pacific/Honolulu -------------------------------------------------------------------------------- /zoneinfo/UTC: -------------------------------------------------------------------------------- 1 | TZif2UTCTZif2UTC 2 | UTC0 3 | --------------------------------------------------------------------------------