Repository: rockorager/zeit
Branch: main
Commit: 0ad98ccc1563
Files: 20
Total size: 242.1 KB
Directory structure:
gitextract_55wlgbp2/
├── .github/
│ └── workflows/
│ ├── docs.yml
│ └── test.yml
├── .gitignore
├── LICENSE
├── README.md
├── bench/
│ ├── bench_benjoffe.zig
│ ├── bench_days_benjoffe.zig
│ ├── bench_days_current.zig
│ ├── bench_hinnant.zig
│ ├── bench_leap_benjoffe.zig
│ ├── bench_leap_current.zig
│ ├── test_days.zig
│ └── test_leap.zig
├── build.zig
├── build.zig.zon
├── gen/
│ ├── main.zig
│ └── windowsZones.xml
└── src/
├── location.zig
├── timezone.zig
└── zeit.zig
================================================
FILE CONTENTS
================================================
================================================
FILE: .github/workflows/docs.yml
================================================
name: docs
on:
push:
branches: ["main"]
workflow_dispatch:
permissions:
contents: read
pages: write
id-token: write
concurrency:
group: "pages"
cancel-in-progress: true
jobs:
deploy:
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Setup Pages
uses: actions/configure-pages@v2
- uses: mlugg/setup-zig@v2
with:
version: 0.15.2
- run: zig build docs
- name: Upload artifact
uses: actions/upload-pages-artifact@v3
with:
path: "zig-out/docs"
- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@v4
================================================
FILE: .github/workflows/test.yml
================================================
name: test
on:
pull_request:
branches: ["main", "0.16"]
workflow_dispatch:
jobs:
test:
strategy:
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
runs-on: ${{matrix.os}}
steps:
- uses: actions/checkout@v3
- uses: mlugg/setup-zig@v2
with:
version: master
- run: zig build test --summary new
check-fmt:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: mlugg/setup-zig@v2
with:
version: master
- run: zig fmt --check .
================================================
FILE: .gitignore
================================================
.zig-cache/
zig-out/
================================================
FILE: LICENSE
================================================
Copyright (c) 2024 Tim Culverhouse
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
================================================
FILE: README.md
================================================
# zeit
A time library written in zig.
## Install
```
zig fetch --save git+https://github.com/rockorager/zeit?ref=main
```
Or install a [tag](https://github.com/rockorager/zeit/tags) instead of main.
## Usage
[API Documentation](https://rockorager.github.io/zeit/)
```zig
const std = @import("std");
const zeit = @import("zeit");
pub fn main() !void {
const allocator = std.heap.page_allocator;
var threaded = std.Io.Threaded.init(allocator, .{});
defer threaded.deinit();
const io = threaded.io();
// Get a "now" instant in UTC.
const now = zeit.instant(.{.now = io}, &zeit.utc);
// Load our local timezone. This needs an allocator. Optionally pass in a
// zeit.EnvConfig to support TZ and TZDIR environment variables
const local = try zeit.local(allocator, io, .{});
defer local.deinit();
// Convert our instant to a new timezone
const now_local = now.in(&local);
// Generate date/time info for this instant
const dt = now_local.time();
// Print it out
std.debug.print("{}", .{dt});
// zeit.Time{
// .year = 2024,
// .month = zeit.Month.mar,
// .day = 16,
// .hour = 8,
// .minute = 38,
// .second = 29,
// .millisecond = 496,
// .microsecond = 706,
// .nanosecond = 64
// .offset = -18000,
// }
var buf: [256]u8 = undefined;
var writer = std.Io.Writer.fixed(&buf);
// Format using strftime specifier. Format strings are not required to be comptime
try dt.strftime(&writer, "%Y-%m-%d %H:%M:%S %Z");
std.debug.print("{s}\n", .{writer.buffered()});
writer.end = 0;
// Or...golang magic date specifiers. Format strings are not required to be comptime
try dt.gofmt(&writer, "2006-01-02 15:04:05 MST");
std.debug.print("{s}\n", .{writer.buffered()});
// Load an arbitrary location using IANA location syntax. The location name
// comes from an enum which will automatically map IANA location names to
// Windows names, as needed. Pass an optional EnvConfig to support TZDIR
const vienna = try zeit.loadTimeZone(allocator, io, .@"Europe/Vienna", .{});
defer vienna.deinit();
// Parse an Instant from an ISO8601 or RFC3339 string
_ = try zeit.instantFromText(
.iso8601,
"2024-03-16T08:38:29.496-1200",
&zeit.utc,
});
_ = try zeit.instantFromText(
.rfc3339,
"2024-03-16T08:38:29.496706064-1200",
&zeit.utc,
});
}
```
================================================
FILE: bench/bench_benjoffe.zig
================================================
const std = @import("std");
const builtin = @import("builtin");
const Date = struct {
year: i32,
month: u4,
day: u5,
};
const is_arm = builtin.cpu.arch == .aarch64 or builtin.cpu.arch == .arm;
const ERAS: u64 = 4726498270;
const D_SHIFT: u64 = 146097 * ERAS - 719469;
const Y_SHIFT: u64 = 400 * ERAS - 1;
const SCALE: u64 = if (is_arm) 1 else 32;
const SHIFT_0: u64 = 30556 * SCALE;
const SHIFT_1: u64 = 5980 * SCALE;
const C1: u64 = 505054698555331;
const C2: u64 = 50504432782230121;
const C3: u64 = @as(u64, 8619973866219416) * 32 / SCALE;
/// Ben Joffe's very fast 64-bit date algorithm
/// https://www.benjoffe.com/fast-date-64
fn civilFromDays(days: i32) Date {
const rev: u64 = D_SHIFT -% @as(u64, @bitCast(@as(i64, days)));
const cen: u64 = @truncate(@as(u128, C1) * rev >> 64);
const jul: u64 = rev +% cen -% cen / 4;
const num: u128 = @as(u128, C2) * jul;
const yrs: u64 = Y_SHIFT -% @as(u64, @truncate(num >> 64));
const low: u64 = @truncate(num);
const ypt: u64 = @truncate(@as(u128, 24451 * SCALE) * low >> 64);
if (is_arm) {
const shift: u64 = SHIFT_0;
const N: u64 = (yrs % 4) * (16 * SCALE) +% shift -% ypt;
const M: u64 = N / (2048 * SCALE);
const D: u64 = @truncate(@as(u128, C3) * (N % (2048 * SCALE)) >> 64);
const bump: u64 = if (M > 12) 1 else 0;
const month: u4 = @intCast(if (bump == 1) M - 12 else M);
const day: u5 = @intCast(D + 1);
const year: i32 = @intCast(@as(i64, @bitCast(yrs)) + @as(i64, @intCast(bump)));
return .{ .year = year, .month = month, .day = day };
} else {
const bump: u64 = if (ypt < (3952 * SCALE)) 1 else 0;
const shift: u64 = if (bump == 1) SHIFT_1 else SHIFT_0;
const N: u64 = (yrs % 4) * (16 * SCALE) +% shift -% ypt;
const M: u64 = N / (2048 * SCALE);
const D: u64 = @truncate(@as(u128, C3) * (N % (2048 * SCALE)) >> 64);
const month: u4 = @intCast(M);
const day: u5 = @intCast(D + 1);
const year: i32 = @intCast(@as(i64, @bitCast(yrs)) + @as(i64, @intCast(bump)));
return .{ .year = year, .month = month, .day = day };
}
}
pub fn main() !void {
var result: i64 = 0;
const iterations = 10_000_000;
for (0..iterations) |i| {
const days: i32 = @intCast(@as(i64, @intCast(i)) - iterations / 2);
const date = civilFromDays(days);
result +%= @as(i64, date.year) + @as(i64, date.month) + @as(i64, date.day);
}
std.debug.print("result: {}\n", .{result});
}
================================================
FILE: bench/bench_days_benjoffe.zig
================================================
const std = @import("std");
const Date = struct {
year: i32,
month: u4,
day: u5,
};
/// Ben Joffe's fast overflow-safe inverse function
/// https://www.benjoffe.com/fast-date-64
fn daysFromCivil(date: Date) i32 {
const month: u32 = date.month;
const bump: u32 = if (month <= 2) 1 else 0;
const yrs: u32 = @bitCast(date.year +% 5880000 - @as(i32, @intCast(bump)));
const cen: u32 = yrs / 100;
const shift: i32 = if (bump == 1) 8829 else -2919;
const year_days: u32 = yrs * 365 + yrs / 4 - cen + cen / 4;
const month_days: u32 = @bitCast(@divFloor(979 * @as(i32, @intCast(month)) + shift, 32));
return @bitCast(year_days +% month_days +% date.day -% 2148345369);
}
pub fn main() !void {
var result: i64 = 0;
const iterations = 10_000_000;
for (0..iterations) |i| {
const year: i32 = @intCast(@as(i64, @intCast(i)) - iterations / 2);
const date = Date{ .year = year, .month = 6, .day = 15 };
result +%= daysFromCivil(date);
}
std.debug.print("result: {}\n", .{result});
}
================================================
FILE: bench/bench_days_current.zig
================================================
const std = @import("std");
const Date = struct {
year: i32,
month: u4,
day: u5,
};
const days_per_era = 365 * 400 + 97;
/// Current Hinnant algorithm
fn daysFromCivil(date: Date) i32 {
const m: i32 = date.month;
const y: i32 = if (m <= 2) date.year - 1 else date.year;
const era = @divFloor(y, 400);
const yoe: u32 = @intCast(y - era * 400);
const doy = blk: {
const a: u32 = if (m > 2) @intCast(m - 3) else @intCast(m + 9);
const b = a * 153 + 2;
break :blk @divFloor(b, 5) + date.day - 1;
};
const doe: i32 = @intCast(yoe * 365 + @divFloor(yoe, 4) - @divFloor(yoe, 100) + doy);
return era * days_per_era + doe - 719468;
}
pub fn main() !void {
var result: i64 = 0;
const iterations = 10_000_000;
for (0..iterations) |i| {
const year: i32 = @intCast(@as(i64, @intCast(i)) - iterations / 2);
const date = Date{ .year = year, .month = 6, .day = 15 };
result +%= daysFromCivil(date);
}
std.debug.print("result: {}\n", .{result});
}
================================================
FILE: bench/bench_hinnant.zig
================================================
const std = @import("std");
const Date = struct {
year: i32,
month: u4,
day: u5,
};
const days_per_era = 365 * 400 + 97;
/// Howard Hinnant's algorithm
/// https://howardhinnant.github.io/date_algorithms.html#civil_from_days
fn civilFromDays(days: i32) Date {
const z = days + 719468;
const era = @divFloor(z, days_per_era);
const doe: u32 = @intCast(z - era * days_per_era);
const yoe: u32 = @intCast(
@divFloor(
doe -
@divFloor(doe, 1460) +
@divFloor(doe, 36524) -
@divFloor(doe, 146096),
365,
),
);
const y: i32 = @as(i32, @intCast(yoe)) + era * 400;
const doy = doe - (365 * yoe + @divFloor(yoe, 4) - @divFloor(yoe, 100));
const mp = @divFloor(5 * doy + 2, 153);
const d = doy - @divFloor(153 * mp + 2, 5) + 1;
const m = if (mp < 10) mp + 3 else mp - 9;
return .{
.year = if (m <= 2) y + 1 else y,
.month = @intCast(m),
.day = @truncate(d),
};
}
pub fn main() !void {
var result: i64 = 0;
const iterations = 10_000_000;
for (0..iterations) |i| {
const days: i32 = @intCast(@as(i64, @intCast(i)) - iterations / 2);
const date = civilFromDays(days);
result +%= @as(i64, date.year) + @as(i64, date.month) + @as(i64, date.day);
}
std.debug.print("result: {}\n", .{result});
}
================================================
FILE: bench/bench_leap_benjoffe.zig
================================================
const std = @import("std");
/// Ben Joffe's fast full-range leap year algorithm
/// https://www.benjoffe.com/fast-leap-year
fn isLeapYear(year: i32) bool {
const cen_bias: u32 = 2147483600;
const cen_mul: u32 = 42949673;
const cen_cutoff: u32 = 171798692;
const a: u32 = @bitCast(year +% @as(i32, @bitCast(cen_bias)));
const low: u32 = a *% cen_mul;
const is_likely_cen = low < cen_cutoff;
const mask: u5 = if (is_likely_cen) 15 else 3;
return (year & mask) == 0;
}
pub fn main() !void {
var result: u64 = 0;
const iterations: i32 = 100_000_000;
var year: i32 = -iterations / 2;
while (year < iterations / 2) : (year += 1) {
if (isLeapYear(year)) result += 1;
}
std.debug.print("result: {}\n", .{result});
}
================================================
FILE: bench/bench_leap_current.zig
================================================
const std = @import("std");
/// Current Neri/Schneider algorithm
fn isLeapYear(year: i32) bool {
const d: i32 = if (@mod(year, 100) != 0) 4 else 16;
return (year & (d - 1)) == 0;
}
pub fn main() !void {
var result: u64 = 0;
const iterations: i32 = 100_000_000;
var year: i32 = -iterations / 2;
while (year < iterations / 2) : (year += 1) {
if (isLeapYear(year)) result += 1;
}
std.debug.print("result: {}\n", .{result});
}
================================================
FILE: bench/test_days.zig
================================================
const std = @import("std");
const Date = struct {
year: i32,
month: u4,
day: u5,
};
const days_per_era = 365 * 400 + 97;
fn daysFromCivilCurrent(date: Date) i32 {
const m: i32 = date.month;
const y: i32 = if (m <= 2) date.year - 1 else date.year;
const era = @divFloor(y, 400);
const yoe: u32 = @intCast(y - era * 400);
const doy = blk: {
const a: u32 = if (m > 2) @intCast(m - 3) else @intCast(m + 9);
const b = a * 153 + 2;
break :blk @divFloor(b, 5) + date.day - 1;
};
const doe: i32 = @intCast(yoe * 365 + @divFloor(yoe, 4) - @divFloor(yoe, 100) + doy);
return era * days_per_era + doe - 719468;
}
fn daysFromCivilBenjoffe(date: Date) i32 {
const month: u32 = date.month;
const bump: u32 = if (month <= 2) 1 else 0;
const yrs: u32 = @bitCast(date.year +% 5880000 - @as(i32, @intCast(bump)));
const cen: u32 = yrs / 100;
const shift: i32 = if (bump == 1) 8829 else -2919;
const year_days: u32 = yrs * 365 + yrs / 4 - cen + cen / 4;
const month_days: u32 = @bitCast(@divFloor(979 * @as(i32, @intCast(month)) + shift, 32));
return @bitCast(year_days +% month_days +% date.day -% 2148345369);
}
pub fn main() void {
var mismatches: u32 = 0;
// Test a wide range of years and all months/days
const test_years = [_]i32{ -5000, -1000, -100, -1, 0, 1, 100, 1000, 1970, 2000, 2024, 5000, 100000, -100000 };
for (test_years) |year| {
for (1..13) |m| {
const month: u4 = @intCast(m);
for (1..29) |d| {
const day: u5 = @intCast(d);
const date = Date{ .year = year, .month = month, .day = day };
const current = daysFromCivilCurrent(date);
const benjoffe = daysFromCivilBenjoffe(date);
if (current != benjoffe) {
mismatches += 1;
if (mismatches <= 10) {
std.debug.print("MISMATCH: {}-{:0>2}-{:0>2}: current={} benjoffe={}\n", .{ year, month, day, current, benjoffe });
}
}
}
}
}
if (mismatches == 0) {
std.debug.print("All tests passed!\n", .{});
} else {
std.debug.print("Total mismatches: {}\n", .{mismatches});
}
}
================================================
FILE: bench/test_leap.zig
================================================
const std = @import("std");
fn isLeapYearCurrent(year: i32) bool {
const d: i32 = if (@mod(year, 100) != 0) 4 else 16;
return (year & (d - 1)) == 0;
}
fn isLeapYearBenjoffe(year: i32) bool {
const cen_bias: u32 = 2147483600;
const cen_mul: u32 = 42949673;
const cen_cutoff: u32 = 171798692;
const a: u32 = @bitCast(year +% @as(i32, @bitCast(cen_bias)));
const low: u32 = a *% cen_mul;
const is_likely_cen = low < cen_cutoff;
const mask: u5 = if (is_likely_cen) 15 else 3;
return (year & mask) == 0;
}
pub fn main() void {
var mismatches: u32 = 0;
// Test full range of interesting years
var year: i32 = -1_000_000;
while (year <= 1_000_000) : (year += 1) {
const current = isLeapYearCurrent(year);
const benjoffe = isLeapYearBenjoffe(year);
if (current != benjoffe) {
mismatches += 1;
if (mismatches <= 10) {
std.debug.print("MISMATCH: year={}: current={} benjoffe={}\n", .{ year, current, benjoffe });
}
}
}
if (mismatches == 0) {
std.debug.print("All tests passed!\n", .{});
} else {
std.debug.print("Total mismatches: {}\n", .{mismatches});
}
}
================================================
FILE: build.zig
================================================
const std = @import("std");
pub fn build(b: *std.Build) void {
const target = b.standardTargetOptions(.{});
const optimize = b.standardOptimizeOption(.{});
const root_module = b.addModule("zeit", .{
.root_source_file = b.path("src/zeit.zig"),
.target = target,
.optimize = optimize,
});
const lib_unit_tests = b.addTest(.{
.root_module = root_module,
});
const run_lib_unit_tests = b.addRunArtifact(lib_unit_tests);
const test_step = b.step("test", "Run unit tests");
test_step.dependOn(&run_lib_unit_tests.step);
const gen_step = b.step("generate", "Update timezone names");
const gen = b.addExecutable(.{
.name = "generate",
.root_module = b.createModule(.{
.root_source_file = b.path("gen/main.zig"),
.target = target,
.optimize = optimize,
}),
});
const fmt = b.addFmt(
.{ .paths = &.{"src/location.zig"} },
);
const gen_run = b.addRunArtifact(gen);
fmt.step.dependOn(&gen_run.step);
gen_step.dependOn(&fmt.step);
// Docs
{
const docs_step = b.step("docs", "Build the zeit docs");
const docs_obj = b.addObject(.{
.name = "zeit",
.root_module = b.createModule(.{
.root_source_file = b.path("src/zeit.zig"),
.target = target,
.optimize = optimize,
}),
});
const docs = docs_obj.getEmittedDocs();
docs_step.dependOn(&b.addInstallDirectory(.{
.source_dir = docs,
.install_dir = .prefix,
.install_subdir = "docs",
}).step);
}
}
================================================
FILE: build.zig.zon
================================================
.{
.name = .zeit,
.fingerprint = 0x888df3e4939b8ee4,
.version = "0.6.0",
.dependencies = .{},
.paths = .{
"LICENSE",
"build.zig",
"build.zig.zon",
"src",
},
.minimum_zig_version = "0.16.0-dev.2533+355c62600",
}
================================================
FILE: gen/main.zig
================================================
//! Generates zig code for well-known timezones. Makes an enum of the "posix" style name
//! ("America/Chicago") and a function to return the timezone name as text. The name as text is
//! portable by platform: on Windows it will return the Windows name of this timezone
//!
//! Source data available at https://github.com/unicode-org/cldr/blob/main/common/supplemental/windowsZones.xml
const std = @import("std");
pub fn main(init: std.process.Init) !void {
const allocator = init.arena.allocator();
var threaded: std.Io.Threaded = .init(allocator, .{});
defer threaded.deinit();
const io = threaded.io();
//const allocator = std.heap.page_allocator;
const data = @embedFile("windowsZones.xml");
var zones: std.ArrayList(MapZone) = .empty;
var read_idx: usize = 0;
while (read_idx < data.len) {
const eol = std.mem.indexOfScalarPos(u8, data, read_idx, '\n') orelse data.len;
defer read_idx = eol + 1;
const input_line = data[read_idx..eol];
const line = std.mem.trimEnd(u8, std.mem.trim(u8, input_line, " \t<>"), "/");
if (!std.mem.startsWith(u8, line, "mapZone")) continue;
var idx: usize = 0;
const windows = blk: {
idx = std.mem.indexOfScalarPos(u8, line, idx, '"') orelse unreachable;
const start = idx + 1;
idx = std.mem.indexOfScalarPos(u8, line, start, '"') orelse unreachable;
const end = idx;
break :blk line[start..end];
};
const territory = blk: {
idx = std.mem.indexOfScalarPos(u8, line, idx + 1, '"') orelse unreachable;
const start = idx + 1;
idx = std.mem.indexOfScalarPos(u8, line, start, '"') orelse unreachable;
const end = idx;
break :blk line[start..end];
};
const posix = blk: {
idx = std.mem.indexOfScalarPos(u8, line, idx + 1, '"') orelse unreachable;
const start = idx + 1;
idx = std.mem.indexOfScalarPos(u8, line, start, '"') orelse unreachable;
const end = idx;
break :blk line[start..end];
};
var iter = std.mem.splitScalar(u8, posix, ' ');
while (iter.next()) |psx| {
const map_zone: MapZone = .{
.windows = windows,
.territory = territory,
.posix = psx,
};
if (psx.len == 0) continue;
for (zones.items) |item| {
if (std.mem.eql(u8, item.windows, map_zone.windows) and
std.mem.eql(u8, item.posix, map_zone.posix)) break;
} else try zones.append(allocator, map_zone);
}
}
std.mem.sort(MapZone, zones.items, {}, lessThan);
var out = try std.Io.Dir.createFile(.cwd(), io, "src/location.zig", .{}); //.cwd().createFile("src/location.zig", .{});
defer out.close(io);
var output_buffer: [2048]u8 = undefined;
var writer = out.writer(io, &output_buffer);
try writeFile(zones.items, &writer.interface);
}
fn lessThan(_: void, lhs: MapZone, rhs: MapZone) bool {
return std.mem.order(u8, lhs.posix, rhs.posix).compare(.lt);
}
const MapZone = struct {
windows: []const u8,
territory: []const u8,
posix: []const u8,
};
fn writeFile(items: []const MapZone, writer: *std.Io.Writer) !void {
try writer.writeAll(
\\//!This file is generated. Do not edit directly! Run `zig build generate` to update after obtaining
\\//!the latest dataset.
\\
\\const builtin = @import("builtin");
\\pub const Location = enum {
\\
);
for (items) |item| {
try writer.print("@\"{s}\",\n", .{item.posix});
}
try writer.writeAll("\n");
try writer.writeAll(
\\ pub fn asText(self: Location) []const u8 {
\\ switch (builtin.os.tag) {
\\ .windows => {},
\\ else => return @tagName(self),
\\ }
);
try writer.writeAll(" return switch (self) {\n");
for (items) |item| {
try writer.print(".@\"{s}\" => \"{s}\",\n", .{ item.posix, item.windows });
}
try writer.writeAll("};}};");
try writer.flush(); // don't forget to flush!
}
================================================
FILE: gen/windowsZones.xml
================================================
================================================
FILE: src/location.zig
================================================
//!This file is generated. Do not edit directly! Run `zig build generate` to update after obtaining
//!the latest dataset.
const builtin = @import("builtin");
pub const Location = enum {
@"Africa/Abidjan",
@"Africa/Accra",
@"Africa/Addis_Ababa",
@"Africa/Algiers",
@"Africa/Asmera",
@"Africa/Bamako",
@"Africa/Bangui",
@"Africa/Banjul",
@"Africa/Bissau",
@"Africa/Blantyre",
@"Africa/Brazzaville",
@"Africa/Bujumbura",
@"Africa/Cairo",
@"Africa/Casablanca",
@"Africa/Ceuta",
@"Africa/Conakry",
@"Africa/Dakar",
@"Africa/Dar_es_Salaam",
@"Africa/Djibouti",
@"Africa/Douala",
@"Africa/El_Aaiun",
@"Africa/Freetown",
@"Africa/Gaborone",
@"Africa/Harare",
@"Africa/Johannesburg",
@"Africa/Juba",
@"Africa/Kampala",
@"Africa/Khartoum",
@"Africa/Kigali",
@"Africa/Kinshasa",
@"Africa/Lagos",
@"Africa/Libreville",
@"Africa/Lome",
@"Africa/Luanda",
@"Africa/Lubumbashi",
@"Africa/Lusaka",
@"Africa/Malabo",
@"Africa/Maputo",
@"Africa/Maseru",
@"Africa/Mbabane",
@"Africa/Mogadishu",
@"Africa/Monrovia",
@"Africa/Nairobi",
@"Africa/Ndjamena",
@"Africa/Niamey",
@"Africa/Nouakchott",
@"Africa/Ouagadougou",
@"Africa/Porto-Novo",
@"Africa/Sao_Tome",
@"Africa/Tripoli",
@"Africa/Tunis",
@"Africa/Windhoek",
@"America/Adak",
@"America/Anchorage",
@"America/Anguilla",
@"America/Antigua",
@"America/Araguaina",
@"America/Argentina/La_Rioja",
@"America/Argentina/Rio_Gallegos",
@"America/Argentina/Salta",
@"America/Argentina/San_Juan",
@"America/Argentina/San_Luis",
@"America/Argentina/Tucuman",
@"America/Argentina/Ushuaia",
@"America/Aruba",
@"America/Asuncion",
@"America/Bahia",
@"America/Bahia_Banderas",
@"America/Barbados",
@"America/Belem",
@"America/Belize",
@"America/Blanc-Sablon",
@"America/Boa_Vista",
@"America/Bogota",
@"America/Boise",
@"America/Buenos_Aires",
@"America/Cambridge_Bay",
@"America/Campo_Grande",
@"America/Cancun",
@"America/Caracas",
@"America/Catamarca",
@"America/Cayenne",
@"America/Cayman",
@"America/Chicago",
@"America/Chihuahua",
@"America/Ciudad_Juarez",
@"America/Coral_Harbour",
@"America/Cordoba",
@"America/Costa_Rica",
@"America/Creston",
@"America/Cuiaba",
@"America/Curacao",
@"America/Danmarkshavn",
@"America/Dawson",
@"America/Dawson_Creek",
@"America/Denver",
@"America/Detroit",
@"America/Dominica",
@"America/Edmonton",
@"America/Eirunepe",
@"America/El_Salvador",
@"America/Fort_Nelson",
@"America/Fortaleza",
@"America/Glace_Bay",
@"America/Godthab",
@"America/Goose_Bay",
@"America/Grand_Turk",
@"America/Grenada",
@"America/Guadeloupe",
@"America/Guatemala",
@"America/Guayaquil",
@"America/Guyana",
@"America/Halifax",
@"America/Havana",
@"America/Hermosillo",
@"America/Indiana/Knox",
@"America/Indiana/Marengo",
@"America/Indiana/Petersburg",
@"America/Indiana/Tell_City",
@"America/Indiana/Vevay",
@"America/Indiana/Vincennes",
@"America/Indiana/Winamac",
@"America/Indianapolis",
@"America/Inuvik",
@"America/Iqaluit",
@"America/Jamaica",
@"America/Jujuy",
@"America/Juneau",
@"America/Kentucky/Monticello",
@"America/Kralendijk",
@"America/La_Paz",
@"America/Lima",
@"America/Los_Angeles",
@"America/Louisville",
@"America/Lower_Princes",
@"America/Maceio",
@"America/Managua",
@"America/Manaus",
@"America/Marigot",
@"America/Martinique",
@"America/Matamoros",
@"America/Mazatlan",
@"America/Mendoza",
@"America/Menominee",
@"America/Merida",
@"America/Metlakatla",
@"America/Mexico_City",
@"America/Miquelon",
@"America/Moncton",
@"America/Monterrey",
@"America/Montevideo",
@"America/Montserrat",
@"America/Nassau",
@"America/New_York",
@"America/Nome",
@"America/Noronha",
@"America/North_Dakota/Beulah",
@"America/North_Dakota/Center",
@"America/North_Dakota/New_Salem",
@"America/Ojinaga",
@"America/Panama",
@"America/Paramaribo",
@"America/Phoenix",
@"America/Port-au-Prince",
@"America/Port_of_Spain",
@"America/Porto_Velho",
@"America/Puerto_Rico",
@"America/Punta_Arenas",
@"America/Rankin_Inlet",
@"America/Recife",
@"America/Regina",
@"America/Resolute",
@"America/Rio_Branco",
@"America/Santarem",
@"America/Santiago",
@"America/Santo_Domingo",
@"America/Sao_Paulo",
@"America/Scoresbysund",
@"America/Sitka",
@"America/St_Barthelemy",
@"America/St_Johns",
@"America/St_Kitts",
@"America/St_Lucia",
@"America/St_Thomas",
@"America/St_Vincent",
@"America/Swift_Current",
@"America/Tegucigalpa",
@"America/Thule",
@"America/Tijuana",
@"America/Toronto",
@"America/Tortola",
@"America/Vancouver",
@"America/Whitehorse",
@"America/Winnipeg",
@"America/Yakutat",
@"Antarctica/Casey",
@"Antarctica/Davis",
@"Antarctica/DumontDUrville",
@"Antarctica/Macquarie",
@"Antarctica/Mawson",
@"Antarctica/McMurdo",
@"Antarctica/Palmer",
@"Antarctica/Rothera",
@"Antarctica/Syowa",
@"Antarctica/Vostok",
@"Arctic/Longyearbyen",
@"Asia/Aden",
@"Asia/Almaty",
@"Asia/Amman",
@"Asia/Anadyr",
@"Asia/Aqtau",
@"Asia/Aqtobe",
@"Asia/Ashgabat",
@"Asia/Atyrau",
@"Asia/Baghdad",
@"Asia/Bahrain",
@"Asia/Baku",
@"Asia/Bangkok",
@"Asia/Barnaul",
@"Asia/Beirut",
@"Asia/Bishkek",
@"Asia/Brunei",
@"Asia/Calcutta",
@"Asia/Chita",
@"Asia/Colombo",
@"Asia/Damascus",
@"Asia/Dhaka",
@"Asia/Dili",
@"Asia/Dubai",
@"Asia/Dushanbe",
@"Asia/Famagusta",
@"Asia/Gaza",
@"Asia/Hebron",
@"Asia/Hong_Kong",
@"Asia/Hovd",
@"Asia/Irkutsk",
@"Asia/Jakarta",
@"Asia/Jayapura",
@"Asia/Jerusalem",
@"Asia/Kabul",
@"Asia/Kamchatka",
@"Asia/Karachi",
@"Asia/Katmandu",
@"Asia/Khandyga",
@"Asia/Krasnoyarsk",
@"Asia/Kuala_Lumpur",
@"Asia/Kuching",
@"Asia/Kuwait",
@"Asia/Macau",
@"Asia/Magadan",
@"Asia/Makassar",
@"Asia/Manila",
@"Asia/Muscat",
@"Asia/Nicosia",
@"Asia/Novokuznetsk",
@"Asia/Novosibirsk",
@"Asia/Omsk",
@"Asia/Oral",
@"Asia/Phnom_Penh",
@"Asia/Pontianak",
@"Asia/Pyongyang",
@"Asia/Qatar",
@"Asia/Qostanay",
@"Asia/Qyzylorda",
@"Asia/Rangoon",
@"Asia/Riyadh",
@"Asia/Saigon",
@"Asia/Sakhalin",
@"Asia/Samarkand",
@"Asia/Seoul",
@"Asia/Shanghai",
@"Asia/Singapore",
@"Asia/Srednekolymsk",
@"Asia/Taipei",
@"Asia/Tashkent",
@"Asia/Tbilisi",
@"Asia/Tehran",
@"Asia/Thimphu",
@"Asia/Tokyo",
@"Asia/Tomsk",
@"Asia/Ulaanbaatar",
@"Asia/Urumqi",
@"Asia/Ust-Nera",
@"Asia/Vientiane",
@"Asia/Vladivostok",
@"Asia/Yakutsk",
@"Asia/Yekaterinburg",
@"Asia/Yerevan",
@"Atlantic/Azores",
@"Atlantic/Bermuda",
@"Atlantic/Canary",
@"Atlantic/Cape_Verde",
@"Atlantic/Faeroe",
@"Atlantic/Madeira",
@"Atlantic/Reykjavik",
@"Atlantic/South_Georgia",
@"Atlantic/St_Helena",
@"Atlantic/Stanley",
@"Australia/Adelaide",
@"Australia/Brisbane",
@"Australia/Broken_Hill",
@"Australia/Darwin",
@"Australia/Eucla",
@"Australia/Hobart",
@"Australia/Lindeman",
@"Australia/Lord_Howe",
@"Australia/Melbourne",
@"Australia/Perth",
@"Australia/Sydney",
@"Etc/GMT",
@"Etc/GMT+1",
@"Etc/GMT+10",
@"Etc/GMT+11",
@"Etc/GMT+12",
@"Etc/GMT+2",
@"Etc/GMT+3",
@"Etc/GMT+4",
@"Etc/GMT+5",
@"Etc/GMT+6",
@"Etc/GMT+7",
@"Etc/GMT+8",
@"Etc/GMT+9",
@"Etc/GMT-1",
@"Etc/GMT-10",
@"Etc/GMT-11",
@"Etc/GMT-12",
@"Etc/GMT-13",
@"Etc/GMT-14",
@"Etc/GMT-2",
@"Etc/GMT-3",
@"Etc/GMT-4",
@"Etc/GMT-5",
@"Etc/GMT-6",
@"Etc/GMT-7",
@"Etc/GMT-8",
@"Etc/GMT-9",
@"Etc/UTC",
@"Europe/Amsterdam",
@"Europe/Andorra",
@"Europe/Astrakhan",
@"Europe/Athens",
@"Europe/Belgrade",
@"Europe/Berlin",
@"Europe/Bratislava",
@"Europe/Brussels",
@"Europe/Bucharest",
@"Europe/Budapest",
@"Europe/Busingen",
@"Europe/Chisinau",
@"Europe/Copenhagen",
@"Europe/Dublin",
@"Europe/Gibraltar",
@"Europe/Guernsey",
@"Europe/Helsinki",
@"Europe/Isle_of_Man",
@"Europe/Istanbul",
@"Europe/Jersey",
@"Europe/Kaliningrad",
@"Europe/Kiev",
@"Europe/Kirov",
@"Europe/Lisbon",
@"Europe/Ljubljana",
@"Europe/London",
@"Europe/Luxembourg",
@"Europe/Madrid",
@"Europe/Malta",
@"Europe/Mariehamn",
@"Europe/Minsk",
@"Europe/Monaco",
@"Europe/Moscow",
@"Europe/Oslo",
@"Europe/Paris",
@"Europe/Podgorica",
@"Europe/Prague",
@"Europe/Riga",
@"Europe/Rome",
@"Europe/Samara",
@"Europe/San_Marino",
@"Europe/Sarajevo",
@"Europe/Saratov",
@"Europe/Simferopol",
@"Europe/Skopje",
@"Europe/Sofia",
@"Europe/Stockholm",
@"Europe/Tallinn",
@"Europe/Tirane",
@"Europe/Ulyanovsk",
@"Europe/Vaduz",
@"Europe/Vatican",
@"Europe/Vienna",
@"Europe/Vilnius",
@"Europe/Volgograd",
@"Europe/Warsaw",
@"Europe/Zagreb",
@"Europe/Zurich",
@"Indian/Antananarivo",
@"Indian/Chagos",
@"Indian/Christmas",
@"Indian/Cocos",
@"Indian/Comoro",
@"Indian/Kerguelen",
@"Indian/Mahe",
@"Indian/Maldives",
@"Indian/Mauritius",
@"Indian/Mayotte",
@"Indian/Reunion",
@"Pacific/Apia",
@"Pacific/Auckland",
@"Pacific/Bougainville",
@"Pacific/Chatham",
@"Pacific/Easter",
@"Pacific/Efate",
@"Pacific/Enderbury",
@"Pacific/Fakaofo",
@"Pacific/Fiji",
@"Pacific/Funafuti",
@"Pacific/Galapagos",
@"Pacific/Gambier",
@"Pacific/Guadalcanal",
@"Pacific/Guam",
@"Pacific/Honolulu",
@"Pacific/Kiritimati",
@"Pacific/Kosrae",
@"Pacific/Kwajalein",
@"Pacific/Majuro",
@"Pacific/Marquesas",
@"Pacific/Midway",
@"Pacific/Nauru",
@"Pacific/Niue",
@"Pacific/Norfolk",
@"Pacific/Noumea",
@"Pacific/Pago_Pago",
@"Pacific/Palau",
@"Pacific/Pitcairn",
@"Pacific/Ponape",
@"Pacific/Port_Moresby",
@"Pacific/Rarotonga",
@"Pacific/Saipan",
@"Pacific/Tahiti",
@"Pacific/Tarawa",
@"Pacific/Tongatapu",
@"Pacific/Truk",
@"Pacific/Wake",
@"Pacific/Wallis",
pub fn asText(self: Location) []const u8 {
switch (builtin.os.tag) {
.windows => {},
else => return @tagName(self),
}
return switch (self) {
.@"Africa/Abidjan" => "Greenwich Standard Time",
.@"Africa/Accra" => "Greenwich Standard Time",
.@"Africa/Addis_Ababa" => "E. Africa Standard Time",
.@"Africa/Algiers" => "W. Central Africa Standard Time",
.@"Africa/Asmera" => "E. Africa Standard Time",
.@"Africa/Bamako" => "Greenwich Standard Time",
.@"Africa/Bangui" => "W. Central Africa Standard Time",
.@"Africa/Banjul" => "Greenwich Standard Time",
.@"Africa/Bissau" => "Greenwich Standard Time",
.@"Africa/Blantyre" => "South Africa Standard Time",
.@"Africa/Brazzaville" => "W. Central Africa Standard Time",
.@"Africa/Bujumbura" => "South Africa Standard Time",
.@"Africa/Cairo" => "Egypt Standard Time",
.@"Africa/Casablanca" => "Morocco Standard Time",
.@"Africa/Ceuta" => "Romance Standard Time",
.@"Africa/Conakry" => "Greenwich Standard Time",
.@"Africa/Dakar" => "Greenwich Standard Time",
.@"Africa/Dar_es_Salaam" => "E. Africa Standard Time",
.@"Africa/Djibouti" => "E. Africa Standard Time",
.@"Africa/Douala" => "W. Central Africa Standard Time",
.@"Africa/El_Aaiun" => "Morocco Standard Time",
.@"Africa/Freetown" => "Greenwich Standard Time",
.@"Africa/Gaborone" => "South Africa Standard Time",
.@"Africa/Harare" => "South Africa Standard Time",
.@"Africa/Johannesburg" => "South Africa Standard Time",
.@"Africa/Juba" => "South Sudan Standard Time",
.@"Africa/Kampala" => "E. Africa Standard Time",
.@"Africa/Khartoum" => "Sudan Standard Time",
.@"Africa/Kigali" => "South Africa Standard Time",
.@"Africa/Kinshasa" => "W. Central Africa Standard Time",
.@"Africa/Lagos" => "W. Central Africa Standard Time",
.@"Africa/Libreville" => "W. Central Africa Standard Time",
.@"Africa/Lome" => "Greenwich Standard Time",
.@"Africa/Luanda" => "W. Central Africa Standard Time",
.@"Africa/Lubumbashi" => "South Africa Standard Time",
.@"Africa/Lusaka" => "South Africa Standard Time",
.@"Africa/Malabo" => "W. Central Africa Standard Time",
.@"Africa/Maputo" => "South Africa Standard Time",
.@"Africa/Maseru" => "South Africa Standard Time",
.@"Africa/Mbabane" => "South Africa Standard Time",
.@"Africa/Mogadishu" => "E. Africa Standard Time",
.@"Africa/Monrovia" => "Greenwich Standard Time",
.@"Africa/Nairobi" => "E. Africa Standard Time",
.@"Africa/Ndjamena" => "W. Central Africa Standard Time",
.@"Africa/Niamey" => "W. Central Africa Standard Time",
.@"Africa/Nouakchott" => "Greenwich Standard Time",
.@"Africa/Ouagadougou" => "Greenwich Standard Time",
.@"Africa/Porto-Novo" => "W. Central Africa Standard Time",
.@"Africa/Sao_Tome" => "Sao Tome Standard Time",
.@"Africa/Tripoli" => "Libya Standard Time",
.@"Africa/Tunis" => "W. Central Africa Standard Time",
.@"Africa/Windhoek" => "Namibia Standard Time",
.@"America/Adak" => "Aleutian Standard Time",
.@"America/Anchorage" => "Alaskan Standard Time",
.@"America/Anguilla" => "SA Western Standard Time",
.@"America/Antigua" => "SA Western Standard Time",
.@"America/Araguaina" => "Tocantins Standard Time",
.@"America/Argentina/La_Rioja" => "Argentina Standard Time",
.@"America/Argentina/Rio_Gallegos" => "Argentina Standard Time",
.@"America/Argentina/Salta" => "Argentina Standard Time",
.@"America/Argentina/San_Juan" => "Argentina Standard Time",
.@"America/Argentina/San_Luis" => "Argentina Standard Time",
.@"America/Argentina/Tucuman" => "Argentina Standard Time",
.@"America/Argentina/Ushuaia" => "Argentina Standard Time",
.@"America/Aruba" => "SA Western Standard Time",
.@"America/Asuncion" => "Paraguay Standard Time",
.@"America/Bahia" => "Bahia Standard Time",
.@"America/Bahia_Banderas" => "Central Standard Time (Mexico)",
.@"America/Barbados" => "SA Western Standard Time",
.@"America/Belem" => "SA Eastern Standard Time",
.@"America/Belize" => "Central America Standard Time",
.@"America/Blanc-Sablon" => "SA Western Standard Time",
.@"America/Boa_Vista" => "SA Western Standard Time",
.@"America/Bogota" => "SA Pacific Standard Time",
.@"America/Boise" => "Mountain Standard Time",
.@"America/Buenos_Aires" => "Argentina Standard Time",
.@"America/Cambridge_Bay" => "Mountain Standard Time",
.@"America/Campo_Grande" => "Central Brazilian Standard Time",
.@"America/Cancun" => "Eastern Standard Time (Mexico)",
.@"America/Caracas" => "Venezuela Standard Time",
.@"America/Catamarca" => "Argentina Standard Time",
.@"America/Cayenne" => "SA Eastern Standard Time",
.@"America/Cayman" => "SA Pacific Standard Time",
.@"America/Chicago" => "Central Standard Time",
.@"America/Chihuahua" => "Central Standard Time (Mexico)",
.@"America/Ciudad_Juarez" => "Mountain Standard Time",
.@"America/Coral_Harbour" => "SA Pacific Standard Time",
.@"America/Cordoba" => "Argentina Standard Time",
.@"America/Costa_Rica" => "Central America Standard Time",
.@"America/Creston" => "US Mountain Standard Time",
.@"America/Cuiaba" => "Central Brazilian Standard Time",
.@"America/Curacao" => "SA Western Standard Time",
.@"America/Danmarkshavn" => "Greenwich Standard Time",
.@"America/Dawson" => "Yukon Standard Time",
.@"America/Dawson_Creek" => "US Mountain Standard Time",
.@"America/Denver" => "Mountain Standard Time",
.@"America/Detroit" => "Eastern Standard Time",
.@"America/Dominica" => "SA Western Standard Time",
.@"America/Edmonton" => "Mountain Standard Time",
.@"America/Eirunepe" => "SA Pacific Standard Time",
.@"America/El_Salvador" => "Central America Standard Time",
.@"America/Fort_Nelson" => "US Mountain Standard Time",
.@"America/Fortaleza" => "SA Eastern Standard Time",
.@"America/Glace_Bay" => "Atlantic Standard Time",
.@"America/Godthab" => "Greenland Standard Time",
.@"America/Goose_Bay" => "Atlantic Standard Time",
.@"America/Grand_Turk" => "Turks And Caicos Standard Time",
.@"America/Grenada" => "SA Western Standard Time",
.@"America/Guadeloupe" => "SA Western Standard Time",
.@"America/Guatemala" => "Central America Standard Time",
.@"America/Guayaquil" => "SA Pacific Standard Time",
.@"America/Guyana" => "SA Western Standard Time",
.@"America/Halifax" => "Atlantic Standard Time",
.@"America/Havana" => "Cuba Standard Time",
.@"America/Hermosillo" => "US Mountain Standard Time",
.@"America/Indiana/Knox" => "Central Standard Time",
.@"America/Indiana/Marengo" => "US Eastern Standard Time",
.@"America/Indiana/Petersburg" => "Eastern Standard Time",
.@"America/Indiana/Tell_City" => "Central Standard Time",
.@"America/Indiana/Vevay" => "US Eastern Standard Time",
.@"America/Indiana/Vincennes" => "Eastern Standard Time",
.@"America/Indiana/Winamac" => "Eastern Standard Time",
.@"America/Indianapolis" => "US Eastern Standard Time",
.@"America/Inuvik" => "Mountain Standard Time",
.@"America/Iqaluit" => "Eastern Standard Time",
.@"America/Jamaica" => "SA Pacific Standard Time",
.@"America/Jujuy" => "Argentina Standard Time",
.@"America/Juneau" => "Alaskan Standard Time",
.@"America/Kentucky/Monticello" => "Eastern Standard Time",
.@"America/Kralendijk" => "SA Western Standard Time",
.@"America/La_Paz" => "SA Western Standard Time",
.@"America/Lima" => "SA Pacific Standard Time",
.@"America/Los_Angeles" => "Pacific Standard Time",
.@"America/Louisville" => "Eastern Standard Time",
.@"America/Lower_Princes" => "SA Western Standard Time",
.@"America/Maceio" => "SA Eastern Standard Time",
.@"America/Managua" => "Central America Standard Time",
.@"America/Manaus" => "SA Western Standard Time",
.@"America/Marigot" => "SA Western Standard Time",
.@"America/Martinique" => "SA Western Standard Time",
.@"America/Matamoros" => "Central Standard Time",
.@"America/Mazatlan" => "Mountain Standard Time (Mexico)",
.@"America/Mendoza" => "Argentina Standard Time",
.@"America/Menominee" => "Central Standard Time",
.@"America/Merida" => "Central Standard Time (Mexico)",
.@"America/Metlakatla" => "Alaskan Standard Time",
.@"America/Mexico_City" => "Central Standard Time (Mexico)",
.@"America/Miquelon" => "Saint Pierre Standard Time",
.@"America/Moncton" => "Atlantic Standard Time",
.@"America/Monterrey" => "Central Standard Time (Mexico)",
.@"America/Montevideo" => "Montevideo Standard Time",
.@"America/Montserrat" => "SA Western Standard Time",
.@"America/Nassau" => "Eastern Standard Time",
.@"America/New_York" => "Eastern Standard Time",
.@"America/Nome" => "Alaskan Standard Time",
.@"America/Noronha" => "UTC-02",
.@"America/North_Dakota/Beulah" => "Central Standard Time",
.@"America/North_Dakota/Center" => "Central Standard Time",
.@"America/North_Dakota/New_Salem" => "Central Standard Time",
.@"America/Ojinaga" => "Central Standard Time",
.@"America/Panama" => "SA Pacific Standard Time",
.@"America/Paramaribo" => "SA Eastern Standard Time",
.@"America/Phoenix" => "US Mountain Standard Time",
.@"America/Port-au-Prince" => "Haiti Standard Time",
.@"America/Port_of_Spain" => "SA Western Standard Time",
.@"America/Porto_Velho" => "SA Western Standard Time",
.@"America/Puerto_Rico" => "SA Western Standard Time",
.@"America/Punta_Arenas" => "Magallanes Standard Time",
.@"America/Rankin_Inlet" => "Central Standard Time",
.@"America/Recife" => "SA Eastern Standard Time",
.@"America/Regina" => "Canada Central Standard Time",
.@"America/Resolute" => "Central Standard Time",
.@"America/Rio_Branco" => "SA Pacific Standard Time",
.@"America/Santarem" => "SA Eastern Standard Time",
.@"America/Santiago" => "Pacific SA Standard Time",
.@"America/Santo_Domingo" => "SA Western Standard Time",
.@"America/Sao_Paulo" => "E. South America Standard Time",
.@"America/Scoresbysund" => "Azores Standard Time",
.@"America/Sitka" => "Alaskan Standard Time",
.@"America/St_Barthelemy" => "SA Western Standard Time",
.@"America/St_Johns" => "Newfoundland Standard Time",
.@"America/St_Kitts" => "SA Western Standard Time",
.@"America/St_Lucia" => "SA Western Standard Time",
.@"America/St_Thomas" => "SA Western Standard Time",
.@"America/St_Vincent" => "SA Western Standard Time",
.@"America/Swift_Current" => "Canada Central Standard Time",
.@"America/Tegucigalpa" => "Central America Standard Time",
.@"America/Thule" => "Atlantic Standard Time",
.@"America/Tijuana" => "Pacific Standard Time (Mexico)",
.@"America/Toronto" => "Eastern Standard Time",
.@"America/Tortola" => "SA Western Standard Time",
.@"America/Vancouver" => "Pacific Standard Time",
.@"America/Whitehorse" => "Yukon Standard Time",
.@"America/Winnipeg" => "Central Standard Time",
.@"America/Yakutat" => "Alaskan Standard Time",
.@"Antarctica/Casey" => "Central Pacific Standard Time",
.@"Antarctica/Davis" => "SE Asia Standard Time",
.@"Antarctica/DumontDUrville" => "West Pacific Standard Time",
.@"Antarctica/Macquarie" => "Tasmania Standard Time",
.@"Antarctica/Mawson" => "West Asia Standard Time",
.@"Antarctica/McMurdo" => "New Zealand Standard Time",
.@"Antarctica/Palmer" => "SA Eastern Standard Time",
.@"Antarctica/Rothera" => "SA Eastern Standard Time",
.@"Antarctica/Syowa" => "E. Africa Standard Time",
.@"Antarctica/Vostok" => "Central Asia Standard Time",
.@"Arctic/Longyearbyen" => "W. Europe Standard Time",
.@"Asia/Aden" => "Arab Standard Time",
.@"Asia/Almaty" => "West Asia Standard Time",
.@"Asia/Amman" => "Jordan Standard Time",
.@"Asia/Anadyr" => "Russia Time Zone 11",
.@"Asia/Aqtau" => "West Asia Standard Time",
.@"Asia/Aqtobe" => "West Asia Standard Time",
.@"Asia/Ashgabat" => "West Asia Standard Time",
.@"Asia/Atyrau" => "West Asia Standard Time",
.@"Asia/Baghdad" => "Arabic Standard Time",
.@"Asia/Bahrain" => "Arab Standard Time",
.@"Asia/Baku" => "Azerbaijan Standard Time",
.@"Asia/Bangkok" => "SE Asia Standard Time",
.@"Asia/Barnaul" => "Altai Standard Time",
.@"Asia/Beirut" => "Middle East Standard Time",
.@"Asia/Bishkek" => "Central Asia Standard Time",
.@"Asia/Brunei" => "Singapore Standard Time",
.@"Asia/Calcutta" => "India Standard Time",
.@"Asia/Chita" => "Transbaikal Standard Time",
.@"Asia/Colombo" => "Sri Lanka Standard Time",
.@"Asia/Damascus" => "Syria Standard Time",
.@"Asia/Dhaka" => "Bangladesh Standard Time",
.@"Asia/Dili" => "Tokyo Standard Time",
.@"Asia/Dubai" => "Arabian Standard Time",
.@"Asia/Dushanbe" => "West Asia Standard Time",
.@"Asia/Famagusta" => "GTB Standard Time",
.@"Asia/Gaza" => "West Bank Standard Time",
.@"Asia/Hebron" => "West Bank Standard Time",
.@"Asia/Hong_Kong" => "China Standard Time",
.@"Asia/Hovd" => "W. Mongolia Standard Time",
.@"Asia/Irkutsk" => "North Asia East Standard Time",
.@"Asia/Jakarta" => "SE Asia Standard Time",
.@"Asia/Jayapura" => "Tokyo Standard Time",
.@"Asia/Jerusalem" => "Israel Standard Time",
.@"Asia/Kabul" => "Afghanistan Standard Time",
.@"Asia/Kamchatka" => "Russia Time Zone 11",
.@"Asia/Karachi" => "Pakistan Standard Time",
.@"Asia/Katmandu" => "Nepal Standard Time",
.@"Asia/Khandyga" => "Yakutsk Standard Time",
.@"Asia/Krasnoyarsk" => "North Asia Standard Time",
.@"Asia/Kuala_Lumpur" => "Singapore Standard Time",
.@"Asia/Kuching" => "Singapore Standard Time",
.@"Asia/Kuwait" => "Arab Standard Time",
.@"Asia/Macau" => "China Standard Time",
.@"Asia/Magadan" => "Magadan Standard Time",
.@"Asia/Makassar" => "Singapore Standard Time",
.@"Asia/Manila" => "Singapore Standard Time",
.@"Asia/Muscat" => "Arabian Standard Time",
.@"Asia/Nicosia" => "GTB Standard Time",
.@"Asia/Novokuznetsk" => "North Asia Standard Time",
.@"Asia/Novosibirsk" => "N. Central Asia Standard Time",
.@"Asia/Omsk" => "Omsk Standard Time",
.@"Asia/Oral" => "West Asia Standard Time",
.@"Asia/Phnom_Penh" => "SE Asia Standard Time",
.@"Asia/Pontianak" => "SE Asia Standard Time",
.@"Asia/Pyongyang" => "North Korea Standard Time",
.@"Asia/Qatar" => "Arab Standard Time",
.@"Asia/Qostanay" => "West Asia Standard Time",
.@"Asia/Qyzylorda" => "Qyzylorda Standard Time",
.@"Asia/Rangoon" => "Myanmar Standard Time",
.@"Asia/Riyadh" => "Arab Standard Time",
.@"Asia/Saigon" => "SE Asia Standard Time",
.@"Asia/Sakhalin" => "Sakhalin Standard Time",
.@"Asia/Samarkand" => "West Asia Standard Time",
.@"Asia/Seoul" => "Korea Standard Time",
.@"Asia/Shanghai" => "China Standard Time",
.@"Asia/Singapore" => "Singapore Standard Time",
.@"Asia/Srednekolymsk" => "Russia Time Zone 10",
.@"Asia/Taipei" => "Taipei Standard Time",
.@"Asia/Tashkent" => "West Asia Standard Time",
.@"Asia/Tbilisi" => "Georgian Standard Time",
.@"Asia/Tehran" => "Iran Standard Time",
.@"Asia/Thimphu" => "Bangladesh Standard Time",
.@"Asia/Tokyo" => "Tokyo Standard Time",
.@"Asia/Tomsk" => "Tomsk Standard Time",
.@"Asia/Ulaanbaatar" => "Ulaanbaatar Standard Time",
.@"Asia/Urumqi" => "Central Asia Standard Time",
.@"Asia/Ust-Nera" => "Vladivostok Standard Time",
.@"Asia/Vientiane" => "SE Asia Standard Time",
.@"Asia/Vladivostok" => "Vladivostok Standard Time",
.@"Asia/Yakutsk" => "Yakutsk Standard Time",
.@"Asia/Yekaterinburg" => "Ekaterinburg Standard Time",
.@"Asia/Yerevan" => "Caucasus Standard Time",
.@"Atlantic/Azores" => "Azores Standard Time",
.@"Atlantic/Bermuda" => "Atlantic Standard Time",
.@"Atlantic/Canary" => "GMT Standard Time",
.@"Atlantic/Cape_Verde" => "Cape Verde Standard Time",
.@"Atlantic/Faeroe" => "GMT Standard Time",
.@"Atlantic/Madeira" => "GMT Standard Time",
.@"Atlantic/Reykjavik" => "Greenwich Standard Time",
.@"Atlantic/South_Georgia" => "UTC-02",
.@"Atlantic/St_Helena" => "Greenwich Standard Time",
.@"Atlantic/Stanley" => "SA Eastern Standard Time",
.@"Australia/Adelaide" => "Cen. Australia Standard Time",
.@"Australia/Brisbane" => "E. Australia Standard Time",
.@"Australia/Broken_Hill" => "Cen. Australia Standard Time",
.@"Australia/Darwin" => "AUS Central Standard Time",
.@"Australia/Eucla" => "Aus Central W. Standard Time",
.@"Australia/Hobart" => "Tasmania Standard Time",
.@"Australia/Lindeman" => "E. Australia Standard Time",
.@"Australia/Lord_Howe" => "Lord Howe Standard Time",
.@"Australia/Melbourne" => "AUS Eastern Standard Time",
.@"Australia/Perth" => "W. Australia Standard Time",
.@"Australia/Sydney" => "AUS Eastern Standard Time",
.@"Etc/GMT" => "UTC",
.@"Etc/GMT+1" => "Cape Verde Standard Time",
.@"Etc/GMT+10" => "Hawaiian Standard Time",
.@"Etc/GMT+11" => "UTC-11",
.@"Etc/GMT+12" => "Dateline Standard Time",
.@"Etc/GMT+2" => "UTC-02",
.@"Etc/GMT+3" => "SA Eastern Standard Time",
.@"Etc/GMT+4" => "SA Western Standard Time",
.@"Etc/GMT+5" => "SA Pacific Standard Time",
.@"Etc/GMT+6" => "Central America Standard Time",
.@"Etc/GMT+7" => "US Mountain Standard Time",
.@"Etc/GMT+8" => "UTC-08",
.@"Etc/GMT+9" => "UTC-09",
.@"Etc/GMT-1" => "W. Central Africa Standard Time",
.@"Etc/GMT-10" => "West Pacific Standard Time",
.@"Etc/GMT-11" => "Central Pacific Standard Time",
.@"Etc/GMT-12" => "UTC+12",
.@"Etc/GMT-13" => "UTC+13",
.@"Etc/GMT-14" => "Line Islands Standard Time",
.@"Etc/GMT-2" => "South Africa Standard Time",
.@"Etc/GMT-3" => "E. Africa Standard Time",
.@"Etc/GMT-4" => "Arabian Standard Time",
.@"Etc/GMT-5" => "West Asia Standard Time",
.@"Etc/GMT-6" => "Central Asia Standard Time",
.@"Etc/GMT-7" => "SE Asia Standard Time",
.@"Etc/GMT-8" => "Singapore Standard Time",
.@"Etc/GMT-9" => "Tokyo Standard Time",
.@"Etc/UTC" => "UTC",
.@"Europe/Amsterdam" => "W. Europe Standard Time",
.@"Europe/Andorra" => "W. Europe Standard Time",
.@"Europe/Astrakhan" => "Astrakhan Standard Time",
.@"Europe/Athens" => "GTB Standard Time",
.@"Europe/Belgrade" => "Central Europe Standard Time",
.@"Europe/Berlin" => "W. Europe Standard Time",
.@"Europe/Bratislava" => "Central Europe Standard Time",
.@"Europe/Brussels" => "Romance Standard Time",
.@"Europe/Bucharest" => "GTB Standard Time",
.@"Europe/Budapest" => "Central Europe Standard Time",
.@"Europe/Busingen" => "W. Europe Standard Time",
.@"Europe/Chisinau" => "E. Europe Standard Time",
.@"Europe/Copenhagen" => "Romance Standard Time",
.@"Europe/Dublin" => "GMT Standard Time",
.@"Europe/Gibraltar" => "W. Europe Standard Time",
.@"Europe/Guernsey" => "GMT Standard Time",
.@"Europe/Helsinki" => "FLE Standard Time",
.@"Europe/Isle_of_Man" => "GMT Standard Time",
.@"Europe/Istanbul" => "Turkey Standard Time",
.@"Europe/Jersey" => "GMT Standard Time",
.@"Europe/Kaliningrad" => "Kaliningrad Standard Time",
.@"Europe/Kiev" => "FLE Standard Time",
.@"Europe/Kirov" => "Russian Standard Time",
.@"Europe/Lisbon" => "GMT Standard Time",
.@"Europe/Ljubljana" => "Central Europe Standard Time",
.@"Europe/London" => "GMT Standard Time",
.@"Europe/Luxembourg" => "W. Europe Standard Time",
.@"Europe/Madrid" => "Romance Standard Time",
.@"Europe/Malta" => "W. Europe Standard Time",
.@"Europe/Mariehamn" => "FLE Standard Time",
.@"Europe/Minsk" => "Belarus Standard Time",
.@"Europe/Monaco" => "W. Europe Standard Time",
.@"Europe/Moscow" => "Russian Standard Time",
.@"Europe/Oslo" => "W. Europe Standard Time",
.@"Europe/Paris" => "Romance Standard Time",
.@"Europe/Podgorica" => "Central Europe Standard Time",
.@"Europe/Prague" => "Central Europe Standard Time",
.@"Europe/Riga" => "FLE Standard Time",
.@"Europe/Rome" => "W. Europe Standard Time",
.@"Europe/Samara" => "Russia Time Zone 3",
.@"Europe/San_Marino" => "W. Europe Standard Time",
.@"Europe/Sarajevo" => "Central European Standard Time",
.@"Europe/Saratov" => "Saratov Standard Time",
.@"Europe/Simferopol" => "Russian Standard Time",
.@"Europe/Skopje" => "Central European Standard Time",
.@"Europe/Sofia" => "FLE Standard Time",
.@"Europe/Stockholm" => "W. Europe Standard Time",
.@"Europe/Tallinn" => "FLE Standard Time",
.@"Europe/Tirane" => "Central Europe Standard Time",
.@"Europe/Ulyanovsk" => "Astrakhan Standard Time",
.@"Europe/Vaduz" => "W. Europe Standard Time",
.@"Europe/Vatican" => "W. Europe Standard Time",
.@"Europe/Vienna" => "W. Europe Standard Time",
.@"Europe/Vilnius" => "FLE Standard Time",
.@"Europe/Volgograd" => "Volgograd Standard Time",
.@"Europe/Warsaw" => "Central European Standard Time",
.@"Europe/Zagreb" => "Central European Standard Time",
.@"Europe/Zurich" => "W. Europe Standard Time",
.@"Indian/Antananarivo" => "E. Africa Standard Time",
.@"Indian/Chagos" => "Central Asia Standard Time",
.@"Indian/Christmas" => "SE Asia Standard Time",
.@"Indian/Cocos" => "Myanmar Standard Time",
.@"Indian/Comoro" => "E. Africa Standard Time",
.@"Indian/Kerguelen" => "West Asia Standard Time",
.@"Indian/Mahe" => "Mauritius Standard Time",
.@"Indian/Maldives" => "West Asia Standard Time",
.@"Indian/Mauritius" => "Mauritius Standard Time",
.@"Indian/Mayotte" => "E. Africa Standard Time",
.@"Indian/Reunion" => "Mauritius Standard Time",
.@"Pacific/Apia" => "Samoa Standard Time",
.@"Pacific/Auckland" => "New Zealand Standard Time",
.@"Pacific/Bougainville" => "Bougainville Standard Time",
.@"Pacific/Chatham" => "Chatham Islands Standard Time",
.@"Pacific/Easter" => "Easter Island Standard Time",
.@"Pacific/Efate" => "Central Pacific Standard Time",
.@"Pacific/Enderbury" => "UTC+13",
.@"Pacific/Fakaofo" => "UTC+13",
.@"Pacific/Fiji" => "Fiji Standard Time",
.@"Pacific/Funafuti" => "UTC+12",
.@"Pacific/Galapagos" => "Central America Standard Time",
.@"Pacific/Gambier" => "UTC-09",
.@"Pacific/Guadalcanal" => "Central Pacific Standard Time",
.@"Pacific/Guam" => "West Pacific Standard Time",
.@"Pacific/Honolulu" => "Hawaiian Standard Time",
.@"Pacific/Kiritimati" => "Line Islands Standard Time",
.@"Pacific/Kosrae" => "Central Pacific Standard Time",
.@"Pacific/Kwajalein" => "UTC+12",
.@"Pacific/Majuro" => "UTC+12",
.@"Pacific/Marquesas" => "Marquesas Standard Time",
.@"Pacific/Midway" => "UTC-11",
.@"Pacific/Nauru" => "UTC+12",
.@"Pacific/Niue" => "UTC-11",
.@"Pacific/Norfolk" => "Norfolk Standard Time",
.@"Pacific/Noumea" => "Central Pacific Standard Time",
.@"Pacific/Pago_Pago" => "UTC-11",
.@"Pacific/Palau" => "Tokyo Standard Time",
.@"Pacific/Pitcairn" => "UTC-08",
.@"Pacific/Ponape" => "Central Pacific Standard Time",
.@"Pacific/Port_Moresby" => "West Pacific Standard Time",
.@"Pacific/Rarotonga" => "Hawaiian Standard Time",
.@"Pacific/Saipan" => "West Pacific Standard Time",
.@"Pacific/Tahiti" => "Hawaiian Standard Time",
.@"Pacific/Tarawa" => "UTC+12",
.@"Pacific/Tongatapu" => "Tonga Standard Time",
.@"Pacific/Truk" => "West Pacific Standard Time",
.@"Pacific/Wake" => "UTC+12",
.@"Pacific/Wallis" => "UTC+12",
};
}
};
================================================
FILE: src/timezone.zig
================================================
const std = @import("std");
const builtin = @import("builtin");
const zeit = @import("zeit.zig");
const assert = std.debug.assert;
const Month = zeit.Month;
const Seconds = zeit.Seconds;
const Weekday = zeit.Weekday;
const s_per_min = std.time.s_per_min;
const s_per_hour = std.time.s_per_hour;
const s_per_day = std.time.s_per_day;
pub const TimeZone = union(enum) {
fixed: Fixed,
posix: Posix,
tzinfo: TZInfo,
windows: switch (builtin.os.tag) {
.windows => Windows,
else => Noop,
},
pub fn adjust(self: TimeZone, timestamp: Seconds) AdjustedTime {
return switch (self) {
inline else => |tz| tz.adjust(timestamp),
};
}
pub fn deinit(self: TimeZone) void {
return switch (self) {
.fixed => {},
.posix => {},
.tzinfo => |tz| tz.deinit(),
.windows => |tz| tz.deinit(),
};
}
};
pub const AdjustedTime = struct {
designation: []const u8,
timestamp: Seconds,
is_dst: bool,
};
/// A Noop timezone we use for the windows struct when not on windows
pub const Noop = struct {
pub fn adjust(_: Noop, timestamp: Seconds) AdjustedTime {
return .{
.designation = "noop",
.timestamp = timestamp,
.is_dst = false,
};
}
pub fn deinit(_: Noop) void {}
};
/// A fixed timezone
pub const Fixed = struct {
name: []const u8,
offset: Seconds,
is_dst: bool,
pub fn adjust(self: Fixed, timestamp: Seconds) AdjustedTime {
return .{
.designation = self.name,
.timestamp = timestamp + self.offset,
.is_dst = self.is_dst,
};
}
};
/// A parsed representation of a Posix TZ string
/// std offset dst [offset],start[/time],end[/time]
/// std and dst can be quoted with <>
/// offsets and times can be [+-]hh[:mm[:ss]]
/// start and end are of the form J, or M..
pub const Posix = struct {
/// abbreviation for standard time
std: []const u8,
/// standard time offset in seconds
std_offset: Seconds,
/// abbreviation for daylight saving time
dst: ?[]const u8 = null,
/// offset when in dst, defaults to one hour less than std_offset if not present
dst_offset: ?Seconds = null,
start: ?DSTSpec = null,
end: ?DSTSpec = null,
const DSTSpec = union(enum) {
/// J: julian day between 1 and 365, Leap day is never counted even in leap
/// years
julian: struct {
day: u9,
time: Seconds = 7200,
},
/// : julian day between 0 and 365. Leap day counts
julian_leap: struct {
day: u9,
time: Seconds = 7200,
},
/// M..: day d of week w of month m. Day is 0 (sunday) to 6. week
/// is 1 to 5, where 5 would mean last d day of the month.
mwd: struct {
month: Month,
week: u6,
day: Weekday,
time: Seconds = 7200,
},
fn parse(str: []const u8) !DSTSpec {
assert(str.len > 0);
switch (str[0]) {
'J' => {
const julian = try std.fmt.parseInt(u9, str[1..], 10);
return .{ .julian = .{ .day = julian } };
},
'0', '1', '2', '3', '4', '5', '6', '7', '8', '9' => {
const julian = try std.fmt.parseInt(u9, str, 10);
return .{ .julian_leap = .{ .day = julian } };
},
'M' => {
var i: usize = 1;
const m_end = std.mem.indexOfScalarPos(u8, str, i, '.') orelse return error.InvalidPosix;
const month = try std.fmt.parseInt(u4, str[i..m_end], 10);
i = m_end + 1;
const w_end = std.mem.indexOfScalarPos(u8, str, i, '.') orelse return error.InvalidPosix;
const week = try std.fmt.parseInt(u6, str[i..w_end], 10);
i = w_end + 1;
const day = try std.fmt.parseInt(u3, str[i..], 10);
return .{
.mwd = .{
.month = @enumFromInt(month),
.week = week,
.day = @enumFromInt(day),
},
};
},
else => {},
}
return error.InvalidPosix;
}
};
pub fn parse(str: []const u8) !Posix {
var std_: []const u8 = "";
var std_offset: Seconds = 0;
var dst: ?[]const u8 = null;
var dst_offset: ?Seconds = null;
var start: ?DSTSpec = null;
var end: ?DSTSpec = null;
const State = enum {
std,
std_offset,
dst,
dst_offset,
start,
end,
};
var state: State = .std;
var i: usize = 0;
while (i < str.len) : (i += 1) {
switch (state) {
.std => {
switch (str[i]) {
'<' => {
// quoted. Consume until >
const end_qt = std.mem.indexOfScalar(u8, str[i..], '>') orelse return error.InvalidPosix;
std_ = str[i + 1 .. end_qt + i];
i = end_qt;
state = .std_offset;
},
else => {
i = std.mem.indexOfAnyPos(u8, str, i, "+-0123456789") orelse return error.InvalidPosix;
std_ = str[0..i];
// backup one so this gets parsed as an offset
i -= 1;
state = .std_offset;
},
}
},
.std_offset => {
const offset_start = i;
while (i < str.len) : (i += 1) {
switch (str[i]) {
'+',
'-',
':',
'0',
'1',
'2',
'3',
'4',
'5',
'6',
'7',
'8',
'9',
=> {
if (i == str.len - 1)
std_offset = parseTime(str[offset_start..]);
},
else => {
std_offset = parseTime(str[offset_start..i]);
i -= 1;
state = .dst;
break;
},
}
}
},
.dst => {
switch (str[i]) {
'<' => {
// quoted. Consume until >
const dst_start = i + 1;
i = std.mem.indexOfScalarPos(u8, str, i, '>') orelse return error.InvalidPosix;
dst = str[dst_start..i];
},
else => {
const dst_start = i;
i += 1;
while (i < str.len) : (i += 1) {
switch (str[i]) {
',' => {
dst = str[dst_start..i];
state = .start;
break;
},
'+', '-', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9' => {
dst = str[dst_start..i];
// backup one so this gets parsed as an offset
i -= 1;
state = .dst_offset;
break;
},
else => {
if (i == str.len - 1)
dst = str[dst_start..];
},
}
}
},
}
},
.dst_offset => {
const offset_start = i;
while (i < str.len) : (i += 1) {
switch (str[i]) {
'+',
'-',
':',
'0',
'1',
'2',
'3',
'4',
'5',
'6',
'7',
'8',
'9',
=> {
if (i == str.len - 1)
std_offset = parseTime(str[offset_start..]);
},
',' => {
dst_offset = parseTime(str[offset_start..i]);
state = .start;
break;
},
else => {},
}
}
},
.start => {
const comma_idx = std.mem.indexOfScalarPos(u8, str, i, ',') orelse return error.InvalidPosix;
if (std.mem.indexOfScalarPos(u8, str[0..comma_idx], i, '/')) |idx| {
start = try DSTSpec.parse(str[i..idx]);
switch (start.?) {
.julian => |*j| j.time = parseTime(str[idx + 1 .. comma_idx]),
.julian_leap => |*j| j.time = parseTime(str[idx + 1 .. comma_idx]),
.mwd => |*m| m.time = parseTime(str[idx + 1 .. comma_idx]),
}
} else {
start = try DSTSpec.parse(str[i..comma_idx]);
}
state = .end;
i = comma_idx;
},
.end => {
if (std.mem.indexOfScalarPos(u8, str, i, '/')) |idx| {
end = try DSTSpec.parse(str[i..idx]);
switch (end.?) {
.julian => |*j| j.time = parseTime(str[idx + 1 ..]),
.julian_leap => |*j| j.time = parseTime(str[idx + 1 ..]),
.mwd => |*m| m.time = parseTime(str[idx + 1 ..]),
}
} else {
end = try DSTSpec.parse(str[i..]);
}
break;
},
}
}
return .{
.std = std_,
.std_offset = std_offset,
.dst = dst,
.dst_offset = dst_offset,
.start = start,
.end = end,
};
}
fn parseTime(str: []const u8) Seconds {
const State = enum {
hour,
minute,
second,
};
var is_neg = false;
var state: State = .hour;
var offset_h: i64 = 0;
var offset_m: i64 = 0;
var offset_s: i64 = 0;
var i: usize = 0;
while (i < str.len) : (i += 1) {
switch (state) {
.hour => {
switch (str[i]) {
'-' => is_neg = true,
'0', '1', '2', '3', '4', '5', '6', '7', '8', '9' => |d| {
offset_h = offset_h * 10 + @as(i64, d - '0');
},
':' => state = .minute,
else => {},
}
},
.minute => {
switch (str[i]) {
'0', '1', '2', '3', '4', '5', '6', '7', '8', '9' => |d| {
offset_m = offset_m * 10 + @as(i64, d - '0');
},
':' => state = .second,
else => {},
}
},
.second => {
switch (str[i]) {
'0', '1', '2', '3', '4', '5', '6', '7', '8', '9' => |d| {
offset_s = offset_s * 10 + @as(i64, d - '0');
},
else => {},
}
},
}
}
const offset = offset_h * s_per_hour + offset_m * s_per_min + offset_s;
return if (is_neg) -offset else offset;
}
/// reports true if the unix timestamp occurs when DST is in effect
fn isDST(self: Posix, timestamp: Seconds) bool {
const start = self.start orelse return false;
const end = self.end orelse return false;
const days_from_epoch: zeit.Days = @intCast(@divFloor(timestamp, s_per_day));
const civil = zeit.civilFromDays(days_from_epoch);
const civil_month = @intFromEnum(civil.month);
const start_s: Seconds = switch (start) {
.julian => |rule| blk: {
const days = days_from_epoch - civil.month.daysBefore(civil.year) - civil.day + rule.day + 1;
var s = (@as(i64, days - 1)) * s_per_day + rule.time;
if (zeit.isLeapYear(civil.year) and rule.day >= 60) {
s += s_per_day;
}
break :blk s + self.std_offset;
},
.julian_leap => |rule| blk: {
const days = days_from_epoch - civil.month.daysBefore(civil.year) - civil.day + rule.day;
break :blk @as(i64, days) * s_per_day + rule.time + self.std_offset;
},
.mwd => |rule| blk: {
const rule_month = @intFromEnum(rule.month);
if (civil_month < rule_month) return false;
// bail early if we are greater than this month. we know we only
// rely on the end time. We yield a value that is before the
// timestamp
if (civil_month > rule_month) break :blk timestamp - 1;
// we are in the same month
// first_of_month is the weekday on the first of the month
const first_of_month = zeit.weekdayFromDays(days_from_epoch - civil.day + 1);
// days is the first "rule day" of the month (ie the first
// Sunday of the month)
var days: u9 = first_of_month.daysUntil(rule.day) + 1;
var i: usize = 1;
while (i < rule.week) : (i += 1) {
if (days + 7 >= rule.month.lastDay(civil.year)) break;
days += 7;
}
// days_from_epoch is the number of days to the DST day from the
// epoch
const dst_days_from_epoch: i64 = days_from_epoch - civil.day + days;
break :blk @as(i64, dst_days_from_epoch) * s_per_day + rule.time + self.std_offset;
},
};
const end_s: Seconds = switch (end) {
.julian => |rule| blk: {
const days = days_from_epoch - civil.month.daysBefore(civil.year) - civil.day + rule.day + 1;
var s = (@as(i64, days) - 1) * s_per_day + rule.time;
if (zeit.isLeapYear(civil.year) and rule.day >= 60) {
s += s_per_day;
}
break :blk s + self.std_offset;
},
.julian_leap => |rule| blk: {
const days = days_from_epoch - civil.month.daysBefore(civil.year) - civil.day + rule.day + 1;
break :blk @as(i64, days) * s_per_day + rule.time + self.std_offset;
},
.mwd => |rule| blk: {
const rule_month = @intFromEnum(rule.month);
if (civil_month > rule_month) return false;
// bail early if we are less than this month. we know we only
// rely on the start time. We yield a value that is after the
// timestamp
if (civil_month < rule_month) break :blk timestamp + 1;
// first_of_month is the weekday on the first of the month
const first_of_month = zeit.weekdayFromDays(days_from_epoch - civil.day + 1);
// days is the first "rule day" of the month (ie the first
// Sunday of the month)
var days: u9 = first_of_month.daysUntil(rule.day) + 1;
var i: usize = 1;
while (i < rule.week) : (i += 1) {
if (days + 7 >= rule.month.lastDay(civil.year)) break;
days += 7;
}
// days_from_epoch is the number of days to the DST day from the
// epoch
const dst_days_from_epoch: i64 = days_from_epoch - civil.day + days;
break :blk @as(i64, dst_days_from_epoch) * s_per_day + rule.time + self.std_offset;
},
};
return timestamp >= start_s and timestamp < end_s;
}
pub fn adjust(self: Posix, timestamp: Seconds) AdjustedTime {
if (self.isDST(timestamp)) {
return .{
.designation = self.dst orelse self.std,
.timestamp = timestamp - (self.dst_offset orelse self.std_offset - s_per_hour),
.is_dst = true,
};
}
return .{
.designation = self.std,
.timestamp = timestamp - self.std_offset,
.is_dst = false,
};
}
};
pub const TZInfo = struct {
allocator: std.mem.Allocator,
transitions: []const Transition,
timetypes: []const Timetype,
leapseconds: []const Leapsecond,
footer: ?[]const u8,
posix_tz: ?Posix,
const Leapsecond = struct {
occurrence: i48,
correction: i16,
};
const Timetype = struct {
offset: i32,
flags: u8,
name_data: [6:0]u8,
pub fn name(self: *const Timetype) [:0]const u8 {
return std.mem.sliceTo(self.name_data[0..], 0);
}
pub fn isDst(self: Timetype) bool {
return (self.flags & 0x01) > 0;
}
pub fn standardTimeIndicator(self: Timetype) bool {
return (self.flags & 0x02) > 0;
}
pub fn utIndicator(self: Timetype) bool {
return (self.flags & 0x04) > 0;
}
};
const Transition = struct {
ts: Seconds,
timetype: *Timetype,
};
const Header = extern struct {
magic: [4]u8,
version: u8,
reserved: [15]u8,
counts: extern struct {
isutcnt: u32,
isstdcnt: u32,
leapcnt: u32,
timecnt: u32,
typecnt: u32,
charcnt: u32,
},
};
pub fn parse(allocator: std.mem.Allocator, reader: *std.Io.Reader) !TZInfo {
var legacy_header = try reader.takeStruct(Header, .big); // handles endianness for us
if (!std.mem.eql(u8, &legacy_header.magic, "TZif")) return error.BadHeader;
if (legacy_header.version != 0 and legacy_header.version != '2' and legacy_header.version != '3') return error.BadVersion;
if (legacy_header.version == 0) {
return parseBlock(allocator, reader, legacy_header, true);
} else {
// If the format is modern, just skip over the legacy data
const skipv = legacy_header.counts.timecnt * 5 + legacy_header.counts.typecnt * 6 + legacy_header.counts.charcnt + legacy_header.counts.leapcnt * 8 + legacy_header.counts.isstdcnt + legacy_header.counts.isutcnt;
const skipped = try reader.discard(.limited(skipv));
// this should be unreachable, as discard above handles EndOfStream and ReadFailed:
if (skipped != skipv) return error.BadHeader;
var header = try reader.takeStruct(Header, .big); // handles endianness for us
if (!std.mem.eql(u8, &header.magic, "TZif")) return error.BadHeader;
if (header.version != '2' and header.version != '3') return error.BadVersion;
return parseBlock(allocator, reader, header, false);
}
}
fn parseBlock(allocator: std.mem.Allocator, reader: *std.Io.Reader, header: Header, legacy: bool) !TZInfo {
if (header.counts.isstdcnt != 0 and header.counts.isstdcnt != header.counts.typecnt) return error.Malformed; // rfc8536: isstdcnt [...] MUST either be zero or equal to "typecnt"
if (header.counts.isutcnt != 0 and header.counts.isutcnt != header.counts.typecnt) return error.Malformed; // rfc8536: isutcnt [...] MUST either be zero or equal to "typecnt"
if (header.counts.typecnt == 0) return error.Malformed; // rfc8536: typecnt [...] MUST NOT be zero
if (header.counts.charcnt == 0) return error.Malformed; // rfc8536: charcnt [...] MUST NOT be zero
if (header.counts.charcnt > 256 + 6) return error.Malformed; // Not explicitly banned by rfc8536 but nonsensical
var leapseconds = try allocator.alloc(Leapsecond, header.counts.leapcnt);
errdefer allocator.free(leapseconds);
var transitions = try allocator.alloc(Transition, header.counts.timecnt);
errdefer allocator.free(transitions);
var timetypes = try allocator.alloc(Timetype, header.counts.typecnt);
errdefer allocator.free(timetypes);
// Parse transition types
var i: usize = 0;
while (i < header.counts.timecnt) : (i += 1) {
transitions[i].ts = if (legacy) try reader.takeInt(i32, .big) else try reader.takeInt(i64, .big);
}
i = 0;
while (i < header.counts.timecnt) : (i += 1) {
const tt = try reader.takeByte();
if (tt >= timetypes.len) return error.Malformed; // rfc8536: Each type index MUST be in the range [0, "typecnt" - 1]
transitions[i].timetype = &timetypes[tt];
}
// Parse time types
i = 0;
while (i < header.counts.typecnt) : (i += 1) {
const offset = try reader.takeInt(i32, .big);
if (offset < -2147483648) return error.Malformed; // rfc8536: utoff [...] MUST NOT be -2**31
const dst = try reader.takeByte();
if (dst != 0 and dst != 1) return error.Malformed; // rfc8536: (is)dst [...] The value MUST be 0 or 1.
const idx = try reader.takeByte();
if (idx > header.counts.charcnt - 1) return error.Malformed; // rfc8536: (desig)idx [...] Each index MUST be in the range [0, "charcnt" - 1]
timetypes[i] = .{
.offset = offset,
.flags = dst,
.name_data = undefined,
};
// Temporarily cache idx in name_data to be processed after we've read the designator names below
timetypes[i].name_data[0] = idx;
}
var designators_data: [256 + 6]u8 = undefined;
try reader.readSliceAll(designators_data[0..header.counts.charcnt]);
const designators = designators_data[0..header.counts.charcnt];
if (designators[designators.len - 1] != 0) return error.Malformed; // rfc8536: charcnt [...] includes the trailing NUL (0x00) octet
// Iterate through the timetypes again, setting the designator names
for (timetypes) |*tt| {
const name = std.mem.sliceTo(designators[tt.name_data[0]..], 0);
// We are mandating the "SHOULD" 6-character limit so we can pack the struct better, and to conform to POSIX.
if (name.len > 6) return error.Malformed; // rfc8536: Time zone designations SHOULD consist of at least three (3) and no more than six (6) ASCII characters.
@memcpy(tt.name_data[0..name.len], name);
tt.name_data[name.len] = 0;
}
// Parse leap seconds
i = 0;
while (i < header.counts.leapcnt) : (i += 1) {
const occur: i64 = if (legacy) try reader.takeInt(i32, .big) else try reader.takeInt(i64, .big);
if (occur < 0) return error.Malformed; // rfc8536: occur [...] MUST be nonnegative
if (i > 0 and leapseconds[i - 1].occurrence + 2419199 > occur) return error.Malformed; // rfc8536: occur [...] each later value MUST be at least 2419199 greater than the previous value
if (occur > std.math.maxInt(i48)) return error.Malformed; // Unreasonably far into the future
const corr = try reader.takeInt(i32, .big);
if (i == 0 and corr != -1 and corr != 1) return error.Malformed; // rfc8536: The correction value in the first leap-second record, if present, MUST be either one (1) or minus one (-1)
if (i > 0 and leapseconds[i - 1].correction != corr + 1 and leapseconds[i - 1].correction != corr - 1) return error.Malformed; // rfc8536: The correction values in adjacent leap-second records MUST differ by exactly one (1)
if (corr > std.math.maxInt(i16)) return error.Malformed; // Unreasonably large correction
leapseconds[i] = .{
.occurrence = @as(i48, @intCast(occur)),
.correction = @as(i16, @intCast(corr)),
};
}
// Parse standard/wall indicators
i = 0;
while (i < header.counts.isstdcnt) : (i += 1) {
const stdtime = try reader.takeByte();
if (stdtime == 1) {
timetypes[i].flags |= 0x02;
}
}
// Parse UT/local indicators
i = 0;
while (i < header.counts.isutcnt) : (i += 1) {
const ut = try reader.takeByte();
if (ut == 1) {
timetypes[i].flags |= 0x04;
if (!timetypes[i].standardTimeIndicator()) return error.Malformed; // rfc8536: standard/wall value MUST be one (1) if the UT/local value is one (1)
}
}
// Footer
var footer: ?[]const u8 = null;
var posix: ?Posix = null;
if (!legacy) {
if ((try reader.takeByte()) != '\n') return error.Malformed; // An rfc8536 footer must start with a newline
const footer_mem = reader.takeDelimiterExclusive('\n') catch |err| switch (err) {
error.StreamTooLong => return error.OverlargeFooter, // Read more than reader buffer bytes, much larger than any reasonable POSIX TZ string
else => return err,
};
if (footer_mem.len != 0) {
footer = try allocator.dupe(u8, footer_mem);
posix = try Posix.parse(footer.?);
}
}
errdefer if (footer) |ft| allocator.free(ft);
return .{
.allocator = allocator,
.transitions = transitions,
.timetypes = timetypes,
.leapseconds = leapseconds,
.footer = footer,
.posix_tz = posix,
};
}
pub fn deinit(self: TZInfo) void {
if (self.footer) |footer| {
self.allocator.free(footer);
}
self.allocator.free(self.leapseconds);
self.allocator.free(self.transitions);
self.allocator.free(self.timetypes);
}
/// adjust a unix timestamp to the timezone
pub fn adjust(self: TZInfo, timestamp: Seconds) AdjustedTime {
// if we are past the last transition and have a footer, we use the
// footer data
if ((self.transitions.len == 0 or self.transitions[self.transitions.len - 1].ts <= timestamp) and
self.posix_tz != null)
{
const posix = self.posix_tz.?;
return posix.adjust(timestamp);
}
const transition: Transition = blk: for (self.transitions, 0..) |transition, i| {
// TODO: implement what go does, which is a copy of c for how to
// handle times before the first transition how to handle this
if (i == 0 and transition.ts > timestamp) @panic("unimplemented. please complain to tim");
if (transition.ts <= timestamp) continue;
// we use the latest transition before ts, which is one less than
// our current iter
break :blk self.transitions[i - 1];
} else self.transitions[self.transitions.len - 1];
return .{
.designation = transition.timetype.name(),
.timestamp = timestamp + transition.timetype.offset,
.is_dst = transition.timetype.isDst(),
};
}
};
pub const Windows = struct {
const windows = struct {
const BOOL = std.os.windows.BOOL;
const BOOLEAN = std.os.windows.BOOLEAN;
const DWORD = std.os.windows.DWORD;
const FILETIME = std.os.windows.FILETIME;
const LONG = std.os.windows.LONG;
const USHORT = std.os.windows.USHORT;
const WCHAR = std.os.windows.WCHAR;
const WORD = std.os.windows.WORD;
const epoch = std.time.epoch.windows;
const ERROR_SUCCESS = 0x00;
const ERROR_NO_MORE_ITEMS = 0x103;
pub const TIME_ZONE_ID_INVALID = @as(DWORD, std.math.maxInt(DWORD));
const DYNAMIC_TIME_ZONE_INFORMATION = extern struct {
Bias: LONG,
StandardName: [32]WCHAR,
StandardDate: SYSTEMTIME,
StandardBias: LONG,
DaylightName: [32]WCHAR,
DaylightDate: SYSTEMTIME,
DaylightBias: LONG,
TimeZoneKeyName: [128]WCHAR,
DynamicDaylightTimeDisabled: BOOLEAN,
};
const SYSTEMTIME = extern struct {
wYear: WORD,
wMonth: WORD,
wDayOfWeek: WORD,
wDay: WORD,
wHour: WORD,
wMinute: WORD,
wSecond: WORD,
wMilliseconds: WORD,
};
const TIME_ZONE_INFORMATION = extern struct {
Bias: LONG,
StandardName: [32]WCHAR,
StandardDate: SYSTEMTIME,
StandardBias: LONG,
DaylightName: [32]WCHAR,
DaylightDate: SYSTEMTIME,
DaylightBias: LONG,
};
pub extern "advapi32" fn EnumDynamicTimeZoneInformation(dwIndex: DWORD, lpTimeZoneInformation: *DYNAMIC_TIME_ZONE_INFORMATION) callconv(.winapi) DWORD;
pub extern "kernel32" fn GetDynamicTimeZoneInformation(pTimeZoneInformation: *DYNAMIC_TIME_ZONE_INFORMATION) callconv(.winapi) DWORD;
pub extern "kernel32" fn GetTimeZoneInformationForYear(wYear: USHORT, pdtzi: ?*const DYNAMIC_TIME_ZONE_INFORMATION, ptzi: *TIME_ZONE_INFORMATION) callconv(.winapi) BOOL;
pub extern "kernel32" fn SystemTimeToTzSpecificLocalTimeEx(lpTimeZoneInfo: ?*const DYNAMIC_TIME_ZONE_INFORMATION, lpUniversalTime: *const SYSTEMTIME, lpLocalTime: *SYSTEMTIME) callconv(.winapi) BOOL;
};
zoneinfo: windows.DYNAMIC_TIME_ZONE_INFORMATION,
allocator: std.mem.Allocator,
standard_name: []const u8,
dst_name: []const u8,
/// retrieves the local timezone settings for this machine
pub fn local(allocator: std.mem.Allocator) !Windows {
var info: windows.DYNAMIC_TIME_ZONE_INFORMATION = undefined;
const result = windows.GetDynamicTimeZoneInformation(&info);
if (result == windows.TIME_ZONE_ID_INVALID) return error.TimeZoneIdInvalid;
const std_idx = std.mem.indexOfScalar(u16, &info.StandardName, 0x00) orelse info.StandardName.len;
const dst_idx = std.mem.indexOfScalar(u16, &info.DaylightName, 0x00) orelse info.DaylightName.len;
const standard_name = try std.unicode.utf16LeToUtf8Alloc(allocator, info.StandardName[0..std_idx]);
const dst_name = try std.unicode.utf16LeToUtf8Alloc(allocator, info.DaylightName[0..dst_idx]);
return .{
.zoneinfo = info,
.allocator = allocator,
.standard_name = standard_name,
.dst_name = dst_name,
};
}
pub fn deinit(self: Windows) void {
self.allocator.free(self.standard_name);
self.allocator.free(self.dst_name);
}
/// Adjusts the time to the timezone
/// 1. Convert timestamp to windows.SYSTEMTIME using internal methods
/// 2. Convert SYSTEMTIME to target timezone using windows api
/// 3. Get the relevant TIME_ZONE_INFORMATION for the year
/// 4. Determine if we are in DST or not
/// 5. Return result
pub fn adjust(self: Windows, timestamp: Seconds) AdjustedTime {
const instant = zeit.instant(.{ .unix_timestamp = timestamp }, &zeit.utc);
const time = instant.time();
const systemtime: windows.SYSTEMTIME = .{
.wYear = @intCast(time.year),
.wMonth = @intFromEnum(time.month),
.wDayOfWeek = 0, // not used in calculation
.wDay = time.day,
.wHour = time.hour,
.wMinute = time.minute,
.wSecond = time.second,
.wMilliseconds = time.millisecond,
};
var localtime: windows.SYSTEMTIME = undefined;
if (windows.SystemTimeToTzSpecificLocalTimeEx(&self.zoneinfo, &systemtime, &localtime) == .FALSE) {
const err = std.os.windows.GetLastError();
std.log.err("{}", .{err});
@panic("TODO");
}
var tzi: windows.TIME_ZONE_INFORMATION = undefined;
if (windows.GetTimeZoneInformationForYear(localtime.wYear, &self.zoneinfo, &tzi) == .FALSE) {
const err = std.os.windows.GetLastError();
std.log.err("{}", .{err});
@panic("TODO");
}
const is_dst = isDST(timestamp, &tzi, &localtime);
return .{
.designation = if (is_dst) self.dst_name else self.standard_name,
.timestamp = systemtimeToUnixTimestamp(localtime),
.is_dst = is_dst,
};
}
fn systemtimeToUnixTimestamp(sys: windows.SYSTEMTIME) Seconds {
const lzt = systemtimetoZeitTime(sys);
return lzt.instant().unixTimestamp();
}
fn systemtimetoZeitTime(sys: windows.SYSTEMTIME) zeit.Time {
return .{
.year = sys.wYear,
.month = @enumFromInt(sys.wMonth),
.day = @intCast(sys.wDay),
.hour = @intCast(sys.wHour),
.minute = @intCast(sys.wMinute),
.second = @intCast(sys.wSecond),
.millisecond = @intCast(sys.wMilliseconds),
};
}
fn isDST(timestamp: Seconds, tzi: *const windows.TIME_ZONE_INFORMATION, time: *const windows.SYSTEMTIME) bool {
// If wMonth on StandardDate is 0, the timezone doesn't have DST
if (tzi.StandardDate.wMonth == 0) return false;
const start = tzi.DaylightDate;
const end = tzi.StandardDate;
// Before DST starts
if (time.wMonth < start.wMonth) return false;
// After DST ends
if (time.wMonth > end.wMonth) return false;
// In the months between
if (time.wMonth > start.wMonth and time.wMonth < end.wMonth) return true;
const days_from_epoch: zeit.Days = @intCast(@divFloor(timestamp, s_per_day));
// first_of_month is the weekday on the first of the month
const first_of_month = zeit.weekdayFromDays(
days_from_epoch - @as(zeit.Days, @intCast(time.wDay)) + 1,
);
// In the start transition month
if (time.wMonth == start.wMonth) {
// days is the first "rule day" of the month (ie the first
// Sunday of the month)
var days: u9 = first_of_month.daysUntil(@enumFromInt(start.wDayOfWeek)) + 1;
var i: usize = 1;
while (i < start.wDay) : (i += 1) {
const month: zeit.Month = @enumFromInt(start.wMonth);
if (days + 7 >= month.lastDay(time.wYear)) break;
days += 7;
}
if (time.wDay == days) {
if (time.wHour == start.wHour) {
return time.wMinute >= start.wMinute;
}
return time.wHour >= start.wHour;
}
return time.wDay >= days;
}
// In the end transition month
if (time.wMonth == end.wMonth) {
// days is the first "rule day" of the month (ie the first
// Sunday of the month)
var days: u9 = first_of_month.daysUntil(@enumFromInt(end.wDayOfWeek)) + 1;
var i: usize = 1;
while (i < end.wDay) : (i += 1) {
const month: zeit.Month = @enumFromInt(end.wMonth);
if (days + 7 >= month.lastDay(time.wYear)) break;
days += 7;
}
if (time.wDay == days) {
if (time.wHour == end.wHour) {
return time.wMinute < end.wMinute;
}
return time.wHour < end.wHour;
}
return time.wDay < days;
}
return false;
}
pub fn loadFromName(allocator: std.mem.Allocator, name: []const u8) !Windows {
var buf: [128]u16 = undefined;
const n = try std.unicode.utf8ToUtf16Le(&buf, name);
const target = buf[0..n];
var result: windows.DWORD = windows.ERROR_SUCCESS;
var i: windows.DWORD = 0;
var dtzi: windows.DYNAMIC_TIME_ZONE_INFORMATION = undefined;
while (result == windows.ERROR_SUCCESS) : (i += 1) {
result = windows.EnumDynamicTimeZoneInformation(i, &dtzi);
const name_idx = std.mem.indexOfScalar(u16, &dtzi.TimeZoneKeyName, 0x00) orelse dtzi.TimeZoneKeyName.len;
if (std.mem.eql(u16, target, dtzi.TimeZoneKeyName[0..name_idx])) break;
} else return error.TimezoneNotFound;
const std_idx = std.mem.indexOfScalar(u16, &dtzi.StandardName, 0x00) orelse dtzi.StandardName.len;
const dst_idx = std.mem.indexOfScalar(u16, &dtzi.DaylightName, 0x00) orelse dtzi.DaylightName.len;
const standard_name = try std.unicode.utf16LeToUtf8Alloc(allocator, dtzi.StandardName[0..std_idx]);
const dst_name = try std.unicode.utf16LeToUtf8Alloc(allocator, dtzi.DaylightName[0..dst_idx]);
return .{
.zoneinfo = dtzi,
.allocator = allocator,
.standard_name = standard_name,
.dst_name = dst_name,
};
}
};
test "timezone.zig: test Fixed" {
const fixed: Fixed = .{
.name = "test",
.offset = -600,
.is_dst = false,
};
const adjusted = fixed.adjust(0);
try std.testing.expectEqual(-600, adjusted.timestamp);
}
test "timezone.zig: Posix.isDST" {
const t = try Posix.parse("CST6CDT,M3.2.0,M11.1.0");
try std.testing.expectEqual(false, t.isDST(1704088800)); // Jan 1 2024 00:00:00 CST
try std.testing.expectEqual(false, t.isDST(1733032800)); // Dec 1 2024 00:00:00 CST
try std.testing.expectEqual(true, t.isDST(1717218000)); // Jun 1 2024 00:00:00 CST
// One second after DST starts
try std.testing.expectEqual(true, t.isDST(1710057601));
// One second before DST starts
try std.testing.expectEqual(false, t.isDST(1710057599));
// One second before DST ends
try std.testing.expectEqual(true, t.isDST(1730620799));
// One second after DST ends
try std.testing.expectEqual(false, t.isDST(1730620801));
const j = try Posix.parse("CST6CDT,J1,J4");
try std.testing.expectEqual(true, j.isDST(1704268800));
}
test "timezone.zig: Posix.parseTime" {
try std.testing.expectEqual(0, Posix.parseTime("00:00:00"));
try std.testing.expectEqual(-3600, Posix.parseTime("-1"));
try std.testing.expectEqual(-7200, Posix.parseTime("-02:00:00"));
try std.testing.expectEqual(3660, Posix.parseTime("+1:01"));
}
test "timezone.zig: Posix.parse" {
{
const t = try Posix.parse("-1");
try std.testing.expectEqualStrings("UTC", t.std);
try std.testing.expectEqual(-3600, t.std_offset);
}
{
const t = try Posix.parse("1");
try std.testing.expectEqualStrings("UTC", t.std);
try std.testing.expectEqual(3600, t.std_offset);
}
{
const t = try Posix.parse("+1");
try std.testing.expectEqualStrings("UTC", t.std);
try std.testing.expectEqual(3600, t.std_offset);
}
{
const t = try Posix.parse("UTC+1");
try std.testing.expectEqualStrings("UTC", t.std);
try std.testing.expectEqual(3600, t.std_offset);
}
{
const t = try Posix.parse("UTC+1:01");
try std.testing.expectEqualStrings("UTC", t.std);
try std.testing.expectEqual(3660, t.std_offset);
}
{
const t = try Posix.parse("UTC-1:01:01");
try std.testing.expectEqualStrings("UTC", t.std);
try std.testing.expectEqual(-3661, t.std_offset);
}
{
const t = try Posix.parse("CST1CDT");
try std.testing.expectEqualStrings("CST", t.std);
try std.testing.expectEqual(3600, t.std_offset);
try std.testing.expectEqualStrings("CDT", t.dst.?);
}
{
const t = try Posix.parse("CST1");
try std.testing.expectEqualStrings("CST", t.std);
try std.testing.expectEqual(3600, t.std_offset);
try std.testing.expectEqualStrings("CDT", t.dst.?);
}
{
const t = try Posix.parse("CST1CDT,J100,J200");
try std.testing.expectEqualStrings("CST", t.std);
try std.testing.expectEqual(3600, t.std_offset);
try std.testing.expectEqualStrings("CDT", t.dst.?);
try std.testing.expectEqual(100, t.start.?.julian.day);
try std.testing.expectEqual(200, t.end.?.julian.day);
}
{
const t = try Posix.parse("CST1CDT,100,200");
try std.testing.expectEqualStrings("CST", t.std);
try std.testing.expectEqual(3600, t.std_offset);
try std.testing.expectEqualStrings("CDT", t.dst.?);
try std.testing.expectEqual(100, t.start.?.julian_leap.day);
try std.testing.expectEqual(200, t.end.?.julian_leap.day);
}
{
const t = try Posix.parse("CST1CDT,M3.5.1,M11.3.0");
try std.testing.expectEqualStrings("CST", t.std);
try std.testing.expectEqual(3600, t.std_offset);
try std.testing.expectEqualStrings("CDT", t.dst.?);
try std.testing.expectEqual(.mar, t.start.?.mwd.month);
try std.testing.expectEqual(5, t.start.?.mwd.week);
try std.testing.expectEqual(.mon, t.start.?.mwd.day);
try std.testing.expectEqual(.nov, t.end.?.mwd.month);
try std.testing.expectEqual(3, t.end.?.mwd.week);
try std.testing.expectEqual(.sun, t.end.?.mwd.day);
}
{
const t = try Posix.parse("CST1CDT,M3.5.1/02:00:00,M11.3.0/1");
try std.testing.expectEqualStrings("CST", t.std);
try std.testing.expectEqual(3600, t.std_offset);
try std.testing.expectEqualStrings("CDT", t.dst.?);
try std.testing.expectEqual(.mar, t.start.?.mwd.month);
try std.testing.expectEqual(5, t.start.?.mwd.week);
try std.testing.expectEqual(.mon, t.start.?.mwd.day);
try std.testing.expectEqual(7200, t.start.?.mwd.time);
try std.testing.expectEqual(.nov, t.end.?.mwd.month);
try std.testing.expectEqual(3, t.end.?.mwd.week);
try std.testing.expectEqual(.sun, t.end.?.mwd.day);
try std.testing.expectEqual(3600, t.end.?.mwd.time);
}
}
test "timezone.zig: Posix.adjust" {
{
const t = try Posix.parse("UTC+1");
const adjusted = t.adjust(0);
try std.testing.expectEqual(-3600, adjusted.timestamp);
}
{
const t = try Posix.parse("CST6CDT,M3.2.0/2:00:00,M11.1.0/2:00:00");
const adjusted = t.adjust(1704088800);
try std.testing.expectEqual(1704067200, adjusted.timestamp);
try std.testing.expectEqualStrings("CST", adjusted.designation);
const adjusted_dst = t.adjust(1710057600);
try std.testing.expectEqual(1710039600, adjusted_dst.timestamp);
try std.testing.expectEqualStrings("CDT", adjusted_dst.designation);
}
}
test "timezone.zig: Posix.DSTSpec.parse" {
{
const spec = try Posix.DSTSpec.parse("J365");
try std.testing.expectEqual(365, spec.julian.day);
}
{
const spec = try Posix.DSTSpec.parse("365");
try std.testing.expectEqual(365, spec.julian_leap.day);
}
{
const spec = try Posix.DSTSpec.parse("M3.5.1");
try std.testing.expectEqual(.mar, spec.mwd.month);
try std.testing.expectEqual(5, spec.mwd.week);
try std.testing.expectEqual(.mon, spec.mwd.day);
}
{
const spec = try Posix.DSTSpec.parse("M11.3.0");
try std.testing.expectEqual(.nov, spec.mwd.month);
try std.testing.expectEqual(3, spec.mwd.week);
try std.testing.expectEqual(.sun, spec.mwd.day);
}
}
================================================
FILE: src/zeit.zig
================================================
const std = @import("std");
const builtin = @import("builtin");
const location = @import("location.zig");
pub const timezone = @import("timezone.zig");
const assert = std.debug.assert;
pub const TimeZone = timezone.TimeZone;
pub const Location = location.Location;
pub const Days = i32;
pub const Nanoseconds = i128;
pub const Milliseconds = i128;
pub const Seconds = i64;
pub const EnvConfig = struct {
tz: ?[]const u8 = null,
tzdir: ?[]const u8 = null,
};
const ns_per_us = std.time.ns_per_us;
const ns_per_ms = std.time.ns_per_ms;
const ns_per_s = std.time.ns_per_s;
const ns_per_min = std.time.ns_per_min;
const ns_per_hour = std.time.ns_per_hour;
const ns_per_day = std.time.ns_per_day;
const s_per_min = std.time.s_per_min;
const s_per_hour = std.time.s_per_hour;
const s_per_day = std.time.s_per_day;
const days_per_era = 365 * 400 + 97;
pub const utc: TimeZone = .{ .fixed = .{
.name = "UTC",
.offset = 0,
.is_dst = false,
} };
pub fn local(alloc: std.mem.Allocator, io: std.Io, env: EnvConfig) !TimeZone {
switch (builtin.os.tag) {
.windows => {
const win = try timezone.Windows.local(alloc);
return .{ .windows = win };
},
else => {
if (env.tz) |tz| {
return localFromEnv(alloc, io, tz, env);
}
const f = try std.Io.Dir.cwd().openFile(io, "/etc/localtime", .{});
defer f.close(io);
var io_buffer: [2048]u8 = undefined;
var reader = f.reader(io, &io_buffer);
return .{ .tzinfo = try timezone.TZInfo.parse(alloc, &reader.interface) };
},
}
}
// Returns the local time zone from the given TZ environment variable
// TZ can be one of three things:
// 1. A POSIX TZ string (TZ=CST6CDT,M3.2.0,M11.1.0)
// 2. An absolute path, prefixed with ':' (TZ=:/etc/localtime)
// 3. A relative path, prefixed with ':'
fn localFromEnv(
alloc: std.mem.Allocator,
io: std.Io,
tz: []const u8,
env: EnvConfig,
) !TimeZone {
assert(tz.len != 0); // TZ is empty string
// Return early we we are a posix TZ string
if (tz[0] != ':') return .{ .posix = try timezone.Posix.parse(tz) };
assert(tz.len > 1); // TZ not long enough
if (tz[1] == '/') {
const f = try std.Io.Dir.cwd().openFile(io, tz[1..], .{});
defer f.close(io);
var io_buffer: [1024]u8 = undefined;
var reader = f.reader(io, &io_buffer);
return .{ .tzinfo = try timezone.TZInfo.parse(alloc, &reader.interface) };
}
if (std.meta.stringToEnum(Location, tz[1..])) |loc|
return loadTimeZone(alloc, io, loc, env)
else
return error.UnknownLocation;
}
pub fn loadTimeZone(
alloc: std.mem.Allocator,
io: std.Io,
loc: Location,
env: EnvConfig,
) !TimeZone {
switch (builtin.os.tag) {
.windows => {
const tz = try timezone.Windows.loadFromName(alloc, loc.asText());
return .{ .windows = tz };
},
else => {},
}
var dir: std.Io.Dir = blk: {
// If we have an env and a TZDIR, use that
if (env.tzdir) |tzdir| {
const d = try std.Io.Dir.cwd().openDir(io, tzdir, .{});
break :blk d;
}
// Otherwise check well-known locations
const zone_dirs = [_][]const u8{
"/usr/share/zoneinfo/",
"/usr/share/lib/zoneinfo/",
"/usr/lib/locale/TZ/",
"/share/zoneinfo/",
"/etc/zoneinfo/",
};
for (zone_dirs) |zone_dir| {
const d = std.Io.Dir.cwd().openDir(io, zone_dir, .{}) catch continue;
break :blk d;
} else return error.FileNotFound;
};
defer dir.close(io);
const f = try dir.openFile(io, loc.asText(), .{});
defer f.close(io);
var io_buffer: [2048]u8 = undefined;
var reader = f.reader(io, &io_buffer);
return .{ .tzinfo = try timezone.TZInfo.parse(alloc, &reader.interface) };
}
/// An Instant in time. Instants occur at a precise time and place, thus must
/// always carry with them a timezone.
pub const Instant = struct {
/// the instant of time, in nanoseconds
timestamp: Nanoseconds = 0,
/// every instant occurs in a timezone. This is the timezone
timezone: *const TimeZone,
/// possible sources to create an Instant
pub const Source = union(enum) {
/// current wall clock determined using std.Io
now: std.Io,
/// a specific unix timestamp (in seconds)
unix_timestamp: Seconds,
/// a specific unix timestamp (in nanoseconds)
unix_nano: Nanoseconds,
/// create an Instant from a calendar date and time
time: Time,
};
/// text time format for parsing
pub const TextFormat = enum {
/// parse a datetime from an ISO8601 string
/// Supports most ISO8601 formats, _except_:
/// - Week numbers (ie YYYY-Www)
/// - Fractional minutes (ie YYYY-MM-DDTHH:MM.mmm)
///
/// Strings can be in the extended or compact format and use ' ' or "T"
/// as the time delimiter
/// Examples of paresable strings:
/// YYYY-MM-DD
/// YYYY-MM-DDTHH
/// YYYY-MM-DDTHH:MM
/// YYYY-MM-DDTHH:MM:SS
/// YYYY-MM-DDTHH:MM:SS.sss
/// YYYY-MM-DDTHH:MM:SS.ssssss
/// YYYY-MM-DDTHH:MM:SSZ
/// YYYY-MM-DDTHH:MM:SS+hh:mm
/// YYYYMMDDTHHMMSSZ
iso8601,
/// Parse a datetime from an RFC3339 string. RFC3339 is similar to
/// ISO8601 but is more strict, and allows for arbitrary fractional
/// seconds. Using this field will use the same parser `iso8601`, but is
/// provided for clarity
/// has nanoseconds precision (9 digits after period), same as rfc3339Nano could be
/// Format: YYYY-MM-DDTHH:MM:SS.s{,9}+hh:mm
rfc3339,
/// Parse a datetime from an RFC5322 date-time spec
rfc5322,
/// Parse a datetime from an RFC2822 date-time spec. This is an alias for RFC5322
rfc2822,
/// Parse a datetime from an RFC1123 date-time spec
rfc1123,
};
/// convert this Instant to another timezone
pub fn in(self: Instant, zone: *const TimeZone) Instant {
return .{
.timestamp = self.timestamp,
.timezone = zone,
};
}
// convert the nanosecond timestamp into a unix timestamp (in seconds)
pub fn unixTimestamp(self: Instant) Seconds {
return @intCast(@divFloor(self.timestamp, ns_per_s));
}
pub fn milliTimestamp(self: Instant) Milliseconds {
return @intCast(@divFloor(self.timestamp, ns_per_ms));
}
// generate a calendar date and time for this instant
pub fn time(self: Instant) Time {
const adjusted = self.timezone.adjust(self.unixTimestamp());
const days = daysSinceEpoch(adjusted.timestamp);
const date = civilFromDays(days);
var seconds = @mod(adjusted.timestamp, s_per_day);
const hours = @divFloor(seconds, s_per_hour);
seconds -= hours * s_per_hour;
const minutes = @divFloor(seconds, s_per_min);
seconds -= minutes * s_per_min;
// get the nanoseconds from the original timestamp
var nanos = @mod(self.timestamp, ns_per_s);
const millis = @divFloor(nanos, ns_per_ms);
nanos -= millis * ns_per_ms;
const micros = @divFloor(nanos, ns_per_us);
nanos -= micros * ns_per_us;
return .{
.year = date.year,
.month = date.month,
.day = date.day,
.hour = @intCast(hours),
.minute = @intCast(minutes),
.second = @intCast(seconds),
.millisecond = @intCast(millis),
.microsecond = @intCast(micros),
.nanosecond = @intCast(nanos),
.offset = @intCast(adjusted.timestamp - self.unixTimestamp()),
.designation = adjusted.designation,
};
}
/// add the duration to the Instant
pub fn add(self: Instant, duration: Duration) error{Overflow}!Instant {
const ns = try duration.inNanoseconds();
// check for addition with overflow
const timestamp = @addWithOverflow(self.timestamp, ns);
if (timestamp[1] == 1) return error.Overflow;
return .{
.timestamp = timestamp[0],
.timezone = self.timezone,
};
}
/// subtract the duration from the Instant
pub fn subtract(self: Instant, duration: Duration) error{Overflow}!Instant {
const ns = try duration.inNanoseconds();
// check for subtraction with overflow
const timestamp = @subWithOverflow(self.timestamp, ns);
if (timestamp[1] == 1) return error.Overflow;
return .{
.timestamp = timestamp[0],
.timezone = self.timezone,
};
}
};
/// Creates a new Instant from time value specified by *source*.
///
/// See also .instantFromText().
pub fn instant(source: Instant.Source, tz: *const TimeZone) Instant {
const ts: Nanoseconds = switch (source) {
.now => |io| std.Io.Clock.now(.real, io).nanoseconds,
.unix_timestamp => |unix| @as(i128, unix) * ns_per_s,
.unix_nano => |nano| nano,
.time => |time| time.instant().timestamp,
};
return .{
.timestamp = ts,
.timezone = tz,
};
}
test "instant" {
const original = instant(.{ .time = .{} }, &utc);
const time = original.time();
const round_trip = time.instant();
try std.testing.expectEqual(original.timestamp, round_trip.timestamp);
}
/// Creates a new Instant by parsing text.
///
/// See also .instant()
pub fn instantFromText(format: Instant.TextFormat, text: []const u8, tz: *const TimeZone) !Instant {
const ts: Nanoseconds = switch (format) {
.iso8601,
.rfc3339,
=> blk: {
const t = try Time.fromISO8601(text);
break :blk t.instant().timestamp;
},
.rfc2822,
.rfc5322,
=> blk: {
const t = try Time.fromRFC5322(text);
break :blk t.instant().timestamp;
},
.rfc1123 => blk: {
const t = try Time.fromRFC1123(text);
break :blk t.instant().timestamp;
},
};
return .{
.timestamp = ts,
.timezone = tz,
};
}
test "instantFromText" {
const original_text = "2001-09-09T03:46:40+0200";
const original_tz: TimeZone = .{ .fixed = .{
.name = "foo",
.offset = 2 * std.time.s_per_hour,
.is_dst = true,
} };
// ^ not a real TZ but close enough (only offset matters here)
const round_trip_instant = try instantFromText(.iso8601, original_text, &original_tz);
const round_trip_text = try std.fmt.allocPrint(
std.testing.allocator,
"{f}",
.{round_trip_instant.time().timeFmt(.strftime, "%Y-%m-%dT%H:%M:%S%z")},
);
defer std.testing.allocator.free(round_trip_text);
try std.testing.expectEqualStrings(original_text, round_trip_text);
}
pub const Month = enum(u4) {
jan = 1,
feb,
mar,
apr,
may,
jun,
jul,
aug,
sep,
oct,
nov,
dec,
/// returns the last day of the month
/// Neri/Schneider algorithm
pub fn lastDay(self: Month, year: i32) u5 {
const m: u5 = @intFromEnum(self);
if (m == 2) return if (isLeapYear(year)) 29 else 28;
return 30 | (m ^ (m >> 3));
}
/// returns the full name of the month, eg "January"
pub fn name(self: Month) []const u8 {
return switch (self) {
.jan => "January",
.feb => "February",
.mar => "March",
.apr => "April",
.may => "May",
.jun => "June",
.jul => "July",
.aug => "August",
.sep => "September",
.oct => "October",
.nov => "November",
.dec => "December",
};
}
/// returns the short name of the month, eg "Jan"
pub fn shortName(self: Month) []const u8 {
return self.name()[0..3];
}
test "lastDayOfMonth" {
try std.testing.expectEqual(29, Month.feb.lastDay(2000));
try std.testing.expectEqual(31, Month.jan.lastDay(2001));
try std.testing.expectEqual(28, Month.feb.lastDay(2001));
try std.testing.expectEqual(31, Month.mar.lastDay(2001));
try std.testing.expectEqual(30, Month.apr.lastDay(2001));
try std.testing.expectEqual(31, Month.may.lastDay(2001));
try std.testing.expectEqual(30, Month.jun.lastDay(2001));
try std.testing.expectEqual(31, Month.jul.lastDay(2001));
try std.testing.expectEqual(31, Month.aug.lastDay(2001));
try std.testing.expectEqual(30, Month.sep.lastDay(2001));
try std.testing.expectEqual(31, Month.oct.lastDay(2001));
try std.testing.expectEqual(30, Month.nov.lastDay(2001));
try std.testing.expectEqual(31, Month.dec.lastDay(2001));
}
/// the number of days in a year before this month
pub fn daysBefore(self: Month, year: i32) u9 {
var m = @intFromEnum(self) - 1;
var result: u9 = 0;
while (m > 0) : (m -= 1) {
const month: Month = @enumFromInt(m);
result += month.lastDay(year);
}
return result;
}
test "daysBefore" {
try std.testing.expectEqual(60, Month.mar.daysBefore(2000));
try std.testing.expectEqual(0, Month.jan.daysBefore(2001));
try std.testing.expectEqual(31, Month.feb.daysBefore(2001));
try std.testing.expectEqual(59, Month.mar.daysBefore(2001));
}
};
pub const Duration = struct {
days: usize = 0,
hours: usize = 0,
minutes: usize = 0,
seconds: usize = 0,
milliseconds: usize = 0,
microseconds: usize = 0,
nanoseconds: usize = 0,
/// duration expressed as the total number of nanoseconds
pub fn inNanoseconds(self: Duration) error{Overflow}!u64 {
// check for multiplication with overflow
const days_in_ns = @mulWithOverflow(self.days, ns_per_day);
const hours_in_ns = @mulWithOverflow(self.hours, ns_per_hour);
const minutes_in_ns = @mulWithOverflow(self.minutes, ns_per_min);
const seconds_in_ns = @mulWithOverflow(self.seconds, ns_per_s);
const milliseconds_in_ns = @mulWithOverflow(self.milliseconds, ns_per_ms);
const microseconds_in_ns = @mulWithOverflow(self.microseconds, ns_per_us);
if (days_in_ns[1] == 1 or
hours_in_ns[1] == 1 or
minutes_in_ns[1] == 1 or
seconds_in_ns[1] == 1 or
milliseconds_in_ns[1] == 1 or
microseconds_in_ns[1] == 1) return error.Overflow;
// check for addition with overflow
var ns = days_in_ns[0];
const components = [_]usize{
hours_in_ns[0],
minutes_in_ns[0],
seconds_in_ns[0],
milliseconds_in_ns[0],
microseconds_in_ns[0],
self.nanoseconds,
};
for (components) |value| {
const sum_with_overflow = @addWithOverflow(ns, value);
if (sum_with_overflow[1] == 1) return error.Overflow;
ns = sum_with_overflow[0];
}
return ns;
}
};
pub const Weekday = enum(u3) {
sun = 0,
mon,
tue,
wed,
thu,
fri,
sat,
/// number of days from self until other. Returns 0 when self == other
pub fn daysUntil(self: Weekday, other: Weekday) u3 {
const d: u8 = @as(u8, @intFromEnum(other)) -% @as(u8, @intFromEnum(self));
return if (d <= 6) @intCast(d) else @intCast(d +% 7);
}
/// returns the full name of the day, eg "Tuesday"
pub fn name(self: Weekday) []const u8 {
return switch (self) {
.sun => "Sunday",
.mon => "Monday",
.tue => "Tuesday",
.wed => "Wednesday",
.thu => "Thursday",
.fri => "Friday",
.sat => "Saturday",
};
}
/// returns the short name of the day, eg "Tue"
pub fn shortName(self: Weekday) []const u8 {
return self.name()[0..3];
}
test "daysUntil" {
const wed: Weekday = .wed;
try std.testing.expectEqual(0, wed.daysUntil(.wed));
try std.testing.expectEqual(6, wed.daysUntil(.tue));
try std.testing.expectEqual(5, wed.daysUntil(.mon));
try std.testing.expectEqual(4, wed.daysUntil(.sun));
}
};
pub const Date = struct {
year: i32,
month: Month,
day: u5, // 1-31
/// Checks for equality of two dates
pub fn eql(date1: Date, date2: Date) bool {
return date1.year == date2.year and
date1.month == date2.month and
date1.day == date2.day;
}
test "Date-Equality" {
const date: Date = .{
.year = 2025,
.month = Month.sep,
.day = 13,
};
try std.testing.expect(date.eql(Date{ .year = 2025, .month = Month.sep, .day = 13 }));
try std.testing.expect(!date.eql(Date{ .year = 2025, .month = Month.sep, .day = 12 }));
try std.testing.expect(!date.eql(Date{ .year = 2025, .month = Month.aug, .day = 13 }));
try std.testing.expect(!date.eql(Date{ .year = 2024, .month = Month.sep, .day = 13 }));
}
/// Compares two dates with another. If `date2` happens after `date1`, then the `TimeComparison.after` is returned.
/// If `date2` happens before `date1`, then `TimeComparison.before` is returned. If both represent the same date, `TimeComparison.equal` is returned;
pub fn compare(date1: Date, date2: Date) TimeComparison {
if (date1.year > date2.year) {
return .before;
} else if (date1.year < date2.year) {
return .after;
}
if (@intFromEnum(date1.month) > @intFromEnum(date2.month)) {
return .before;
} else if (@intFromEnum(date1.month) < @intFromEnum(date2.month)) {
return .after;
}
if (date1.day > date2.day) {
return .before;
} else if (date1.day < date2.day) {
return .after;
}
return .equal;
}
test "Date-Comparison" {
const date: Date = .{
.year = 2025,
.month = Month.sep,
.day = 13,
};
try std.testing.expectEqual(TimeComparison.before, date.compare(Date{ .year = 2025, .month = Month.sep, .day = 12 }));
try std.testing.expectEqual(TimeComparison.before, date.compare(Date{ .year = 2025, .month = Month.aug, .day = 13 }));
try std.testing.expectEqual(TimeComparison.before, date.compare(Date{ .year = 2024, .month = Month.sep, .day = 13 }));
try std.testing.expectEqual(TimeComparison.before, date.compare(Date{ .year = 2024, .month = Month.dec, .day = 31 }));
try std.testing.expectEqual(TimeComparison.before, date.compare(Date{ .year = 2025, .month = Month.aug, .day = 31 }));
try std.testing.expectEqual(TimeComparison.after, date.compare(Date{ .year = 2025, .month = Month.sep, .day = 14 }));
try std.testing.expectEqual(TimeComparison.after, date.compare(Date{ .year = 2025, .month = Month.oct, .day = 13 }));
try std.testing.expectEqual(TimeComparison.after, date.compare(Date{ .year = 2026, .month = Month.sep, .day = 13 }));
try std.testing.expectEqual(TimeComparison.after, date.compare(Date{ .year = 2026, .month = Month.jan, .day = 1 }));
try std.testing.expectEqual(TimeComparison.after, date.compare(Date{ .year = 2025, .month = Month.oct, .day = 1 }));
try std.testing.expectEqual(TimeComparison.equal, date.compare(Date{ .year = 2025, .month = Month.sep, .day = 13 }));
}
};
pub const TimeComparison = enum(u2) {
after,
before,
equal,
};
pub const Time = struct {
year: i32 = 1970,
month: Month = .jan,
day: u5 = 1, // 1-31
hour: u5 = 0, // 0-23
minute: u6 = 0, // 0-59
second: u6 = 0, // 0-60
millisecond: u10 = 0, // 0-999
microsecond: u10 = 0, // 0-999
nanosecond: u10 = 0, // 0-999
offset: i32 = 0, // offset from UTC in seconds
designation: []const u8 = "",
/// Creates a UTC Instant for this time
pub fn instant(self: Time) Instant {
const days = daysFromCivil(.{
.year = self.year,
.month = self.month,
.day = self.day,
});
return .{
.timestamp = @as(i128, days) * ns_per_day +
@as(i128, self.hour) * ns_per_hour +
@as(i128, self.minute) * ns_per_min +
@as(i128, self.second) * ns_per_s +
@as(i128, self.millisecond) * ns_per_ms +
@as(i128, self.microsecond) * ns_per_us +
@as(i128, self.nanosecond) -
@as(i128, self.offset) * ns_per_s,
.timezone = &utc,
};
}
pub fn fromISO8601(iso: []const u8) !Time {
const parseInt = std.fmt.parseInt;
var time: Time = .{};
const State = enum {
year,
month_or_ordinal,
day,
hour,
minute,
minute_fraction_or_second,
second_fraction_or_offset,
};
var state: State = .year;
var i: usize = 0;
while (i < iso.len) {
switch (state) {
.year => {
if (iso.len <= 4) {
// year only data
const int = try parseInt(i32, iso, 10);
time.year = int * std.math.pow(i32, 10, @as(i32, @intCast(4 - iso.len)));
break;
} else {
time.year = try parseInt(i32, iso[0..4], 10);
state = .month_or_ordinal;
i += 4;
if (iso[i] == '-') i += 1;
}
},
.month_or_ordinal => {
const token_end = std.mem.indexOfAnyPos(u8, iso, i, "- T") orelse iso.len;
switch (token_end - i) {
2 => {
const m: u4 = try parseInt(u4, iso[i..token_end], 10);
time.month = @enumFromInt(m);
state = .day;
},
3 => { // ordinal
const doy = try parseInt(u9, iso[i..token_end], 10);
var m: u4 = 1;
var days: u9 = 0;
while (m <= 12) : (m += 1) {
const month: Month = @enumFromInt(m);
if (days + month.lastDay(time.year) < doy) {
days += month.lastDay(time.year);
continue;
}
time.month = month;
time.day = @intCast(doy - days);
break;
}
state = .hour;
},
4 => { // MMDD
const m: u4 = try parseInt(u4, iso[i .. i + 2], 10);
time.month = @enumFromInt(m);
time.day = try parseInt(u5, iso[i + 2 .. token_end], 10);
state = .hour;
},
else => return error.InvalidISO8601,
}
i = token_end + 1;
},
.day => {
time.day = try parseInt(u5, iso[i .. i + 2], 10);
// add 3 instead of 2 because we either have a trailing ' ',
// 'T', or EOF
i += 3;
state = .hour;
},
.hour => {
time.hour = try parseInt(u5, iso[i .. i + 2], 10);
i += 2;
state = .minute;
},
.minute => {
if (iso[i] == ':') i += 1;
time.minute = try parseInt(u6, iso[i .. i + 2], 10);
i += 2;
state = .minute_fraction_or_second;
},
.minute_fraction_or_second => {
const b = iso[i];
if (b == '.') return error.UnhandledFormat; // TODO:
if (b == ':') i += 1;
if (std.ascii.isDigit(iso[i])) {
time.second = try parseInt(u6, iso[i .. i + 2], 10);
i += 2;
}
state = .second_fraction_or_offset;
},
.second_fraction_or_offset => {
switch (iso[i]) {
'Z' => break,
'+', '-' => {
const sign: i32 = if (iso[i] == '-') -1 else 1;
i += 1;
const hour = try parseInt(u5, iso[i .. i + 2], 10);
i += 2;
time.offset = sign * hour * s_per_hour;
if (i >= iso.len - 1) break;
if (iso[i] == ':') i += 1;
const minute = try parseInt(u6, iso[i .. i + 2], 10);
time.offset += sign * minute * s_per_min;
i += 2;
break;
},
'.' => {
i += 1;
const frac_end = std.mem.indexOfAnyPos(u8, iso, i, "Z+-") orelse iso.len;
const rhs = try parseInt(u64, iso[i..frac_end], 10);
const sigs = frac_end - i;
// convert sigs to nanoseconds
const pow = std.math.pow(u64, 10, @as(u64, @intCast(9 - sigs)));
var nanos = rhs * pow;
time.millisecond = @intCast(@divFloor(nanos, ns_per_ms));
nanos -= @as(u64, time.millisecond) * ns_per_ms;
time.microsecond = @intCast(@divFloor(nanos, ns_per_us));
nanos -= @as(u64, time.microsecond) * ns_per_us;
time.nanosecond = @intCast(nanos);
i = frac_end;
},
else => return error.InvalidISO8601,
}
},
}
}
return time;
}
test "fromISO8601" {
{
const year = try Time.fromISO8601("2000");
try std.testing.expectEqual(2000, year.year);
}
{
const ym = try Time.fromISO8601("200002");
try std.testing.expectEqual(2000, ym.year);
try std.testing.expectEqual(.feb, ym.month);
const ym_ext = try Time.fromISO8601("2000-02");
try std.testing.expectEqual(2000, ym_ext.year);
try std.testing.expectEqual(.feb, ym_ext.month);
}
{
const ymd = try Time.fromISO8601("20000212");
try std.testing.expectEqual(2000, ymd.year);
try std.testing.expectEqual(.feb, ymd.month);
try std.testing.expectEqual(12, ymd.day);
const ymd_ext = try Time.fromISO8601("2000-02-12");
try std.testing.expectEqual(2000, ymd_ext.year);
try std.testing.expectEqual(.feb, ymd_ext.month);
try std.testing.expectEqual(12, ymd_ext.day);
}
{
const ordinal = try Time.fromISO8601("2000031");
try std.testing.expectEqual(2000, ordinal.year);
try std.testing.expectEqual(.jan, ordinal.month);
try std.testing.expectEqual(31, ordinal.day);
const ordinal_ext = try Time.fromISO8601("2000-043");
try std.testing.expectEqual(2000, ordinal_ext.year);
try std.testing.expectEqual(.feb, ordinal_ext.month);
try std.testing.expectEqual(12, ordinal_ext.day);
}
{
const ymdh = try Time.fromISO8601("20000212 11");
try std.testing.expectEqual(2000, ymdh.year);
try std.testing.expectEqual(.feb, ymdh.month);
try std.testing.expectEqual(12, ymdh.day);
try std.testing.expectEqual(11, ymdh.hour);
const ymdh_ext = try Time.fromISO8601("2000-02-12T11");
try std.testing.expectEqual(2000, ymdh_ext.year);
try std.testing.expectEqual(.feb, ymdh_ext.month);
try std.testing.expectEqual(12, ymdh_ext.day);
try std.testing.expectEqual(11, ymdh_ext.hour);
}
{
const ymdhm = try Time.fromISO8601("2025-05-19T11:23");
try std.testing.expectEqual(2025, ymdhm.year);
try std.testing.expectEqual(.may, ymdhm.month);
try std.testing.expectEqual(19, ymdhm.day);
try std.testing.expectEqual(11, ymdhm.hour);
try std.testing.expectEqual(23, ymdhm.minute);
}
{
const full = try Time.fromISO8601("20000212 111213Z");
try std.testing.expectEqual(2000, full.year);
try std.testing.expectEqual(.feb, full.month);
try std.testing.expectEqual(12, full.day);
try std.testing.expectEqual(11, full.hour);
try std.testing.expectEqual(12, full.minute);
try std.testing.expectEqual(13, full.second);
const full_ext = try Time.fromISO8601("2000-02-12T11:12:13Z");
try std.testing.expectEqual(2000, full_ext.year);
try std.testing.expectEqual(.feb, full_ext.month);
try std.testing.expectEqual(12, full_ext.day);
try std.testing.expectEqual(11, full_ext.hour);
try std.testing.expectEqual(12, full_ext.minute);
try std.testing.expectEqual(13, full_ext.second);
}
{
const s_frac = try Time.fromISO8601("2000-02-12T11:12:13.123Z");
try std.testing.expectEqual(123, s_frac.millisecond);
try std.testing.expectEqual(0, s_frac.microsecond);
try std.testing.expectEqual(0, s_frac.nanosecond);
}
{
const offset = try Time.fromISO8601("2000-02-12T11:12:13.123-12:00");
try std.testing.expectEqual(-12 * s_per_hour, offset.offset);
}
{
const offset = try Time.fromISO8601("2000-02-12T11:12:13+12:30");
try std.testing.expectEqual(12 * s_per_hour + 30 * s_per_min, offset.offset);
}
{
const offset = try Time.fromISO8601("2025-05-19T11:23+0200");
try std.testing.expectEqual(2 * s_per_hour, offset.offset);
}
{
const offset = try Time.fromISO8601("20000212T111213+1230");
try std.testing.expectEqual(12 * s_per_hour + 30 * s_per_min, offset.offset);
}
{
const basic = try Time.fromISO8601("20240224T154944");
try std.testing.expectEqual(2024, basic.year);
try std.testing.expectEqual(Month.feb, basic.month);
try std.testing.expectEqual(24, basic.day);
try std.testing.expectEqual(15, basic.hour);
try std.testing.expectEqual(49, basic.minute);
try std.testing.expectEqual(44, basic.second);
try std.testing.expectEqual(0, basic.offset);
}
{
const basic = try Time.fromISO8601("20240224T154944Z");
try std.testing.expectEqual(2024, basic.year);
try std.testing.expectEqual(Month.feb, basic.month);
try std.testing.expectEqual(24, basic.day);
try std.testing.expectEqual(15, basic.hour);
try std.testing.expectEqual(49, basic.minute);
try std.testing.expectEqual(44, basic.second);
try std.testing.expectEqual(0, basic.offset);
}
}
/// Parse an RFC 5322 date-time string (e.g., "Thu, 13 Feb 1969 23:32:54 -0330").
///
/// Supports obsolete timezone names from RFC 5322 section 4.3:
/// - UT, GMT: UTC (+0000)
/// - US timezones: EST/EDT, CST/CDT, MST/MDT, PST/PDT
/// - Single-letter military timezones (A-I, K-M, N-Y, Z)
///
/// Note: Military timezones use conventional offsets (A=+1, N=-1, etc.) rather than
/// RFC 5322's recommendation to treat them as "-0000" (unknown offset).
pub fn fromRFC5322(eml: []const u8) !Time {
const parseInt = std.fmt.parseInt;
var time: Time = .{};
var i: usize = 0;
// day
{
// consume until a digit
while (i < eml.len and !std.ascii.isDigit(eml[i])) : (i += 1) {}
const end = std.mem.indexOfScalarPos(u8, eml, i, ' ') orelse return error.InvalidFormat;
time.day = try parseInt(u5, eml[i..end], 10);
i = end + 1;
}
// month
{
// consume until an alpha
while (i < eml.len and !std.ascii.isAlphabetic(eml[i])) : (i += 1) {}
assert(eml.len >= i + 3);
var buf: [3]u8 = undefined;
buf[0] = std.ascii.toLower(eml[i]);
buf[1] = std.ascii.toLower(eml[i + 1]);
buf[2] = std.ascii.toLower(eml[i + 2]);
time.month = std.meta.stringToEnum(Month, &buf) orelse return error.InvalidFormat;
i += 3;
}
// year
{
// consume until a digit
while (i < eml.len and !std.ascii.isDigit(eml[i])) : (i += 1) {}
assert(eml.len >= i + 4);
time.year = try parseInt(i32, eml[i .. i + 4], 10);
i += 4;
}
// hour
{
// consume until a digit
while (i < eml.len and !std.ascii.isDigit(eml[i])) : (i += 1) {}
const end = std.mem.indexOfScalarPos(u8, eml, i, ':') orelse return error.InvalidFormat;
time.hour = try parseInt(u5, eml[i..end], 10);
i = end + 1;
}
// minute
{
// consume until a digit
while (i < eml.len and !std.ascii.isDigit(eml[i])) : (i += 1) {}
assert(i + 2 < eml.len);
time.minute = try parseInt(u6, eml[i .. i + 2], 10);
i += 2;
}
// second and zone
{
assert(i < eml.len);
// seconds are optional
if (eml[i] == ':') {
i += 1;
assert(i + 2 < eml.len);
time.second = try parseInt(u6, eml[i .. i + 2], 10);
i += 2;
}
// consume whitespace
while (i < eml.len and std.ascii.isWhitespace(eml[i])) : (i += 1) {}
switch (eml.len - i) {
else => {
const hours = try parseInt(i32, eml[i .. i + 3], 10);
const minutes = try parseInt(i32, eml[i + 3 .. i + 5], 10);
const offset_minutes: i32 = if (hours > 0)
hours * 60 + minutes
else
hours * 60 - minutes;
time.offset = offset_minutes * 60;
},
4 => return error.InvalidFormat, // No formats should have a 4 character zone
3 => {
const ObsoleteZoneParseState = enum {
start,
g,
gm,
e,
ed,
es,
c,
cs,
cd,
m,
ms,
md,
p,
ps,
pd,
invalid,
};
const first = std.ascii.toUpper(eml[i]);
const second = std.ascii.toUpper(eml[i + 1]);
const third = std.ascii.toUpper(eml[i + 2]);
// The last of all should be 'T'
if (third != 'T') return error.InvalidFormat;
parse: switch (ObsoleteZoneParseState.start) {
.start => switch (first) {
'G' => continue :parse .g,
'E' => continue :parse .e,
'C' => continue :parse .c,
'M' => continue :parse .m,
'P' => continue :parse .p,
else => return error.InvalidFormat,
},
.g => if (second == 'M') continue :parse .gm else continue :parse .invalid,
.e => if (second == 'D') continue :parse .ed else if (second == 'S') continue :parse .es else continue :parse .invalid,
.c => if (second == 'D') continue :parse .cd else if (second == 'S') continue :parse .cs else continue :parse .invalid,
.m => if (second == 'D') continue :parse .md else if (second == 'S') continue :parse .ms else continue :parse .invalid,
.p => if (second == 'D') continue :parse .pd else if (second == 'S') continue :parse .ps else continue :parse .invalid,
.gm => {
time.offset = 0;
},
.ed => {
time.offset = -4 * 3600;
},
.es, .cd => {
time.offset = -5 * 3600;
},
.cs, .md => {
time.offset = -6 * 3600;
},
.ms, .pd => {
time.offset = -7 * 3600;
},
.ps => {
time.offset = -8 * 3600;
},
.invalid => return error.InvalidFormat,
}
},
2 => {
if (std.ascii.toUpper(eml[i]) == 'U' and std.ascii.toUpper(eml[i + 1]) == 'T') {
time.offset = 0;
} else {
return error.InvalidFormat;
}
},
1 => {
switch (eml[i]) {
'Z', 'z' => time.offset = 0,
'A'...'I' => time.offset = (@as(i32, eml[i] - 'A') + 1) * 3600,
'a'...'i' => time.offset = (@as(i32, eml[i] - 'a') + 1) * 3600,
'K'...'M' => time.offset = (@as(i32, eml[i] - 'A')) * 3600, // J is skipped, already offset by 1
'k'...'m' => time.offset = (@as(i32, eml[i] - 'a')) * 3600, // j is skipped, already offset by 1
'N'...'Y' => time.offset = -((@as(i32, eml[i] - 'N') + 1) * 3600),
'n'...'y' => time.offset = -((@as(i32, eml[i] - 'n') + 1) * 3600),
else => return error.InvalidFormat,
}
},
}
}
return time;
}
test "fromRFC5322" {
{
const time = try Time.fromRFC5322("Thu, 13 Feb 1969 23:32:54 -0330");
try std.testing.expectEqual(1969, time.year);
try std.testing.expectEqual(.feb, time.month);
try std.testing.expectEqual(13, time.day);
try std.testing.expectEqual(23, time.hour);
try std.testing.expectEqual(32, time.minute);
try std.testing.expectEqual(54, time.second);
try std.testing.expectEqual(-12_600, time.offset);
}
{
// FWS everywhere
const time = try Time.fromRFC5322(" Thu, 13 \tFeb 1969\t\r\n 23:32:54 -0330");
try std.testing.expectEqual(1969, time.year);
try std.testing.expectEqual(.feb, time.month);
try std.testing.expectEqual(13, time.day);
try std.testing.expectEqual(23, time.hour);
try std.testing.expectEqual(32, time.minute);
try std.testing.expectEqual(54, time.second);
try std.testing.expectEqual(-12_600, time.offset);
}
{
const Test = struct {
name: []const u8,
value: i32,
};
const tests = [_]Test{
.{ .name = "UT", .value = 0 },
.{ .name = "ut", .value = 0 },
.{ .name = "GMT", .value = 0 },
.{ .name = "gmt", .value = 0 },
.{ .name = "EDT", .value = -4 * 3600 },
.{ .name = "edt", .value = -4 * 3600 },
.{ .name = "EST", .value = -5 * 3600 },
.{ .name = "est", .value = -5 * 3600 },
.{ .name = "CDT", .value = -5 * 3600 },
.{ .name = "cdt", .value = -5 * 3600 },
.{ .name = "CST", .value = -6 * 3600 },
.{ .name = "cst", .value = -6 * 3600 },
.{ .name = "MDT", .value = -6 * 3600 },
.{ .name = "mdt", .value = -6 * 3600 },
.{ .name = "MST", .value = -7 * 3600 },
.{ .name = "mst", .value = -7 * 3600 },
.{ .name = "PDT", .value = -7 * 3600 },
.{ .name = "pdt", .value = -7 * 3600 },
.{ .name = "PST", .value = -8 * 3600 },
.{ .name = "pst", .value = -8 * 3600 },
.{ .name = "I", .value = 9 * 3600 },
.{ .name = "K", .value = 10 * 3600 },
.{ .name = "M", .value = 12 * 3600 },
.{ .name = "n", .value = -1 * 3600 },
.{ .name = "y", .value = -12 * 3600 },
.{ .name = "z", .value = 0 },
.{ .name = "Z", .value = 0 },
};
for (tests) |t| {
var buf: [64]u8 = undefined;
const rfc5322_str = std.fmt.bufPrint(&buf, "Thu, 13 Feb 1969 23:32:54 {s}", .{t.name}) catch unreachable;
const time = try Time.fromRFC5322(rfc5322_str);
try std.testing.expectEqual(1969, time.year);
try std.testing.expectEqual(.feb, time.month);
try std.testing.expectEqual(13, time.day);
try std.testing.expectEqual(23, time.hour);
try std.testing.expectEqual(32, time.minute);
try std.testing.expectEqual(54, time.second);
try std.testing.expectEqual(t.value, time.offset);
}
try std.testing.expectError(
error.InvalidFormat,
Time.fromRFC5322("Thu, 13 Feb 1969 23:32:54 XYZ"),
);
try std.testing.expectError(
error.InvalidFormat,
Time.fromRFC5322("Thu, 13 Feb 1969 23:32:54 J"), // J is not allowed in Military timezones
);
}
}
pub fn fromRFC1123(http_date: []const u8) !Time {
const parseInt = std.fmt.parseInt;
var time: Time = .{};
var i: usize = 0;
// day
{
// consume until a digit
while (i < http_date.len and !std.ascii.isDigit(http_date[i])) : (i += 1) {}
const end = std.mem.indexOfScalarPos(u8, http_date, i, ' ') orelse return error.InvalidFormat;
time.day = try parseInt(u5, http_date[i..end], 10);
i = end + 1;
}
// month
{
// consume until an alpha
while (i < http_date.len and !std.ascii.isAlphabetic(http_date[i])) : (i += 1) {}
assert(http_date.len >= i + 3);
var buf: [3]u8 = undefined;
buf[0] = std.ascii.toLower(http_date[i]);
buf[1] = std.ascii.toLower(http_date[i + 1]);
buf[2] = std.ascii.toLower(http_date[i + 2]);
time.month = std.meta.stringToEnum(Month, &buf) orelse return error.InvalidFormat;
i += 3;
}
// year
{
// consume until a digit
while (i < http_date.len and !std.ascii.isDigit(http_date[i])) : (i += 1) {}
assert(http_date.len >= i + 4);
time.year = try parseInt(i32, http_date[i .. i + 4], 10);
i += 4;
}
// hour
{
// consume until a digit
while (i < http_date.len and !std.ascii.isDigit(http_date[i])) : (i += 1) {}
const end = std.mem.indexOfScalarPos(u8, http_date, i, ':') orelse return error.InvalidFormat;
time.hour = try parseInt(u5, http_date[i..end], 10);
i = end + 1;
}
// minute
{
// consume until a digit
while (i < http_date.len and !std.ascii.isDigit(http_date[i])) : (i += 1) {}
assert(i + 2 < http_date.len);
time.minute = try parseInt(u6, http_date[i .. i + 2], 10);
i += 2;
}
// second
{
assert(i < http_date.len);
i += 1;
assert(i + 2 < http_date.len);
time.second = try parseInt(u6, http_date[i .. i + 2], 10);
i += 2;
}
// zone
{
// consume whitespace
while (i < http_date.len and std.ascii.isWhitespace(http_date[i])) : (i += 1) {}
assert(std.mem.eql(u8, http_date[i..], "GMT"));
time.offset = 0;
}
return time;
}
test "fromRFC1123" {
{
const time = try Time.fromRFC1123("Sun, 06 Nov 1994 08:49:37 GMT");
try std.testing.expectEqual(1994, time.year);
try std.testing.expectEqual(.nov, time.month);
try std.testing.expectEqual(6, time.day);
try std.testing.expectEqual(8, time.hour);
try std.testing.expectEqual(49, time.minute);
try std.testing.expectEqual(37, time.second);
try std.testing.expectEqual(0, time.offset);
}
}
pub const Format = union(enum) {
rfc3339, // YYYY-MM-DD-THH:MM:SS.sss+00:00
rfc3339Nano, // YYYY-MM-DD-THH:MM:SS.sssssssss+00:00, has 9 digits after period, nanos precision
};
pub fn bufPrint(self: Time, buf: []u8, fmt: Format) ![]u8 {
switch (fmt) {
.rfc3339 => {
if (self.year < 0) return error.InvalidTime;
if (self.offset == 0)
return std.fmt.bufPrint(
buf,
"{d:0>4}-{d:0>2}-{d:0>2}T{d:0>2}:{d:0>2}:{d:0>2}.{d:0>3}Z",
.{
@as(u32, @intCast(self.year)),
@intFromEnum(self.month),
self.day,
self.hour,
self.minute,
self.second,
self.millisecond,
},
)
else {
const h = @divFloor(@abs(self.offset), s_per_hour);
const min = @divFloor(@abs(self.offset) - h * s_per_hour, s_per_min);
const sign: u8 = if (self.offset > 0) '+' else '-';
return std.fmt.bufPrint(
buf,
"{d:0>4}-{d:0>2}-{d:0>2}T{d:0>2}:{d:0>2}:{d:0>2}.{d:0>3}{c}{d:0>2}:{d:0>2}",
.{
@as(u32, @intCast(self.year)),
@intFromEnum(self.month),
self.day,
self.hour,
self.minute,
self.second,
self.millisecond,
sign,
h,
min,
},
);
}
},
.rfc3339Nano => {
if (self.year < 0) return error.InvalidTime;
if (self.offset == 0)
return std.fmt.bufPrint(
buf,
"{d:0>4}-{d:0>2}-{d:0>2}T{d:0>2}:{d:0>2}:{d:0>2}.{d:0>3}{d:0>3}{d:0>3}Z",
.{
@as(u32, @intCast(self.year)),
@intFromEnum(self.month),
self.day,
self.hour,
self.minute,
self.second,
self.millisecond,
self.microsecond,
self.nanosecond,
},
)
else {
const h = @divFloor(@abs(self.offset), s_per_hour);
const min = @divFloor(@abs(self.offset) - h * s_per_hour, s_per_min);
const sign: u8 = if (self.offset > 0) '+' else '-';
return std.fmt.bufPrint(
buf,
"{d:0>4}-{d:0>2}-{d:0>2}T{d:0>2}:{d:0>2}:{d:0>2}.{d:0>3}{d:0>3}{d:0>3}{c}{d:0>2}:{d:0>2}",
.{
@as(u32, @intCast(self.year)),
@intFromEnum(self.month),
self.day,
self.hour,
self.minute,
self.second,
self.millisecond,
self.microsecond,
self.nanosecond,
sign,
h,
min,
},
);
}
},
}
}
pub const FmtKind = enum { gofmt, strftime };
pub fn timeFmt(self: Time, kind: FmtKind, fmt_str: []const u8) TimeFmt {
return .{
.time = self,
.kind = kind,
.str = fmt_str,
};
}
const TimeFmt = struct {
time: Time,
kind: FmtKind,
str: []const u8,
pub fn format(self: TimeFmt, writer: *std.Io.Writer) !void {
switch (self.kind) {
.gofmt => self.time.gofmt(writer, self.str) catch return error.WriteFailed,
.strftime => self.time.strftime(writer, self.str) catch return error.WriteFailed,
}
}
};
/// Format time using strftime(3) specified, eg %Y-%m-%dT%H:%M:%S
pub fn strftime(self: Time, writer: *std.Io.Writer, fmt: []const u8) !void {
const inst = self.instant();
var i: usize = 0;
while (i < fmt.len) {
const last = i;
i = std.mem.indexOfScalarPos(u8, fmt, i, '%') orelse {
try writer.writeAll(fmt[i..]);
i = fmt.len;
break;
};
if (i + 1 >= fmt.len) return error.InvalidFormat;
try writer.writeAll(fmt[last..i]);
defer i = i + 2;
const b = fmt[i + 1];
switch (b) {
'%' => try writer.writeByte('%'),
'a' => {
const days = daysFromCivil(
.{ .year = self.year, .month = self.month, .day = self.day },
);
const weekday = weekdayFromDays(days);
try writer.writeAll(weekday.shortName());
},
'A' => {
const days = daysFromCivil(
.{ .year = self.year, .month = self.month, .day = self.day },
);
const weekday = weekdayFromDays(days);
try writer.writeAll(weekday.name());
},
'b', 'h' => try writer.writeAll(self.month.shortName()),
'B' => try writer.writeAll(self.month.name()),
'c' => try self.strftime(writer, "%a %b %e %H:%M:%S %Y"), // locale specific
'C' => {
if (self.year > 9999 or self.year < -9999) return error.Overflow;
var buf: [5]u8 = undefined;
// year is an i64, which gets printed with a + or a -
_ = try std.fmt.bufPrint(&buf, "{d:0>4}", .{self.year});
try writer.writeAll(buf[1..3]);
},
'd' => try writer.print("{d:0>2}", .{self.day}),
'D' => try self.strftime(writer, "%m/%d/%y"),
'e' => try writer.print("{d: >2}", .{self.day}),
'f' => try writer.print("{d:0>3}{d:0>3}", .{ self.millisecond, self.microsecond }),
'F' => try self.strftime(writer, "%Y-%m-%d"),
'G' => return error.UnsupportedSpecifier,
'g' => return error.UnsupportedSpecifier,
'H' => try writer.print("{d:0>2}", .{self.hour}),
'I' => {
switch (self.hour) {
0 => try writer.writeAll("12"),
1...12 => try writer.print("{d:0>2}", .{self.hour}),
else => try writer.print("{d:0>2}", .{self.hour - 12}),
}
},
'j' => {
const before_month = self.month.daysBefore(self.year);
try writer.print("{d:0>3}", .{self.day + before_month});
},
'k' => try writer.print("{d}", .{self.hour}),
'l' => {
switch (self.hour) {
0 => try writer.writeAll("12"),
1...12 => try writer.print("{d}", .{self.hour}),
else => try writer.print("{d}", .{self.hour - 12}),
}
},
'm' => try writer.print("{d:0>2}", .{@intFromEnum(self.month)}),
'M' => try writer.print("{d:0>2}", .{self.minute}),
'n' => try writer.writeByte('\n'),
'O' => return error.UnsupportedSpecifier,
'p' => {
if (self.hour >= 12)
try writer.writeAll("PM")
else
try writer.writeAll("AM");
},
'P' => {
if (self.hour >= 12)
try writer.writeAll("pm")
else
try writer.writeAll("am");
},
'r' => try self.strftime(writer, "%I:%M:%S %p"),
'R' => try self.strftime(writer, "%H:%M"),
's' => try writer.print("{d}", .{inst.unixTimestamp()}),
'S' => try writer.print("{d:0>2}", .{self.second}),
't' => try writer.writeByte('\t'),
'T' => try self.strftime(writer, "%H:%M:%S"),
'u' => {
const days = daysFromCivil(
.{ .year = self.year, .month = self.month, .day = self.day },
);
const weekday = weekdayFromDays(days);
switch (weekday) {
.sun => try writer.writeByte('7'),
else => try writer.writeByte(@as(u8, @intFromEnum(weekday)) + 0x30),
}
},
'U' => {
const day_of_year = self.day + self.month.daysBefore(self.year);
// find the date of the first sunday
const weekd_jan_1 = blk: {
const jan_1: Date = .{ .year = self.year, .month = .jan, .day = 1 };
const days = daysFromCivil(jan_1);
break :blk weekdayFromDays(days);
};
// Day of year of first sunday. This represents the start of week 1
const first_sunday = switch (weekd_jan_1) {
.sun => 1,
else => 7 - @intFromEnum(weekd_jan_1) + 1,
};
if (day_of_year < first_sunday)
try writer.writeAll("00")
else
try writer.print("{d:0>2}", .{(day_of_year + 7 - first_sunday) / 7});
},
'V' => return error.UnsupportedSpecifier,
'w' => {
const days = daysFromCivil(
.{ .year = self.year, .month = self.month, .day = self.day },
);
const weekday = weekdayFromDays(days);
try writer.writeByte(@as(u8, @intFromEnum(weekday)) + 0x30);
},
'W' => {
const day_of_year = self.day + self.month.daysBefore(self.year);
// find the date of the first sunday
const weekd_jan_1 = blk: {
const jan_1: Date = .{ .year = self.year, .month = .jan, .day = 1 };
const days = daysFromCivil(jan_1);
break :blk weekdayFromDays(days);
};
// Day of year of first sunday. This represents the start of week 1
const first_monday = switch (weekd_jan_1) {
.sun => 2,
.mon => 1,
else => 7 - @intFromEnum(weekd_jan_1) + 2,
};
if (day_of_year < first_monday)
try writer.writeAll("00")
else
try writer.print("{d:0>2}", .{(day_of_year + 7 - first_monday) / 7});
},
'x' => try self.strftime(writer, "%m/%d/%y"),
'X' => try self.strftime(writer, "%H:%M:%S"),
'y' => {
var buf: [16]u8 = undefined;
_ = try std.fmt.bufPrint(&buf, "{d:0>16}", .{self.year});
try writer.writeAll(buf[14..16]);
},
'Y' => try writer.print("{d}", .{self.year}),
'z' => {
const hours = absHoursFromSeconds(self.offset);
const minutes = absMinutesFromSeconds(self.offset);
if (self.offset < 0)
try writer.print("-{d:0>2}{d:0>2}", .{ hours, minutes })
else
try writer.print("+{d:0>2}{d:0>2}", .{ hours, minutes });
},
'Z' => try writer.writeAll(self.designation),
else => return error.UnknownSpecifier,
}
}
}
/// Format using golang magic date format.
pub fn gofmt(self: Time, writer: *std.Io.Writer, fmt: []const u8) !void {
var i: usize = 0;
while (i < fmt.len) : (i += 1) {
const b = fmt[i];
switch (b) {
'J' => { // Jan, January
if (std.mem.startsWith(u8, fmt[i..], "January")) {
try writer.writeAll(self.month.name());
i += 6;
} else if (std.mem.startsWith(u8, fmt[i..], "Jan")) {
try writer.writeAll(self.month.shortName());
i += 2;
} else try writer.writeByte(b);
},
'M' => { // Monday, Mon, MST
if (std.mem.startsWith(u8, fmt[i..], "Monday")) {
const days = daysFromCivil(
.{ .year = self.year, .month = self.month, .day = self.day },
);
const weekday = weekdayFromDays(days);
try writer.writeAll(weekday.name());
i += 5;
} else if (std.mem.startsWith(u8, fmt[i..], "Mon")) {
if (i + 3 >= fmt.len) {
const days = daysFromCivil(
.{ .year = self.year, .month = self.month, .day = self.day },
);
const weekday = weekdayFromDays(days);
try writer.writeAll(weekday.shortName());
i += 2;
} else if (!std.ascii.isLower(fmt[i + 3])) {
// We only write "Mon" if the next char is *not* a lowercase
const days = daysFromCivil(
.{ .year = self.year, .month = self.month, .day = self.day },
);
const weekday = weekdayFromDays(days);
try writer.writeAll(weekday.shortName());
i += 2;
}
} else if (std.mem.startsWith(u8, fmt[i..], "MST")) {
try writer.writeAll(self.designation);
i += 2;
} else try writer.writeByte(b);
},
'0' => { // 01, 02, 03, 04, 05, 06, 002
if (i == fmt.len - 1) {
try writer.writeByte(b);
continue;
}
i += 1;
const b2 = fmt[i];
switch (b2) {
'1' => try writer.print("{d:0>2}", .{@intFromEnum(self.month)}),
'2' => try writer.print("{d:0>2}", .{self.day}),
'3' => {
if (self.hour == 0)
try writer.writeAll("12")
else if (self.hour > 12)
try writer.print("{d:0>2}", .{self.hour - 12})
else
try writer.print("{d:0>2}", .{self.hour});
},
'4' => try writer.print("{d:0>2}", .{self.minute}),
'5' => try writer.print("{d:0>2}", .{self.second}),
'6' => {
var buf: [16]u8 = undefined;
_ = try std.fmt.bufPrint(&buf, "{d:0>16}", .{self.year});
try writer.writeAll(buf[14..16]);
},
else => {
if (std.mem.startsWith(u8, fmt[i..], "02")) {
i += 1;
const before_month = self.month.daysBefore(self.year);
try writer.print("{d:0>3}", .{self.day + before_month});
} else {
try writer.writeByte(b);
try writer.writeByte(b2);
}
},
}
},
'1' => { // 15, 1
if (std.mem.startsWith(u8, fmt[i..], "15")) {
i += 1;
try writer.print("{d:0>2}", .{self.hour});
} else {
try writer.print("{d}", .{@intFromEnum(self.month)});
}
},
'2' => { // 2006, 2
if (std.mem.startsWith(u8, fmt[i..], "2006")) {
i += 3;
if (self.year < 0)
try writer.print("{d}", .{self.year})
else
try writer.print("{d}", .{@as(u32, @intCast(self.year))});
} else try writer.print("{d}", .{self.day});
},
'_' => { // _2, __2
if (std.mem.startsWith(u8, fmt[i..], "_2")) {
i += 1;
try writer.print("{d: >2}", .{self.day});
} else if (std.mem.startsWith(u8, fmt[i..], "__2")) {
i += 2;
const before_month = self.month.daysBefore(self.year);
try writer.print("{d: >3}", .{self.day + before_month});
} else try writer.writeByte(b);
},
'3' => {
if (self.hour == 0)
try writer.writeAll("12")
else if (self.hour > 12)
try writer.print("{d}", .{self.hour - 12})
else
try writer.print("{d}", .{self.hour});
},
'4' => try writer.print("{d}", .{self.minute}),
'5' => try writer.print("{d}", .{self.second}),
'P' => {
if (i + 1 < fmt.len and fmt[i + 1] == 'M') {
i += 1;
if (self.hour >= 12)
try writer.writeAll("PM")
else
try writer.writeAll("AM");
} else try writer.writeByte(b);
},
'p' => {
if (i + 1 < fmt.len and fmt[i + 1] == 'm') {
i += 1;
if (self.hour >= 12)
try writer.writeAll("pm")
else
try writer.writeAll("am");
} else try writer.writeByte(b);
},
'-', 'Z' => { // -070000, -07:00:00, -0700, -07:00, -07
if (i == fmt.len - 1) {
try writer.writeByte(b);
continue;
}
if (std.mem.startsWith(u8, fmt[i + 1 ..], "070000")) {
i += 6;
if (self.offset == 0 and b == 'Z') {
try writer.writeByte('Z');
continue;
}
const hours = absHoursFromSeconds(self.offset);
const minutes = absMinutesFromSeconds(self.offset);
const seconds = absSecondsFromSeconds(self.offset);
const sign: u8 = if (self.offset < 0) '-' else '+';
try writer.print("{c}{d:0>2}{d:0>2}{d:0>2}", .{ sign, hours, minutes, seconds });
} else if (std.mem.startsWith(u8, fmt[i + 1 ..], "07:00:00")) {
i += 8;
if (self.offset == 0 and b == 'Z') {
try writer.writeByte('Z');
continue;
}
const hours = absHoursFromSeconds(self.offset);
const minutes = absMinutesFromSeconds(self.offset);
const seconds = absSecondsFromSeconds(self.offset);
const sign: u8 = if (self.offset < 0) '-' else '+';
try writer.print("{c}{d:0>2}:{d:0>2}:{d:0>2}", .{ sign, hours, minutes, seconds });
} else if (std.mem.startsWith(u8, fmt[i + 1 ..], "0700")) {
i += 4;
if (self.offset == 0 and b == 'Z') {
try writer.writeByte('Z');
continue;
}
const hours = absHoursFromSeconds(self.offset);
const minutes = absMinutesFromSeconds(self.offset);
const sign: u8 = if (self.offset < 0) '-' else '+';
try writer.print("{c}{d:0>2}{d:0>2}", .{ sign, hours, minutes });
} else if (std.mem.startsWith(u8, fmt[i + 1 ..], "07:00")) {
i += 5;
if (self.offset == 0 and b == 'Z') {
try writer.writeByte('Z');
continue;
}
const hours = absHoursFromSeconds(self.offset);
const minutes = absMinutesFromSeconds(self.offset);
const sign: u8 = if (self.offset < 0) '-' else '+';
try writer.print("{c}{d:0>2}:{d:0>2}", .{ sign, hours, minutes });
} else if (std.mem.startsWith(u8, fmt[i + 1 ..], "07")) {
i += 2;
if (self.offset == 0 and b == 'Z') {
try writer.writeByte('Z');
continue;
}
const hours = absHoursFromSeconds(self.offset);
const sign: u8 = if (self.offset < 0) '-' else '+';
try writer.print("{c}{d:0>2}", .{ sign, hours });
} else try writer.writeByte(b);
},
'.', ',' => { // ,000, or .000, or ,999, or .999 - repeated digits for fractional seconds.
try writer.writeByte(b);
if (i == fmt.len - 1) continue;
const c = fmt[i + 1];
switch (c) {
'0' => {
var n: usize = 0;
const j: usize = i + 1;
while (j + n < fmt.len and fmt[j + n] == '0') : (n += 1) {}
// If we ended on a digit, it wasn't a 0. That means this was not a
// valid fractional second
if (j + n < fmt.len and std.ascii.isDigit(fmt[j + n])) continue;
i += j + n;
var buf: [9]u8 = undefined;
const str = try std.fmt.bufPrint(
&buf,
"{d:0>3}{d:0>3}{d:0>3}",
.{ self.millisecond, self.microsecond, self.nanosecond },
);
try writer.writeAll(str[0..@min(n, str.len)]);
if (n > str.len)
try writer.splatByteAll('0', n - str.len);
},
'9' => {
var n: usize = 0;
const j: usize = i + 1;
while (j + n < fmt.len and fmt[j + n] == '9') : (n += 1) {}
// If we ended on a digit, it wasn't a 0. That means this was not a
// valid fractional second
if (j + n < fmt.len and std.ascii.isDigit(fmt[j + n])) continue;
i += j + n;
var buf: [9]u8 = undefined;
const str = try std.fmt.bufPrint(
&buf,
"{d:0>3}{d:0>3}{d:0>3}",
.{ self.millisecond, self.microsecond, self.nanosecond },
);
var iter = std.mem.reverseIterator(str[0..@min(n, str.len)]);
var last_non_zero = @min(n, str.len);
while (iter.next()) |d| {
if (d != '0') break;
last_non_zero -= 1;
}
try writer.writeAll(str[0..last_non_zero]);
},
else => continue,
}
},
'N' => {
if (std.mem.startsWith(u8, fmt[i..], "ND")) {
i += 1;
switch (self.day) {
0, 4...20, 24...30 => try writer.writeAll("TH"),
1, 21, 31 => try writer.writeAll("ST"),
2, 22 => try writer.writeAll("ND"),
3, 23 => try writer.writeAll("RD"),
}
} else try writer.writeByte(b);
},
'n' => {
if (std.mem.startsWith(u8, fmt[i..], "nd")) {
i += 1;
switch (self.day) {
0, 4...20, 24...30 => try writer.writeAll("th"),
1, 21, 31 => try writer.writeAll("st"),
2, 22 => try writer.writeAll("nd"),
3, 23 => try writer.writeAll("rd"),
}
} else try writer.writeByte(b);
},
else => try writer.writeByte(b),
}
}
}
fn absHoursFromSeconds(seconds: Seconds) u32 {
if (seconds < 0)
return @intCast(@divTrunc(-seconds, 60 * 60))
else
return @intCast(@divTrunc(seconds, 60 * 60));
}
fn absMinutesFromSeconds(seconds: Seconds) u32 {
const hours = absHoursFromSeconds(seconds);
if (seconds < 0)
return @intCast(@divTrunc((-seconds) - hours * 3600, 60))
else
return @intCast(@divTrunc(seconds - hours * 3600, 60));
}
fn absSecondsFromSeconds(seconds: Seconds) u32 {
const hours = absHoursFromSeconds(seconds);
const minutes = absMinutesFromSeconds(seconds);
if (seconds < 0)
return @intCast(@divTrunc((-seconds) - hours * 3600 - minutes * 60, 1))
else
return @intCast(@divTrunc(seconds - hours * 3600 - minutes * 60, 1));
}
pub fn compare(self: Time, time: Time) TimeComparison {
const self_instant = self.instant();
const time_instant = time.instant();
if (self_instant.timestamp > time_instant.timestamp) {
return .after;
} else if (self_instant.timestamp < time_instant.timestamp) {
return .before;
} else {
return .equal;
}
}
pub fn after(self: Time, time: Time) bool {
const self_instant = self.instant();
const time_instant = time.instant();
return self_instant.timestamp > time_instant.timestamp;
}
pub fn before(self: Time, time: Time) bool {
const self_instant = self.instant();
const time_instant = time.instant();
return self_instant.timestamp < time_instant.timestamp;
}
pub fn eql(self: Time, time: Time) bool {
const self_instant = self.instant();
const time_instant = time.instant();
return self_instant.timestamp == time_instant.timestamp;
}
};
/// Returns the number of days since the Unix epoch. timestamp should be the number of seconds from
/// the Unix epoch
pub fn daysSinceEpoch(timestamp: Seconds) Days {
return @intCast(@divFloor(timestamp, s_per_day));
}
test "days since epoch" {
try std.testing.expectEqual(0, daysSinceEpoch(0));
try std.testing.expectEqual(0, daysSinceEpoch(1));
try std.testing.expectEqual(-1, daysSinceEpoch(-1));
try std.testing.expectEqual(-2, daysSinceEpoch(-(s_per_day + 1)));
try std.testing.expectEqual(1, daysSinceEpoch(s_per_day + 1));
try std.testing.expectEqual(19797, daysSinceEpoch(1710523947));
}
pub fn isLeapYear(year: i32) bool {
// Neri/Schneider algorithm
const d: i32 = if (@mod(year, 100) != 0) 4 else 16;
return (year & (d - 1)) == 0;
}
/// returns the weekday given a number of days since the unix epoch
/// https://howardhinnant.github.io/date_algorithms.html#weekday_from_days
pub fn weekdayFromDays(days: Days) Weekday {
return @enumFromInt(@mod((days + 4), 7));
}
test "weekdayFromDays" {
try std.testing.expectEqual(.thu, weekdayFromDays(0));
try std.testing.expectEqual(.sat, weekdayFromDays(-5));
try std.testing.expectEqual(.wed, weekdayFromDays(-8));
}
/// Ben Joffe's very fast 64-bit date algorithm
/// https://www.benjoffe.com/fast-date-64
pub fn civilFromDays(days: Days) Date {
const is_arm = builtin.cpu.arch == .aarch64 or builtin.cpu.arch == .arm;
const eras: u64 = 4726498270;
const d_shift: u64 = 146097 * eras - 719469;
const y_shift: u64 = 400 * eras - 1;
const scale: u64 = if (is_arm) 1 else 32;
const shift_0: u64 = 30556 * scale;
const shift_1: u64 = 5980 * scale;
const c1: u64 = 505054698555331;
const c2: u64 = 50504432782230121;
const c3: u64 = 8619973866219416 * 32 / scale;
// Adjust for 100/400 leap year rule, reverse day count
const rev: u64 = d_shift -% @as(u64, @bitCast(@as(i64, days)));
const cen: u64 = @truncate(@as(u128, c1) * rev >> 64);
const jul: u64 = rev +% cen -% cen / 4;
// Determine year and year-part
const num: u128 = @as(u128, c2) * jul;
const yrs: u64 = y_shift -% @as(u64, @truncate(num >> 64));
const low: u64 = @truncate(num);
const ypt: u64 = @truncate(@as(u128, 24451 * scale) * low >> 64);
// Year-modulo-bitshift for leap years
const bump: u64, const shift: u64, const month: u64 = if (is_arm) blk: {
const s: u64 = shift_0;
const n: u64 = (yrs % 4) * (16 * scale) +% s -% ypt;
const m: u64 = n / (2048 * scale);
break :blk .{ if (m > 12) 1 else 0, s, if (m > 12) m - 12 else m };
} else blk: {
const b: u64 = if (ypt < 3952 * scale) 1 else 0;
const s: u64 = if (b == 1) shift_1 else shift_0;
const n: u64 = (yrs % 4) * (16 * scale) +% s -% ypt;
break :blk .{ b, s, n / (2048 * scale) };
};
const n: u64 = (yrs % 4) * (16 * scale) +% shift -% ypt;
const day: u64 = @truncate(@as(u128, c3) * (n % (2048 * scale)) >> 64);
return .{
.year = @intCast(@as(i64, @bitCast(yrs)) + @as(i64, @intCast(bump))),
.month = @enumFromInt(month),
.day = @intCast(day + 1),
};
}
test "civilFromDays" {
// trigger a doe and yoe range asserts if the era is not computed correctly
try std.testing.expectEqual(0, civilFromDays(-719469).year);
try std.testing.expectEqual(1970, civilFromDays(0).year);
}
/// Ben Joffe's fast overflow-safe inverse function
/// https://www.benjoffe.com/fast-date-64
pub fn daysFromCivil(date: Date) Days {
const month: u32 = @intFromEnum(date.month);
const bump: u32 = if (month <= 2) 1 else 0;
const yrs: u32 = @bitCast(date.year +% 5880000 - @as(i32, @intCast(bump)));
const cen: u32 = yrs / 100;
const shift: i32 = if (bump == 1) 8829 else -2919;
const year_days: u32 = yrs * 365 + yrs / 4 - cen + cen / 4;
const month_days: u32 = @bitCast(@divFloor(979 * @as(i32, @intCast(month)) + shift, 32));
return @bitCast(year_days +% month_days +% date.day -% 2148345369);
}
test "daysFromCivil" {
// -1 is not a leap year, so Feb 28th and Mar 1st should be 1 day apart
try std.testing.expectEqual(false, isLeapYear(-1));
try std.testing.expectEqual(
1,
daysFromCivil(.{ .year = -1, .month = .mar, .day = 1 }) -
daysFromCivil(.{ .year = -1, .month = .feb, .day = 28 }),
);
try std.testing.expectEqual(-719893, daysFromCivil(.{ .year = -1, .month = .jan, .day = 1 }));
}
test {
std.testing.refAllDecls(@This());
_ = @import("timezone.zig");
}
test "fmtStrftime" {
var buf: [128]u8 = undefined;
const epoch = instant(.{ .unix_timestamp = 0 }, &utc);
const time = epoch.time();
var writer = std.Io.Writer.fixed(&buf);
try std.testing.expectError(error.InvalidFormat, time.strftime(&writer, "no trailing lone percent %"));
writer.end = 0;
try time.strftime(&writer, "%%");
try std.testing.expectEqualStrings("%", writer.buffered());
writer.end = 0;
try time.strftime(&writer, "%a %A %b %B %c %C");
try std.testing.expectEqualStrings("Thu Thursday Jan January Thu Jan 1 00:00:00 1970 19", writer.buffered());
writer.end = 0;
try time.strftime(&writer, "%d %D %e %F %h");
try std.testing.expectEqualStrings("01 01/01/70 1 1970-01-01 Jan", writer.buffered());
writer.end = 0;
try time.strftime(&writer, "%H %I %j %k %l %m %M");
try std.testing.expectEqualStrings("00 12 001 0 12 01 00", writer.buffered());
writer.end = 0;
try time.strftime(&writer, "%p %P %r %R %s %S");
try std.testing.expectEqualStrings("AM am 12:00:00 AM 00:00 0 00", writer.buffered());
writer.end = 0;
try time.strftime(&writer, "%T %u");
try std.testing.expectEqualStrings("00:00:00 4", writer.buffered());
writer.end = 0;
try time.strftime(&writer, "%U");
try std.testing.expectEqualStrings("00", writer.buffered());
writer.end = 0;
const d2 = (try time.instant().add(.{ .days = 3 })).time();
try d2.strftime(&writer, "%U");
try std.testing.expectEqualStrings("01", writer.buffered());
writer.end = 0;
try time.strftime(&writer, "%w %W %x %X %y %Y %z %Z");
try std.testing.expectEqualStrings("4 00 01/01/70 00:00:00 70 1970 +0000 UTC", writer.buffered());
writer.end = 0;
var d3 = time;
d3.offset = -3600;
try d3.strftime(&writer, "%z");
try std.testing.expectEqualStrings("-0100", writer.buffered());
}
test "gofmt" {
var buf: [128]u8 = undefined;
var writer = std.Io.Writer.fixed(&buf);
const time: Time = .{
.year = 1970,
.month = .feb,
.day = 3,
.designation = "UTC",
};
writer.end = 0;
try time.gofmt(&writer, "Jan January J 01 02 03 04 05 06 002 Jan");
try std.testing.expectEqualStrings("Feb February J 02 03 12 00 00 70 034 Feb", writer.buffered());
writer.end = 0;
try time.gofmt(&writer, "Mon Monday MST M 1 15 2 2006 _2 __2 Mon");
try std.testing.expectEqualStrings("Tue Tuesday UTC M 2 00 3 1970 3 34 Tue", writer.buffered());
writer.end = 0;
try time.gofmt(&writer, "3 4 5");
try std.testing.expectEqualStrings("12 0 0", writer.buffered());
const time2: Time = .{
.offset = 3661, // 1 hour, 1 minute, 1 second
.millisecond = 123,
.microsecond = 456,
.nanosecond = 789,
};
writer.end = 0;
try time2.gofmt(&writer, "-070000 -07:00:00 -0700 -07:00 -07 -00");
try std.testing.expectEqualStrings("+010101 +01:01:01 +0101 +01:01 +01 -00", writer.buffered());
writer.end = 0;
try time2.gofmt(&writer, "Z070000 Z07:00:00 Z0700 Z07:00 Z07 Z00");
try std.testing.expectEqualStrings("+010101 +01:01:01 +0101 +01:01 +01 Z00", writer.buffered());
writer.end = 0;
try time.gofmt(&writer, "Z070000 Z07:00:00 Z0700 Z07:00 Z07 Z00");
try std.testing.expectEqualStrings("Z Z Z Z Z Z00", writer.buffered());
writer.end = 0;
try time2.gofmt(&writer, "frac .");
try std.testing.expectEqualStrings("frac .", writer.buffered());
writer.end = 0;
try time2.gofmt(&writer, "frac .000000000");
try std.testing.expectEqualStrings("frac .123456789", writer.buffered());
writer.end = 0;
try time2.gofmt(&writer, "frac .999999999");
try std.testing.expectEqualStrings("frac .123456789", writer.buffered());
writer.end = 0;
try time2.gofmt(&writer, "frac .000000000000");
try std.testing.expectEqualStrings("frac .123456789000", writer.buffered());
writer.end = 0;
try time2.gofmt(&writer, "frac .0000000");
try std.testing.expectEqualStrings("frac .1234567", writer.buffered());
const time3: Time = .{
.offset = 3661, // 1 hour, 1 minute, 1 second
.millisecond = 123,
.microsecond = 456,
};
writer.end = 0;
try time3.gofmt(&writer, "frac .999999999");
try std.testing.expectEqualStrings("frac .123456", writer.buffered());
}
test "Time.timeFmt" {
const time: Time = .{
.year = 1970,
.month = .feb,
.day = 3,
.designation = "UTC",
};
try std.testing.expectFmt(
"Feb February J 02 03 12 00 00 70 034 Feb",
"{f}",
.{time.timeFmt(.gofmt, "Jan January J 01 02 03 04 05 06 002 Jan")},
);
try std.testing.expectFmt(
"Tue Tuesday UTC M 2 00 3 1970 3 34 Tue",
"{f}",
.{time.timeFmt(.gofmt, "Mon Monday MST M 1 15 2 2006 _2 __2 Mon")},
);
try std.testing.expectFmt(
"12 0 0",
"{f}",
.{time.timeFmt(.gofmt, "3 4 5")},
);
const time2: Time = .{
.offset = 3661, // 1 hour, 1 minute, 1 second
.millisecond = 123,
.microsecond = 456,
.nanosecond = 789,
};
try std.testing.expectFmt(
"+010101 +01:01:01 +0101 +01:01 +01 -00",
"{f}",
.{time2.timeFmt(.gofmt, "-070000 -07:00:00 -0700 -07:00 -07 -00")},
);
try std.testing.expectFmt(
"+010101 +01:01:01 +0101 +01:01 +01 Z00",
"{f}",
.{time2.timeFmt(.gofmt, "Z070000 Z07:00:00 Z0700 Z07:00 Z07 Z00")},
);
try std.testing.expectFmt(
"Z Z Z Z Z Z00",
"{f}",
.{time.timeFmt(.gofmt, "Z070000 Z07:00:00 Z0700 Z07:00 Z07 Z00")},
);
try std.testing.expectFmt(
"frac .",
"{f}",
.{time2.timeFmt(.gofmt, "frac .")},
);
const time3: Time = .{
.offset = 3661, // 1 hour, 1 minute, 1 second
.millisecond = 123,
.microsecond = 456,
};
try std.testing.expectFmt(
"frac .123456",
"{f}",
.{time3.timeFmt(.gofmt, "frac .999999999")},
);
const epoch = instant(.{ .unix_timestamp = 0 }, &utc);
const time4 = epoch.time();
var buf: [128]u8 = undefined;
var writer = std.Io.Writer.fixed(&buf);
try std.testing.expectError(error.WriteFailed, writer.print(
"{f}",
.{time4.timeFmt(.strftime, "no trailing lone percent %")},
));
try std.testing.expectFmt(
"%",
"{f}",
.{time4.timeFmt(.strftime, "%%")},
);
try std.testing.expectFmt(
"Thu Thursday Jan January Thu Jan 1 00:00:00 1970 19",
"{f}",
.{time4.timeFmt(.strftime, "%a %A %b %B %c %C")},
);
try std.testing.expectFmt(
"01 01/01/70 1 1970-01-01 Jan",
"{f}",
.{time4.timeFmt(.strftime, "%d %D %e %F %h")},
);
}
test Instant {
const zeit = @This();
const alloc = std.testing.allocator;
// Get an instant in time. The default gets "now" in UTC
const now = instant(.{ .now = std.testing.io }, &utc);
// Load our local timezone. This needs an allocator. Optionally pass in an
// EnvConfig to support TZ and TZDIR environment variables
const local_tz = try zeit.local(alloc, std.testing.io, .{});
defer local_tz.deinit();
// Convert our instant to a new timezone
const now_local = now.in(&local_tz);
// Generate date/time info for this instant
const dt = now_local.time();
// Print it out
std.log.info("{}", .{dt});
// zeit.Time{
// .year = 2024,
// .month = zeit.Month.mar,
// .day = 16,
// .hour = 8,
// .minute = 38,
// .second = 29,
// .millisecond = 496,
// .microsecond = 706,
// .nanosecond = 64
// .offset = -18000,
// }
var discard_buf: [256]u8 = undefined;
var discarding: std.Io.Writer.Discarding = .init(&discard_buf);
// Format using strftime specifier. Format strings are not required to be comptime
try dt.strftime(&discarding.writer, "%Y-%m-%d %H:%M:%S %Z");
// Or...golang magic date specifiers. Format strings are not required to be comptime
try dt.gofmt(&discarding.writer, "2006-01-02 15:04:05 MST");
// Load an arbitrary location using IANA location syntax. The location name
// comes from an enum which will automatically map IANA location names to
// Windows names, as needed. Pass an optional EnvConfig to support TZDIR
const vienna = try zeit.loadTimeZone(alloc, std.testing.io, .@"Europe/Vienna", .{});
defer vienna.deinit();
// Parse an Instant from an ISO8601 or RFC3339 string
_ = try zeit.instantFromText(
.iso8601,
"2024-03-16T08:38:29.496-1200",
&utc,
);
_ = try zeit.instantFromText(
.rfc3339,
"2024-03-16T08:38:29.496706064-1200",
&utc,
);
}
test "github.com/rockorager/zeit/issues/15" {
// https://github.com/rockorager/zeit/issues/15
const timestamp = 1732838300;
const tz = try loadTimeZone(std.testing.allocator, std.testing.io, .@"Europe/Berlin", .{});
defer tz.deinit();
const inst = instant(.{ .unix_timestamp = timestamp }, &tz);
const time = inst.time();
try std.testing.expectEqual(timestamp, time.instant().unixTimestamp());
try std.testing.expectEqual(2024, time.year);
try std.testing.expectEqual(Month.nov, time.month);
try std.testing.expectEqual(29, time.day);
try std.testing.expectEqual(0, time.hour);
try std.testing.expectEqual(58, time.minute);
try std.testing.expectEqual(20, time.second);
var aw: std.Io.Writer.Allocating = .init(std.testing.allocator);
defer aw.deinit();
try time.strftime(&aw.writer, "%a %A %u");
try std.testing.expectEqualStrings("Fri Friday 5", aw.writer.buffered());
aw.writer.end = 0;
try time.gofmt(&aw.writer, "Mon Monday");
try std.testing.expectEqualStrings("Fri Friday", aw.writer.buffered());
}
test "github.com/rockorager/zeit/issues/27" {
// April 23, 2025
const timestamp = 1745414170;
const inst = instant(.{ .unix_timestamp = timestamp }, &utc);
var aw: std.Io.Writer.Allocating = .init(std.testing.allocator);
defer aw.deinit();
const time = inst.time();
try time.gofmt(&aw.writer, "02.01.2006");
try std.testing.expectEqualStrings("23.04.2025", aw.writer.buffered());
}
test "github.com/rockorager/zeit/issues/24" {
// April 23, 2025
const timestamp = 1745414170;
const inst = instant(.{ .unix_timestamp = timestamp }, &utc);
var aw: std.Io.Writer.Allocating = .init(std.testing.allocator);
defer aw.deinit();
const time = inst.time();
try time.gofmt(&aw.writer, "3pm MST");
try std.testing.expectEqualStrings("1pm UTC", aw.writer.buffered());
aw.writer.end = 0;
try time.gofmt(&aw.writer, "3p MST");
try std.testing.expectEqualStrings("1p UTC", aw.writer.buffered());
}
test "github.com/rockorager/zeit/issues/26" {
// April 23, 2025
const timestamp = 1745414170;
const inst = instant(.{ .unix_timestamp = timestamp }, &utc);
var aw: std.Io.Writer.Allocating = .init(std.testing.allocator);
defer aw.deinit();
const time = inst.time();
try time.gofmt(&aw.writer, "02nd");
try std.testing.expectEqualStrings("23rd", aw.writer.buffered());
aw.writer.end = 0;
try time.gofmt(&aw.writer, "02ND");
try std.testing.expectEqualStrings("23RD", aw.writer.buffered());
}
test "bufPrintRFC3339Nano" {
const Case = struct {
timestamp: []const u8,
};
const cases = [_]Case{
.{ .timestamp = "2023-01-15T23:45:51.123456789Z" },
.{ .timestamp = "2023-01-15T23:45:51.000000789Z" },
.{ .timestamp = "2023-01-15T00:00:00.000000000Z" },
.{ .timestamp = "2023-01-15T00:00:00.001002003Z" },
};
for (cases) |case| {
const iso = try instantFromText(
.rfc3339,
case.timestamp,
&utc,
);
var timeBuf: [35]u8 = undefined;
const time = try iso.time().bufPrint(&timeBuf, .rfc3339Nano);
try std.testing.expectEqualStrings(case.timestamp, time);
}
}
test "bufPrintRFC3339Nano with offset" {
var timeBuf: [35]u8 = undefined;
// positive offset +05:30
const t1 = Time{
.year = 2023,
.month = .jan,
.day = 15,
.hour = 23,
.minute = 45,
.second = 51,
.millisecond = 123,
.microsecond = 456,
.nanosecond = 789,
.offset = 5 * 3600 + 30 * 60,
};
const s1 = try t1.bufPrint(&timeBuf, .rfc3339Nano);
try std.testing.expectEqualStrings("2023-01-15T23:45:51.123456789+05:30", s1);
// negative offset -08:00
const t2 = Time{
.year = 2023,
.month = .jan,
.day = 15,
.hour = 23,
.minute = 45,
.second = 51,
.millisecond = 123,
.microsecond = 456,
.nanosecond = 789,
.offset = -(8 * 3600),
};
const s2 = try t2.bufPrint(&timeBuf, .rfc3339Nano);
try std.testing.expectEqualStrings("2023-01-15T23:45:51.123456789-08:00", s2);
}