commit e9694648b3e6896b9a7ae9b0ad69417f5cb25710 Author: Jeffrey C. Ollie Date: Sat Oct 14 12:39:16 2023 -0500 first diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..93c1b5f --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +/zig-cache +/zig-out diff --git a/build.zig b/build.zig new file mode 100644 index 0000000..db6e814 --- /dev/null +++ b/build.zig @@ -0,0 +1,47 @@ +const std = @import("std"); + +// Although this function looks imperative, note that its job is to +// declaratively construct a build graph that will be executed by an external +// runner. +pub fn build(b: *std.Build) void { + // Standard target options allows the person running `zig build` to choose + // what target to build for. Here we do not override the defaults, which + // means any target is allowed, and the default is native. Other options + // for restricting supported target set are available. + const target = b.standardTargetOptions(.{}); + + // Standard optimization options allow the person running `zig build` to select + // between Debug, ReleaseSafe, ReleaseFast, and ReleaseSmall. Here we do not + // set a preferred release mode, allowing the user to decide how to optimize. + const optimize = b.standardOptimizeOption(.{}); + + const lib = b.addStaticLibrary(.{ + .name = "zig-datetime", + // In this case the main source file is merely a path, however, in more + // complicated build scripts, this could be a generated file. + .root_source_file = .{ .path = "src/main.zig" }, + .target = target, + .optimize = optimize, + }); + + // This declares intent for the library to be installed into the standard + // location when the user invokes the "install" step (the default step when + // running `zig build`). + b.installArtifact(lib); + + // Creates a step for unit testing. This only builds the test executable + // but does not run it. + const main_tests = b.addTest(.{ + .root_source_file = .{ .path = "src/main.zig" }, + .target = target, + .optimize = optimize, + }); + + const run_main_tests = b.addRunArtifact(main_tests); + + // This creates a build step. It will be visible in the `zig build --help` menu, + // and can be selected like this: `zig build test` + // This will evaluate the `test` step rather than the default, which is "install". + const test_step = b.step("test", "Run library tests"); + test_step.dependOn(&run_main_tests.step); +} diff --git a/build.zig.zon b/build.zig.zon new file mode 100644 index 0000000..c47e993 --- /dev/null +++ b/build.zig.zon @@ -0,0 +1,5 @@ +.{ + .name = "datetime", + .version = "0.0.1", + .paths = .{""}, +} diff --git a/flake.lock b/flake.lock new file mode 100644 index 0000000..1e6803a --- /dev/null +++ b/flake.lock @@ -0,0 +1,239 @@ +{ + "nodes": { + "bash": { + "locked": { + "lastModified": 1697126158, + "narHash": "sha256-XoRmgs8U78oVMVzk4riJpkmXaX1Pk2Ya/wYMmTYt2mA=", + "ref": "refs/heads/main", + "rev": "443dc212854202ddf2bb3bf29ad6d6c1f8829ff6", + "revCount": 11, + "type": "git", + "url": "https://git.ocjtech.us/jeff/nixos-bash-prompt-builder.git" + }, + "original": { + "type": "git", + "url": "https://git.ocjtech.us/jeff/nixos-bash-prompt-builder.git" + } + }, + "flake-compat": { + "flake": false, + "locked": { + "lastModified": 1673956053, + "narHash": "sha256-4gtG9iQuiKITOjNQQeQIpoIB6b16fm+504Ch3sNKLd8=", + "owner": "edolstra", + "repo": "flake-compat", + "rev": "35bb57c0c8d8b62bbfd284272c928ceb64ddbde9", + "type": "github" + }, + "original": { + "owner": "edolstra", + "repo": "flake-compat", + "type": "github" + } + }, + "flake-utils": { + "inputs": { + "systems": "systems" + }, + "locked": { + "lastModified": 1694529238, + "narHash": "sha256-zsNZZGTGnMOf9YpHKJqMSsa0dXbfmxeoJ7xHlrt+xmY=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "ff7b65b44d01cf9ba6a71320833626af21126384", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "flake-utils", + "type": "github" + } + }, + "flake-utils_2": { + "locked": { + "lastModified": 1659877975, + "narHash": "sha256-zllb8aq3YO3h8B/U0/J1WBgAL8EX5yWf5pMj3G0NAmc=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "c0e246b9b83f637f4681389ecabcb2681b4f3af0", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "flake-utils", + "type": "github" + } + }, + "flake-utils_3": { + "inputs": { + "systems": "systems_2" + }, + "locked": { + "lastModified": 1694529238, + "narHash": "sha256-zsNZZGTGnMOf9YpHKJqMSsa0dXbfmxeoJ7xHlrt+xmY=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "ff7b65b44d01cf9ba6a71320833626af21126384", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "flake-utils", + "type": "github" + } + }, + "gitignore": { + "inputs": { + "nixpkgs": [ + "zls", + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1694102001, + "narHash": "sha256-vky6VPK1n1od6vXbqzOXnekrQpTL4hbPAwUhT5J9c9E=", + "owner": "hercules-ci", + "repo": "gitignore.nix", + "rev": "9e21c80adf67ebcb077d75bd5e7d724d21eeafd6", + "type": "github" + }, + "original": { + "owner": "hercules-ci", + "repo": "gitignore.nix", + "type": "github" + } + }, + "langref": { + "flake": false, + "locked": { + "narHash": "sha256-mYdDCBdNEIeMbavdhSo8qXqW+3fqPC8BAich7W3umrI=", + "type": "file", + "url": "https://raw.githubusercontent.com/ziglang/zig/63bd2bff12992aef0ce23ae4b344e9cb5d65f05d/doc/langref.html.in" + }, + "original": { + "type": "file", + "url": "https://raw.githubusercontent.com/ziglang/zig/63bd2bff12992aef0ce23ae4b344e9cb5d65f05d/doc/langref.html.in" + } + }, + "make-shell": { + "locked": { + "lastModified": 1634940815, + "narHash": "sha256-P69OmveboXzS+es1vQGS4bt+ckwbeIExqxfGLjGuJqA=", + "owner": "ursi", + "repo": "nix-make-shell", + "rev": "8add91681170924e4d0591b22f294aee3f5516f9", + "type": "github" + }, + "original": { + "owner": "ursi", + "repo": "nix-make-shell", + "type": "github" + } + }, + "nixpkgs": { + "locked": { + "lastModified": 1697059129, + "narHash": "sha256-9NJcFF9CEYPvHJ5ckE8kvINvI84SZZ87PvqMbH6pro0=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "5e4c2ada4fcd54b99d56d7bd62f384511a7e2593", + "type": "github" + }, + "original": { + "id": "nixpkgs", + "ref": "nixos-unstable", + "type": "indirect" + } + }, + "root": { + "inputs": { + "bash": "bash", + "flake-utils": "flake-utils", + "make-shell": "make-shell", + "nixpkgs": "nixpkgs", + "zig": "zig", + "zls": "zls" + } + }, + "systems": { + "locked": { + "lastModified": 1681028828, + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", + "owner": "nix-systems", + "repo": "default", + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "repo": "default", + "type": "github" + } + }, + "systems_2": { + "locked": { + "lastModified": 1681028828, + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", + "owner": "nix-systems", + "repo": "default", + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "repo": "default", + "type": "github" + } + }, + "zig": { + "inputs": { + "flake-compat": "flake-compat", + "flake-utils": "flake-utils_2", + "nixpkgs": [ + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1697285248, + "narHash": "sha256-CPV/LWV/2nJKasawkshdCX1bLRsKGtSSiDoy14jix8c=", + "owner": "mitchellh", + "repo": "zig-overlay", + "rev": "ee376d4012e3c853123ca7083af852313235407c", + "type": "github" + }, + "original": { + "owner": "mitchellh", + "repo": "zig-overlay", + "type": "github" + } + }, + "zls": { + "inputs": { + "flake-utils": "flake-utils_3", + "gitignore": "gitignore", + "langref": "langref", + "nixpkgs": [ + "nixpkgs" + ], + "zig-overlay": [ + "zig" + ] + }, + "locked": { + "lastModified": 1697187644, + "narHash": "sha256-O1u6ysfQXbu2+DOFCLzG8xUNrUxsJ1v1GajTgfWWVm0=", + "owner": "zigtools", + "repo": "zls", + "rev": "8673e747dedf52bd85e165b7be9605a56cb669d5", + "type": "github" + }, + "original": { + "owner": "zigtools", + "repo": "zls", + "type": "github" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 0000000..a08dbf0 --- /dev/null +++ b/flake.nix @@ -0,0 +1,65 @@ +{ + description = "zig-ha"; + + inputs = { + nixpkgs = { + url = "nixpkgs/nixos-unstable"; + }; + flake-utils = { + url = "github:numtide/flake-utils"; + }; + bash = { + url = "git+https://git.ocjtech.us/jeff/nixos-bash-prompt-builder.git"; + }; + make-shell = { + url = "github:ursi/nix-make-shell"; + }; + zig = { + url = "github:mitchellh/zig-overlay"; + inputs.nixpkgs.follows = "nixpkgs"; + }; + zls = { + url = "github:zigtools/zls"; + inputs.nixpkgs.follows = "nixpkgs"; + inputs.zig-overlay.follows = "zig"; + }; + }; + + outputs = { self, nixpkgs, flake-utils, bash, ... }@inputs: + let + systems = builtins.attrNames inputs.zig.packages; + in + flake-utils.lib.eachSystem systems ( + system: + let + pkgs = import nixpkgs { + inherit system; + }; + in + { + devShells.default = + let + project = "zig-datetime"; + prompt = ( + bash.build_ps1_prompt + bash.ansi_normal_blue + "${project} - ${bash.username}@${bash.hostname_short}: ${bash.current_working_directory}" + "${project}:${bash.current_working_directory}" + ); + make-shell = import inputs.make-shell { + inherit system; + pkgs = pkgs; + }; + in + make-shell { + packages = [ + inputs.zig.packages.${system}.master + # inputs.zls.packages.${system}.zls + ]; + env = { + PS1 = prompt; + }; + }; + } + ); +} diff --git a/src/main.zig b/src/main.zig new file mode 100644 index 0000000..b11eb12 --- /dev/null +++ b/src/main.zig @@ -0,0 +1,1037 @@ +const std = @import("std"); + +pub const Day = u6; + +pub const Month = enum(u4) { + Jan = 1, + Feb = 2, + Mar = 3, + Apr = 4, + May = 5, + Jun = 6, + Jul = 7, + Aug = 8, + Sep = 9, + Oct = 10, + Nov = 11, + Dec = 12, + + const Self = @This(); + + pub fn monthNumber(self: Self) u4 { + return switch (self) { + .Jan => 1, + .Feb => 2, + .Mar => 3, + .Apr => 4, + .May => 5, + .Jun => 6, + .Jul => 7, + .Aug => 8, + .Sep => 9, + .Oct => 10, + .Nov => 11, + .Dec => 12, + }; + } + + pub fn next(self: Self) Self { + return switch (self) { + .Jan => .Feb, + .Feb => .Mar, + .Mar => .Apr, + .Apr => .May, + .May => .Jun, + .Jun => .Jul, + .Jul => .Aug, + .Aug => .Sep, + .Sep => .Oct, + .Oct => .Nov, + .Nov => .Dec, + .Dec => .Jan, + }; + } + + pub fn prev(self: Self) Self { + return switch (self) { + .Jan => .Dec, + .Feb => .Jan, + .Mar => .Feb, + .Apr => .Mar, + .May => .Apr, + .Jun => .May, + .Jul => .Jun, + .Aug => .Jul, + .Sep => .Aug, + .Oct => .Sep, + .Nov => .Oct, + .Dec => .Nov, + }; + } + + pub fn lastDay(self: Self, year: Year) Day { + return switch (self) { + .Jan => 31, + .Feb => if (isLeap(year)) 29 else 28, + .Mar => 31, + .Apr => 30, + .May => 31, + .Jun => 30, + .Jul => 31, + .Aug => 31, + .Sep => 30, + .Oct => 31, + .Nov => 30, + .Dec => 31, + }; + } + + pub fn quarter(self: Self) u3 { + return switch (self) { + .Jan => 1, + .Feb => 1, + .Mar => 1, + .Apr => 2, + .May => 2, + .Jun => 2, + .Jul => 3, + .Aug => 3, + .Sep => 3, + .Oct => 4, + .Nov => 4, + .Dec => 4, + }; + } + + pub fn shortName(self: Self) []const u8 { + return switch (self) { + .Jan => "Jan", + .Feb => "Feb", + .Mar => "Mar", + .Apr => "Apr", + .May => "May", + .Jun => "Jun", + .Jul => "Jul", + .Aug => "Aug", + .Sep => "Sep", + .Oct => "Oct", + .Nov => "Nov", + .Dec => "Dec", + }; + } + + pub fn longName(self: Self) []const u8 { + return switch (self) { + .Jan => "January", + .Feb => "Febuary", + .Mar => "March", + .Apr => "April", + .May => "May", + .Jun => "June", + .Jul => "July", + .Aug => "August", + .Sep => "September", + .Oct => "October", + .Nov => "November", + .Dec => "December", + }; + } + + pub fn daysBefore(self: Self, year: i32) u9 { + // var days = 0; + // var month = .Jan; + // while (month != self) { + // days += month.lastDay(year); + // month = month.next(); + // } + // return days; + return switch (self) { + .Jan => 0, + .Feb => 31, + .Mar => if (isLeap(year)) 60 else 59, + .Apr => if (isLeap(year)) 91 else 90, + .May => if (isLeap(year)) 121 else 120, + .Jun => if (isLeap(year)) 152 else 151, + .Jul => if (isLeap(year)) 182 else 181, + .Aug => if (isLeap(year)) 213 else 212, + .Sep => if (isLeap(year)) 244 else 243, + .Oct => if (isLeap(year)) 274 else 273, + .Nov => if (isLeap(year)) 305 else 304, + .Dec => if (isLeap(year)) 335 else 334, + }; + } +}; + +pub const Year = i32; +pub const Second = u6; +pub const Minute = u6; +pub const Hour = u5; + +pub fn writeTwelveHour(hour: Hour, case: enum { lower, upper }, writer: anytype) !void { + if (hour < 12) switch (case) { + .lower => try writer.writeAll("am"), + .upper => try writer.writeAll("AM"), + } else switch (case) { + .lower => try writer.writeAll("pm"), + .upper => try writer.writeAll("PM"), + } +} + +pub const Nanosecond = u30; + +// https://github.com/nektro/zig-time + +pub const DateTime = struct { + nanosecond: Nanosecond, // [0..999999999] + second: Second, // [0..61] + minute: Minute, // [0..59] + hour: Hour, // [0-23] + day: Day, // [0-31] + month: Month, // [1-12] + year: Year, + weekday: DayOfWeek, + + const Self = @This(); + + pub fn format(self: Self, comptime fmt: []const u8, options: std.fmt.FormatOptions, writer: anytype) !void { + _ = options; + + if (fmt.len == 0) @compileError("DateTime: format string can't be empty"); + + @setEvalBranchQuota(100000); + + comptime var s = 0; + comptime var e = 0; + comptime var next: ?FormatSeq = null; + inline for (fmt, 0..) |c, i| { + e = i + 1; + // std.debug.print("test: {s}\n", .{fmt[s..e]}); + if (comptime std.meta.stringToEnum(FormatSeq, fmt[s..e])) |tag| { + // std.debug.print("next {any}\n", .{tag}); + next = tag; + if (i < fmt.len - 1) continue; + } + + if (next) |tag| { + // std.debug.print("tag {any}\n", .{tag}); + switch (tag) { + .M => try writer.print("{}", .{@intFromEnum(self.month)}), + .Mo => try printOrdinal(writer, @intFromEnum(self.month)), + .MO => try printOrdinalSuperscript(writer, @intFromEnum(self.month)), + .Mm => try writer.writeAll(self.month.veryShortName()), + .MM => try writer.print("{:0>2}", .{@intFromEnum(self.month)}), + .MMM => try writer.writeAll(self.month.shortName()), + .MMMM => try writer.writeAll(self.month.longName()), + + .Q => try writer.print("{}", .{self.month.quarter()}), + .Qo => try printOrdinal(writer, self.month.quarter()), + .QO => try printOrdinalSuperscript(writer, self.month.quarter()), + + .D => try writer.print("{}", .{self.day}), + .Do => try printOrdinal(writer, self.day), + .DO => try printOrdinalSuperscript(writer, self.day), + .DD => try writer.print("{:0>2}", .{self.day}), + + .DDD => try writer.print("{}", .{self.dayOfThisYear()}), + .DDDo => try printOrdinal(writer, self.dayOfThisYear()), + .DDDO => try printOrdinalSuperscript(writer, self.dayOfThisYear()), + .DDDD => try writer.print("{:0>3}", .{self.dayOfThisYear()}), + + .d => try writer.print("{}", .{self.weekday.weekdayNumber()}), + .do => try printOrdinal(writer, self.weekday.weekdayNumber()), + .dO => try printOrdinalSuperscript(writer, self.weekday.weekdayNumber()), + .dd => try writer.writeAll(self.weekday.veryShortName()), + .ddd => try writer.writeAll(self.weekday.shortName()), + .dddd => try writer.writeAll(self.weekday.longName()), + + .e => try writer.print("{}", .{self.weekday.weekdayNumber()}), + .E => try writer.print("{}", .{self.weekday.isoWeekdayNumber()}), + + .w => try writer.print("{}", .{self.dayOfThisYear() / 7}), + .wo => try printOrdinal(writer, self.dayOfThisYear() / 7), + .wO => try printOrdinalSuperscript(writer, self.dayOfThisYear() / 7), + .ww => try writer.print("{:0>2}", .{self.dayOfThisYear() / 7}), + + .YY => try writer.print("{d:0>2}", .{self.year % 100}), + .YYY => try writer.print("{d}", .{self.year}), + .YYYY => { + if (self.year < 0) { + try writer.writeAll("-"); + } + try writer.print("{d:0>4}", .{@abs(self.year)}); + }, + + .A => try writeTwelveHour(self.hour, .upper), + .a => try writeTwelveHour(self.hour, .lower), + + .H => try writer.print("{}", .{self.hour}), + .HH => try writer.print("{:0>2}", .{self.hour}), + .h => try writer.print("{}", .{wrap(self.hour, 12)}), + .hh => try writer.print("{:0>2}", .{wrap(self.hour, 12)}), + .k => try writer.print("{}", .{wrap(self.hour, 24)}), + .kk => try writer.print("{:0>2}", .{wrap(self.hour, 24)}), + + .m => try writer.print("{}", .{self.minute}), + .mm => try writer.print("{:0>2}", .{self.minute}), + + .s => try writer.print("{d}", .{self.second}), + .ss => try writer.print("{d:0>2}", .{self.second}), + + .S => try writer.print("{d:>1}", .{self.nanosecond / tenthsPerNanoSecond}), + .SS => try writer.print("{d:0>2}", .{self.nanosecond / hundredthsPerNanoSecond}), + .SSS => try writer.print("{d:0>3}", .{self.nanosecond / milliSecondsPerNanoSecond}), + .SSSS => try writer.print("{d:0>4}", .{self.nanosecond / (milliSecondsPerNanoSecond / 10)}), + .SSSSS => try writer.print("{d:0>5}", .{self.nanosecond / (milliSecondsPerNanoSecond / 100)}), + .SSSSSS => try writer.print("{d:0>6}", .{self.nanosecond / microSecondsPerNanoSecond}), + .SSSSSSS => try writer.print("{d:0>7}", .{self.nanosecond / (microSecondsPerNanoSecond / 10)}), + .SSSSSSSS => try writer.print("{d:0>8}", .{self.nanosecond / (microSecondsPerNanoSecond / 100)}), + .SSSSSSSSS => try writer.print("{d:0>9}", .{self.nanosecond}), + + // .z => try writer.writeAll(@tagName(self.timezone)), + // .Z => try writer.writeAll("+00:00"), + // .ZZ => try writer.writeAll("+0000"), + + // .x => try writer.print("{}", .{self.toUnixMilli()}), + // .X => try writer.print("{}", .{self.toUnix()}), + } + next = null; + s = i; + } + + switch (c) { + ',', ' ', ':', '-', '.', 'T', 'W' => { + try writer.writeAll(&.{c}); + s = i + 1; + continue; + }, + else => {}, + } + } + } + + pub fn formatAlloc(self: Self, alloc: std.mem.Allocator, comptime fmt: []const u8) ![]const u8 { + var list = std.ArrayList(u8).init(alloc); + defer list.deinit(); + + try self.format(fmt, .{}, list.writer()); + return list.toOwnedSlice(); + } + + const FormatSeq = enum { + M, // 1 2 ... 11 12 (month, numeric) + Mo, // 1st 2nd ... 11th 12th (month, numeric ordinal) + MO, // 1ˢᵗ 2ⁿᵈ 3ʳᵈ ... 11ᵗʰ 12ᵗʰ + MM, // 01 02 ... 11 12 (month, numeric ordinal) + Mm, // Ja, Fe, Ma ... No, De (very short month name) + MMM, // Jan Feb ... Nov Dec (short month name) + MMMM, // January February ... November December (long month name) + Q, // 1 2 3 4 (quarter) + Qo, // 1st 2nd 3rd 4th (quarter) + QO, // 1ˢᵗ 2ⁿᵈ 3ʳᵈ 4ᵗʰ (quarter) + D, // 1 2 ... 30 31 (day of the month) + Do, // 1st 2nd ... 30th 31st (day of the month, ordinal) + DO, // 1ˢᵗ 2ⁿᵈ 3ʳᵈ... 30ᵗʰ 31ˢᵗ (day of the month, ordinal) + DD, // 01 02 ... 30 31 (day of the month, zero padded) + DDD, // 1 2 ... 364 365 + DDDo, // 1st 2nd ... 364th 365th (day of the year, ordinal) + DDDO, // 1ˢᵗ 2ⁿᵈ ... 364ᵗʰ 365ᵗʰ (day of the year, ordinal) + DDDD, // 001 002 ... 364 365 (day of the year) + d, // 0 1 ... 5 6 (day of the week) + do, // 0th 1st 2nd 3rd ... 5th 6th (day of the week, ordinal) + dO, // 0ᵗʰ 1ˢᵗ 2ⁿᵈ 3ʳᵈ ... 5ᵗʰ 6ᵗʰ (day of the week, ordinal) + dd, // Su Mo ... Fr Sa (day of the week, very short name) + ddd, // Sun Mon ... Fri Sat (day of the week, short name) + dddd, // Sunday Monday ... Friday Saturday (day of the week, long name) + e, // 0 1 ... 5 6 (locale) + E, // 1 2 ... 6 7 (ISO) + w, // 1 2 ... 52 53 + wo, // 1st 2nd 3rd 4th ... 52nd 53rd + wO, // 1ˢᵗ 2ⁿᵈ 3ʳᵈ 4th ... 52ⁿᵈ 53ʳᵈ + ww, // 01 02 ... 52 53 + YY, // 70 71 ... 29 30 (year, last two digits only) + YYY, // 1 2 ... 1970 1971 ... 2029 2030 (year) + YYYY, // 0001 0002 ... 1970 1971 ... 2029 2030 (year, zero padded to 4 digits) + // N, // BC AD + // NN, // Before Christ ... Anno Domini + A, // AM PM (ante/post meridian, upper case) + a, // am pm (ante/post meridian, lower case) + H, // 0 1 ... 22 23 (hour, zero padded) + HH, // 00 01 ... 22 23 (hour, zero padded) + h, // 1 2 ... 11 12 (hour, 12 hour clock) + hh, // 01 02 ... 11 12 (hour, 12 hour clock, zero padded) + k, // 1 2 ... 23 24 + kk, // 01 02 ... 23 24 + m, // 0 1 ... 58 59 (minute) + mm, // 00 01 ... 58 59 (minute, zero padded) + s, // 0 1 ... 58 59 (second) + ss, // 00 01 ... 58 59 (second, zero padded) + S, // 0 1 ... 8 9 (tenths of a second) + SS, // 00 01 ... 98 99(hundredths second fraction) + SSS, // 000 001 ... 998 999 (milliseconds) + SSSS, // 0000 0000 ... 9998 9999 (hundreds of microseconds) + SSSSS, // 00000 00000 ... 99998 99999 (tens of microseconds) + SSSSSS, // 000000 000000 ... 999998 999999 (microseconds) + SSSSSSS, // 0000000 00000000 ... 9999998 9999999 (hundreds of nanoseconds) + SSSSSSSS, // 00000000 000000000 ... 99999998 99999999 (tens of nanoseconds) + SSSSSSSSS, // 000000000 000000000 ... 999999998 999999999 (nanoseconds) + // z, // EST CST ... MST PST + // Z, // -07:00 -06:00 ... +06:00 +07:00 + // ZZ, // -0700 -0600 ... +0600 +0700 + // x, // unix milli + // X, // unix + }; + + pub fn dayOfThisYear(self: Self) u9 { + return self.month.daysBefore(self.year) + self.day; + } +}; + +test "formatTest" { + var buffer: [1024]u8 = undefined; + var fba = std.heap.FixedBufferAllocator.init(&buffer); + const allocator = fba.allocator(); + + const epoch = DateTime{ + .nanosecond = 123456789, + .second = 0, + .minute = 0, + .hour = 0, + .day = 1, + .month = .Jan, + .year = 1970, + .weekday = .Fri, + }; + const cases = [_]struct { datetime: DateTime, fmt: []const u8, result: []const u8 }{ + .{ + .datetime = epoch, + .fmt = "YYYY-MM-DDTHH:mm:ss.SSSSSSSSS", + .result = "1970-01-01T00:00:00.123456789", + }, + .{ + .datetime = epoch, + .fmt = "YYYY-MM-DDTHH:mm:ss.SSSSSSSS", + .result = "1970-01-01T00:00:00.12345678", + }, + .{ + .datetime = epoch, + .fmt = "YYYY-MM-DDTHH:mm:ss.SSSSSSS", + .result = "1970-01-01T00:00:00.1234567", + }, + .{ + .datetime = epoch, + .fmt = "YYYY-MM-DDTHH:mm:ss.SSSSSS", + .result = "1970-01-01T00:00:00.123456", + }, + .{ + .datetime = epoch, + .fmt = "YYYY-MM-DDTHH:mm:ss.SSSSS", + .result = "1970-01-01T00:00:00.12345", + }, + .{ + .datetime = epoch, + .fmt = "YYYY-MM-DDTHH:mm:ss.SSSS", + .result = "1970-01-01T00:00:00.1234", + }, + .{ + .datetime = epoch, + .fmt = "YYYY-MM-DDTHH:mm:ss.SSS", + .result = "1970-01-01T00:00:00.123", + }, + .{ + .datetime = epoch, + .fmt = "YYYY-MM-DDTHH:mm:ss.SS", + .result = "1970-01-01T00:00:00.12", + }, + .{ + .datetime = epoch, + .fmt = "YYYY-MM-DDTHH:mm:ss.S", + .result = "1970-01-01T00:00:00.1", + }, + }; + + inline for (cases) |case| { + var array = std.ArrayList(u8).init(allocator); + defer array.deinit(); + + try case.datetime.format(case.fmt, .{}, array.writer()); + + std.testing.expect(std.mem.eql(u8, array.items, case.result)) catch |err| { + std.debug.print("{s} {s} {s}\n", .{ + case.fmt, + case.result, + array.items, + }); + return err; + }; + } +} + +pub const tenthsPerNanoSecond = hundredthsPerNanoSecond * 10; +pub const hundredthsPerNanoSecond = milliSecondsPerNanoSecond * 10; +pub const milliSecondsPerNanoSecond = microSecondsPerNanoSecond * 1_000; +pub const microSecondsPerNanoSecond = 1_000; +pub const nanoSecondsPerSecond = microSecondsPerNanoSecond * 1_000; +pub const microSecondsPerSecond = milliSecondsPerSecond * 1_000; +pub const milliSecondsPerSecond = 1_000; +pub const secondsPerMinute = 60; +pub const minutesPerHour = 60; +pub const secondsPerHour = minutesPerHour * secondsPerMinute; +pub const hoursPerDay = 24; +pub const secodsPerDay = hoursPerDay * minutesPerHour * secondsPerMinute; + +// const daysSinceJan1st = [2][13]u32{ +// [_]u32{ 0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334, 365 }, // 365 days, non-leap +// [_]u32{ 0, 31, 60, 91, 121, 152, 182, 213, 244, 274, 305, 335, 366 }, // 366 days, leap +// }; + +pub const Instant = struct { + timestamp: i128, + timezone: []const u8, + + const Self = @This(); + + pub fn now() Self { + return Instant{ + .timestamp = std.time.nanoTimestamp(), + .timezone = "UTC", + }; + } + + pub fn utc() Self { + return Instant{ + .timestamp = std.time.nanoTimestamp(), + .timezone = "UTC", + }; + } + + pub fn fromNanoTimeStamp(timestamp: i128) Self { + return Self{ + .timestamp = timestamp, + .timezone = "UTC", + }; + } + + pub fn asDateTime(self: Self) DateTime { + const nanosecond: Nanosecond = @intCast(@mod( + self.timestamp, + nanoSecondsPerSecond, + )); + var seconds = @divTrunc( + self.timestamp, + nanoSecondsPerSecond, + ); + const second: Second = @intCast(@mod(seconds, secondsPerMinute)); + seconds -= second; + + const minute: Minute = @intCast(@divTrunc( + @mod(seconds, secondsPerHour), + secondsPerMinute, + )); + seconds -= @as(u32, minute) * secondsPerMinute; + + const hour: Hour = @intCast(@divTrunc( + @mod(seconds, secodsPerDay), + secondsPerHour, + )); + seconds -= @as(u32, hour) * secondsPerHour; + + const days: i32 = @intCast(@divTrunc( + seconds, + secodsPerDay, + )); + + const date = civilFromDays(days); + + return DateTime{ + .year = date.year, + .month = date.month, + .day = date.day, + .hour = hour, + .minute = minute, + .second = second, + .nanosecond = nanosecond, + .weekday = weekdayFromDays(days), + }; + } + // pub fn format(self: @This()) []const u8 { + // std.debug.print("{i} {i} {i} {i} {i} {i} {i}", .{ year, month, mday, hour, minute, second, nanosecond }); + // } +}; + +pub fn daysFromCivil(year: Year, month: Month, d: Day) i32 { + std.debug.assert(d >= 1 and d <= month.lastDay(year)); + + const janOrFeb = month == .Jan or month == .Feb; + + const m = month.monthNumber(); + + const y = year - (if (janOrFeb) @as(Year, 1) else @as(Year, 0)); + + const era = @divTrunc( + if (y >= 0) y else y - 399, + 400, + ); + + const yoe = y - era * 400; + std.debug.assert(yoe >= 0 and yoe <= 399); + + const doy = @divTrunc( + 153 * (m + (if (!janOrFeb) @as(i32, -3) else @as(i32, 9))) + 2, + 5, + ) + d - 1; + std.debug.assert(doy >= 0 and doy <= 365); + + const doe = yoe * 365 + @divTrunc(yoe, 4) - @divTrunc(yoe, 100) + doy; + std.debug.assert(doe >= 0 and doe <= 146096); + + return era * 146097 + doe - 719468; +} + +test "daysFromCivil" { + const tests = [_]struct { year: Year, month: Month, day: Day, result: i128 }{ + .{ .year = 1970, .month = .Jan, .day = 1, .result = 0 }, + .{ .year = 1970, .month = .Jan, .day = 2, .result = 1 }, + .{ .year = 1969, .month = .Dec, .day = 31, .result = -1 }, + }; + + for (tests) |case| { + const result = daysFromCivil(case.year, case.month, case.day); + try std.testing.expect(std.meta.eql(case.result, result)); + } +} + +const Date = struct { + year: Year, + month: Month, + day: Day, +}; + +pub fn civilFromDays(days: i32) Date { + const z = days + 719468; + + const era = @divTrunc(if (z >= 0) z else z - 146096, 146097); + + const doe = (z - era * 146097); + std.debug.assert(doe >= 0 and doe <= 146096); + + const yoe = @divTrunc(doe - @divTrunc(doe, 1460) + @divTrunc(doe, 36524) - @divTrunc(doe, 146096), 365); + std.debug.assert(yoe >= 0 and yoe <= 399); + + const y = yoe + era * 400; + + const doy = doe - (365 * yoe + @divTrunc(yoe, 4) - @divTrunc(yoe, 100)); + std.debug.assert(doy >= 0 and doy <= 365); + + const mp = @divTrunc(5 * doy + 2, 153); + std.debug.assert(mp >= 0 and mp <= 11); + + const d = doy - @divTrunc(153 * mp + 2, 5) + 1; + std.debug.assert(d >= 1 and d <= 31); + + const m = mp + (if (mp < 10) @as(i32, 3) else @as(i32, -9)); + std.debug.assert(m >= 1 and m <= 12); + + return Date{ + .year = y + (if (m <= 2) @as(Year, 1) else @as(Year, 0)), + .month = @enumFromInt(m), + .day = @intCast(d), + }; +} + +test "civilFromDays" { + const tests = [_]struct { days: i32, result: Date }{ + .{ + .days = 0, + .result = Date{ + .year = 1970, + .month = .Jan, + .day = 1, + }, + }, + .{ + .days = 1, + .result = Date{ + .year = 1970, + .month = .Jan, + .day = 2, + }, + }, + .{ + .days = -1, + .result = Date{ + .year = 1969, + .month = .Dec, + .day = 31, + }, + }, + .{ + .days = 19605, + .result = Date{ + .year = 2023, + .month = .Sep, + .day = 5, + }, + }, + }; + + for (tests) |case| { + const result = civilFromDays(case.days); + try std.testing.expect(std.meta.eql(case.result, result)); + } +} + +const DayOfWeek = enum(u3) { + Sun = 0, + Mon = 1, + Tue = 2, + Wed = 3, + Thu = 4, + Fri = 5, + Sat = 6, + + const Self = @This(); + + pub fn next(self: Self) Self { + return switch (self) { + .Sun => .Mon, + .Mon => .Tue, + .Tue => .Wed, + .Wed => .Thu, + .Thu => .Fri, + .Fri => .Sat, + .Sat => .Sun, + }; + } + + pub fn prev(self: Self) Self { + return switch (self) { + .Sun => .Sat, + .Mon => .Sun, + .Tue => .Mon, + .Wed => .Tue, + .Thu => .Wed, + .Fri => .Thu, + .Sat => .Fri, + }; + } + + pub fn veryShortName(self: Self) []const u8 { + return switch (self) { + .Sun => "Su", + .Mon => "Mo", + .Tue => "Tu", + .Wed => "We", + .Thu => "Th", + .Fri => "Fr", + .Sat => "Sa", + }; + } + + pub fn shortName(self: Self) []const u8 { + return switch (self) { + .Sun => "Sun", + .Mon => "Mon", + .Tue => "Tue", + .Wed => "Wed", + .Thu => "Thu", + .Fri => "Fri", + .Sat => "Sat", + }; + } + + pub fn longName(self: Self) []const u8 { + return switch (self) { + .Sun => "Sunday", + .Mon => "Monday", + .Tue => "Tuesday", + .Wed => "Wednesday", + .Thu => "Thursday", + .Fri => "Friday", + .Sat => "Saturday", + }; + } + + pub fn weekdayNumber(self: Self) u3 { + return @intFromEnum(self); + // return switch (self) { + // .Sun => 0, + // .Mon => 1, + // .Tue => 2, + // .Wed => 3, + // .Thu => 4, + // .Fri => 5, + // .Sat => 6, + // }; + } + pub fn isoWeekdayNumber(self: Self) u3 { + return if (self == .Sun) 7 else @intFromEnum(self); + // return switch (self) { + // .Sun => 7, + // .Mon => 1, + // .Tue => 2, + // .Wed => 3, + // .Thu => 4, + // .Fri => 5, + // .Sat => 6, + // }; + } +}; + +pub fn weekdayDifference(start: DayOfWeek, end: DayOfWeek) u3 { + const d = @as(i4, end.weekdayNumber()) - @as(i4, start.weekdayNumber()); + return if (d >= 0) @intCast(d) else @intCast(d + 7); +} + +test "weekdayDifference" { + const difference = [7][7]u3{ + [_]u3{ 0, 1, 2, 3, 4, 5, 6 }, + [_]u3{ 6, 0, 1, 2, 3, 4, 5 }, + [_]u3{ 5, 6, 0, 1, 2, 3, 4 }, + [_]u3{ 4, 5, 6, 0, 1, 2, 3 }, + [_]u3{ 3, 4, 5, 6, 0, 1, 2 }, + [_]u3{ 2, 3, 4, 5, 6, 0, 1 }, + [_]u3{ 1, 2, 3, 4, 5, 6, 0 }, + }; + for (0..6) |start| { + for (0..6) |end| { + const result = weekdayDifference( + @as(DayOfWeek, @enumFromInt(start)), + @as(DayOfWeek, @enumFromInt(end)), + ); + // try std.debug.print("{any} {any} {any}\n", .{}) + try std.testing.expect(result == difference[start][end]); + } + } +} + +pub fn weekdayFromDays(z: i32) DayOfWeek { + return @as( + DayOfWeek, + @enumFromInt(if (z >= -4) @rem(z + 4, 7) else @rem(z + 5, 7) + 6), + ); +} + +test "weekdayFromDays" { + const tests = [_]struct { days: i32, result: DayOfWeek }{ + .{ + .days = -1, + .result = .Wed, + }, + .{ + .days = 0, + .result = .Thu, + }, + .{ + .days = 1, + .result = .Fri, + }, + .{ + .days = 19605, + .result = .Tue, + }, + }; + + for (tests) |case| { + const result = weekdayFromDays(case.days); + try std.testing.expect(std.meta.eql(case.result, result)); + } +} + +fn printOrdinal(writer: anytype, num: u16) !void { + try writer.print("{}", .{num}); + try writer.writeAll(if (num >= 11 and num <= 13) "th" else switch (num % 10) { + 1 => "st", + 2 => "nd", + 3 => "rd", + else => "th", + }); +} + +test "testPrintOrdinal" { + var buffer: [1024]u8 = undefined; + var fba = std.heap.FixedBufferAllocator.init(&buffer); + const allocator = fba.allocator(); + + const cases = [_]struct { number: u16, result: []const u8 }{ + .{ + .number = 0, + .result = "0th", + }, + .{ + .number = 1, + .result = "1st", + }, + .{ + .number = 2, + .result = "2nd", + }, + .{ + .number = 3, + .result = "3rd", + }, + .{ + .number = 4, + .result = "4th", + }, + .{ + .number = 11, + .result = "11th", + }, + .{ + .number = 12, + .result = "12th", + }, + .{ + .number = 13, + .result = "13th", + }, + .{ + .number = 14, + .result = "14th", + }, + .{ + .number = 31, + .result = "31st", + }, + }; + + inline for (cases) |case| { + var array = std.ArrayList(u8).init(allocator); + defer array.deinit(); + + try printOrdinal(array.writer(), case.number); + std.testing.expect(std.mem.eql(u8, array.items, case.result)) catch |err| { + std.debug.print("{d} failed {s} != {s}\n", .{ case.number, array.items, case.result }); + return err; + }; + } +} + +fn printOrdinalSuperscript(writer: anytype, num: u16) !void { + try writer.print("{}", .{num}); + try writer.writeAll(if (num >= 11 and num <= 13) "ᵗʰ" else switch (num % 10) { + 1 => "ˢᵗ", + 2 => "ⁿᵈ", + 3 => "ʳᵈ", + else => "ᵗʰ", + }); +} + +test "testPrintOrdinalSuperscript" { + // var buffer: [1024]u8 = undefined; + // var fbs = std.io.fixedBufferStream(buffer); + // const allocator = fba.allocator(); + + const cases = [_]struct { number: u16, result: []const u8 }{ + .{ + .number = 0, + .result = "0ᵗʰ", + }, + .{ + .number = 1, + .result = "1ˢᵗ", + }, + .{ + .number = 2, + .result = "2ⁿᵈ", + }, + .{ + .number = 3, + .result = "3ʳᵈ", + }, + .{ + .number = 4, + .result = "4ᵗʰ", + }, + .{ + .number = 11, + .result = "11ᵗʰ", + }, + .{ + .number = 12, + .result = "12ᵗʰ", + }, + .{ + .number = 13, + .result = "13ᵗʰ", + }, + .{ + .number = 14, + .result = "14ᵗʰ", + }, + .{ + .number = 31, + .result = "31ˢᵗ", + }, + }; + + inline for (cases) |case| { + var buffer: [16]u8 = undefined; + var stream = std.io.fixedBufferStream(&buffer); + var writer = stream.writer(); + + try printOrdinalSuperscript(writer, case.number); + std.testing.expect(std.mem.eql(u8, stream.getWritten(), case.result)) catch |err| { + std.debug.print("{d} failed {s} != {s}\n", .{ case.number, stream.getWritten(), case.result }); + return err; + }; + } +} + +fn printLongName(writer: anytype, index: u16, names: []const []const u8) !void { + try writer.writeAll(names[index]); +} + +fn wrap(val: u16, at: u16) u16 { + var tmp = val % at; + return if (tmp == 0) at else tmp; +} + +test "asDateTime" { + _ = Instant.utc().asDateTime(); +} + +fn isLeap(year: i32) bool { + return @rem(year, 4) == 0 and (@rem(year, 100) != 0 or @rem(year, 400) == 0); +} + +test "isLeap" { + try std.testing.expectEqual(false, isLeap(2005)); + try std.testing.expectEqual(true, isLeap(2096)); + try std.testing.expectEqual(false, isLeap(2100)); + try std.testing.expectEqual(true, isLeap(2400)); +} + +test "bigTest" { + const ystart = -1000000; + var prev_z: i32 = daysFromCivil(ystart, .Jan, 1) - 1; + try std.testing.expect(prev_z < 0); + var prev_wd = weekdayFromDays(prev_z); + try std.testing.expect(0 <= @intFromEnum(prev_wd) and @intFromEnum(prev_wd) <= 6); + var y: Year = ystart; + while (y <= -ystart) { + for ([_]Month{ .Jan, .Feb, .Mar, .Apr, .May, .Jun, .Jul, .Aug, .Sep, .Oct, .Nov, .Dec }) |m| { + var d: Day = 1; + const e = m.lastDay(y); + while (d <= e) { + // std.debug.print("{d} {d} {d}\n", .{ y, @intFromEnum(m), d }); + const z = daysFromCivil(y, m, d); + // std.debug.print("{d} {d}\n", .{ prev_z, z }); + try std.testing.expect(prev_z < z); + try std.testing.expect(z == prev_z + 1); + const date = civilFromDays(z); + try std.testing.expect(y == date.year); + try std.testing.expect(m == date.month); + try std.testing.expect(d == date.day); + const wd = weekdayFromDays(z); + try std.testing.expect(0 <= @intFromEnum(wd) and @intFromEnum(wd) <= 6); + try std.testing.expect(wd == prev_wd.next()); + try std.testing.expect(prev_wd == wd.prev()); + prev_z = z; + prev_wd = wd; + d += 1; + } + } + y += 1; + } +}