zig-datetime/src/main.zig
2024-03-13 15:47:56 -05:00

1520 lines
46 KiB
Zig

const std = @import("std");
// https://www.nist.gov/pml/owm/metric-si-prefixes
pub const SIPrefix = enum(i8) {
quetta = 30,
ronna = 27,
yotta = 24,
zetta = 21,
exa = 18,
peta = 15,
tera = 12,
giga = 9,
mega = 6,
kilo = 3,
hecto = 2,
deka = 1,
deci = -1,
centi = -2,
milli = -3,
micro = -6,
nano = -9,
pico = -12,
femto = -15,
atto = -18,
zepto = -21,
yocto = -24,
ronto = -27,
quecto = -30,
const Self = @This();
pub fn exponent(self: Self) i8 {
return @intFromEnum(self);
}
pub fn symbol(self: Self) []const u8 {
return switch (self) {
.quetta => "Q",
.ronna => "R",
.yotta => "Y",
.zetta => "Z",
.exa => "E",
.peta => "P",
.tera => "T",
.giga => "G",
.mega => "M",
.kilo => "k",
.hecto => "h",
.deka => "da",
.deci => "d",
.centi => "c",
.milli => "m",
.micro => "µ",
.nano => "n",
.pico => "p",
.femto => "f",
.atto => "a",
.zepto => "z",
.yocto => "y",
.ronto => "r",
.quecto => "q",
};
}
};
pub fn convert(from: i8, to: i8, value: i128) struct { result: i128, remainder: i128 } {
const exponent = from - to;
if (exponent == 0) return value;
if (exponent < 0) {
const factor = std.math.pow(i128, 10, @abs(exponent));
const remainder = @rem(value, factor);
const result = @divTrunc(value, factor);
return .{ result, remainder };
}
if (exponent > 0) {
const factor = std.math.pow(i128, 10, exponent);
return .{ factor * value, 0 };
}
}
// test "convert-1" {
// const result = convert(.milli, .micro, 1);
// try std.testing.expectEqual(i128, result.result, 1000);
// try std.testing.expectEqual(i128, result.remainder, 0);
// }
// pub fn conversionFactor(from: i8, to: i8) !i30 {
// if (from < to) return error.InvalidConversion;
// if (from == to) return 1;
// }
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;
fn readInt(text: []const u8, maxlen: usize) []const u8 {
if (text.len == 0) return text[0..0];
for (0..@min(text.len, maxlen)) |i| {
if (!std.ascii.isDigit(text[i])) return text[0 .. i - 1];
}
return text[0..maxlen];
}
fn readFrac(text: []const u8, length: usize) !Nanosecond {
if (length == 0) return error.TooShort;
if (length > 9) return error.TooLong;
const v = readInt(text, length);
if (v.len != length) return error.TooShort;
return try std.fmt.parseInt(Nanosecond, v, 10) * try std.math.powi(Nanosecond, 10, 9 - @as(Nanosecond, @intCast(length)));
}
// 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 formatX(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}),
.S => try writer.print("{d:>1}", .{tag.cnv(self.nanosecond)}),
.SS => try writer.print("{d:>2}", .{tag.cnv(self.nanosecond)}),
.SSS => try writer.print("{d:>3}", .{tag.cnv(self.nanosecond)}),
.SSSS => try writer.print("{d:>4}", .{tag.cnv(self.nanosecond)}),
.SSSSS => try writer.print("{d:>5}", .{tag.cnv(self.nanosecond)}),
.SSSSSS => try writer.print("{d:>6}", .{tag.cnv(self.nanosecond)}),
.SSSSSSS => try writer.print("{d:>7}", .{tag.cnv(self.nanosecond)}),
.SSSSSSSS => try writer.print("{d:>8}", .{tag.cnv(self.nanosecond)}),
.SSSSSSSSS => try writer.print("{d:>9}", .{tag.cnv(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.formatX(fmt, .{}, list.writer());
return list.toOwnedSlice();
}
pub fn parse(comptime fmt: []const u8, value: []const u8) !Self {
if (fmt.len == 0) @compileError("DateTime: format string can't be empty");
@setEvalBranchQuota(100000);
var date: Self = .{
.nanosecond = 0,
.second = 0,
.minute = 0,
.hour = 0,
.day = 1,
.month = .Jan,
.year = 1970,
.weekday = .Thu,
};
var left = value;
comptime var start = 0;
comptime var end = 0;
comptime var next: ?FormatSeq = null;
inline for (fmt, 0..) |char, index| {
end = index + 1;
if (comptime std.meta.stringToEnum(FormatSeq, fmt[start..end])) |tag| {
next = tag;
if (index < fmt.len - 1) continue;
}
if (next) |tag| {
switch (tag) {
.YY => {
const year = readInt(left, 2);
date.year = try std.fmt.parseInt(Year, year, 10) + 2000;
left = left[year.len..];
date.weekday = DayOfWeek.dayOfWeek(date.year, date.month, date.day);
},
.YYYY, .YYY => {
const year = readInt(left, 4);
date.year = try std.fmt.parseInt(Year, year, 10);
left = left[year.len..];
date.weekday = DayOfWeek.dayOfWeek(date.year, date.month, date.day);
},
.SSSSSSSSS => {
date.nanosecond = try readFrac(left, 9);
left = left[9..];
},
.SSSSSSSS => {
date.nanosecond = try readFrac(left, 8);
left = left[8..];
},
.SSSSSSS => {
date.nanosecond = try readFrac(left, 7);
left = left[7..];
},
.SSSSSS => {
date.nanosecond = try readFrac(left, 6);
left = left[6..];
},
.SSSSS => {
date.nanosecond = try readFrac(left, 5);
left = left[5..];
},
.SSSS => {
date.nanosecond = try readFrac(left, 4);
left = left[4..];
},
.SSS => {
date.nanosecond = try readFrac(left, 3);
left = left[3..];
},
.SS => {
date.nanosecond = try readFrac(left, 2);
left = left[2..];
},
.S => {
date.nanosecond = try readFrac(left, 1);
left = left[1..];
},
else => {},
}
next = null;
start = index;
}
switch (char) {
',', ' ', ':', '-', '.', 'T', 'W' => {
start = index + 1;
continue;
},
else => {},
}
}
return date;
}
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 cnv(comptime tag: FormatSeq, value: u30) u30 {
const name = comptime @tagName(tag);
comptime var count: i8 = undefined;
inline for (name, 1..) |c, i| {
if (c != 'S') @compileError(name ++ " is not a fractional second format sequence");
count = i;
}
if (count > 9) @compileError("fractional seconds smaller than nanoseconds are not supported");
if (count == 9) return value;
const exponent = 9 - count;
const factor = std.math.pow(u30, 10, exponent);
return @as(u30, @divTrunc(value, factor));
}
test "cnv" {
const cases = [_]struct { tag: FormatSeq, expected: u30, value: u30 }{
.{ .tag = .S, .expected = 1, .value = 123456789 },
.{ .tag = .SS, .expected = 12, .value = 123456789 },
.{ .tag = .SSS, .expected = 123, .value = 123456789 },
.{ .tag = .SSSS, .expected = 1234, .value = 123456789 },
.{ .tag = .SSSSS, .expected = 12345, .value = 123456789 },
.{ .tag = .SSSSSS, .expected = 123456, .value = 123456789 },
.{ .tag = .SSSSSSS, .expected = 1234567, .value = 123456789 },
.{ .tag = .SSSSSSSS, .expected = 12345678, .value = 123456789 },
.{ .tag = .SSSSSSSSS, .expected = 123456789, .value = 123456789 },
};
inline for (cases) |case| {
try std.testing.expectEqual(case.expected, cnv(case.tag, case.value));
}
}
};
pub fn dayOfThisYear(self: Self) u9 {
return self.month.daysBefore(self.year) + self.day;
}
};
test "parseTest" {
const cases = [_]struct { value: []const u8, fmt: []const u8, expected: DateTime }{
.{
.value = "1970",
.fmt = "YYYY",
.expected = .{
.nanosecond = 0,
.second = 0,
.minute = 0,
.hour = 0,
.day = 1,
.month = .Jan,
.year = 1970,
.weekday = .Thu,
},
},
.{
.value = "2001",
.fmt = "YYYY",
.expected = .{
.nanosecond = 0,
.second = 0,
.minute = 0,
.hour = 0,
.day = 1,
.month = .Jan,
.year = 2001,
.weekday = .Mon,
},
},
.{
.value = "70",
.fmt = "YY",
.expected = .{
.nanosecond = 0,
.second = 0,
.minute = 0,
.hour = 0,
.day = 1,
.month = .Jan,
.year = 2070,
.weekday = .Wed,
},
},
.{
.value = "123456789",
.fmt = "SSSSSSSSS",
.expected = .{
.nanosecond = 123456789,
.second = 0,
.minute = 0,
.hour = 0,
.day = 1,
.month = .Jan,
.year = 1970,
.weekday = .Thu,
},
},
.{
.value = "12345678",
.fmt = "SSSSSSSS",
.expected = .{
.nanosecond = 123456780,
.second = 0,
.minute = 0,
.hour = 0,
.day = 1,
.month = .Jan,
.year = 1970,
.weekday = .Thu,
},
},
.{
.value = "1234567",
.fmt = "SSSSSSS",
.expected = .{
.nanosecond = 123456700,
.second = 0,
.minute = 0,
.hour = 0,
.day = 1,
.month = .Jan,
.year = 1970,
.weekday = .Thu,
},
},
.{
.value = "123456",
.fmt = "SSSSSS",
.expected = .{
.nanosecond = 123456000,
.second = 0,
.minute = 0,
.hour = 0,
.day = 1,
.month = .Jan,
.year = 1970,
.weekday = .Thu,
},
},
.{
.value = "12345",
.fmt = "SSSSS",
.expected = .{
.nanosecond = 123450000,
.second = 0,
.minute = 0,
.hour = 0,
.day = 1,
.month = .Jan,
.year = 1970,
.weekday = .Thu,
},
},
.{
.value = "1234",
.fmt = "SSSS",
.expected = .{
.nanosecond = 123400000,
.second = 0,
.minute = 0,
.hour = 0,
.day = 1,
.month = .Jan,
.year = 1970,
.weekday = .Thu,
},
},
.{
.value = "123",
.fmt = "SSS",
.expected = .{
.nanosecond = 123000000,
.second = 0,
.minute = 0,
.hour = 0,
.day = 1,
.month = .Jan,
.year = 1970,
.weekday = .Thu,
},
},
.{
.value = "12",
.fmt = "SS",
.expected = .{
.nanosecond = 120000000,
.second = 0,
.minute = 0,
.hour = 0,
.day = 1,
.month = .Jan,
.year = 1970,
.weekday = .Thu,
},
},
.{
.value = "1",
.fmt = "S",
.expected = .{
.nanosecond = 100000000,
.second = 0,
.minute = 0,
.hour = 0,
.day = 1,
.month = .Jan,
.year = 1970,
.weekday = .Thu,
},
},
};
inline for (cases) |case| {
const result = try DateTime.parse(case.fmt, case.value);
std.testing.expectEqual(case.expected, result) catch |err| {
std.debug.print("\n{s} {s} {} {}\n", .{ case.value, case.fmt, case.expected, result });
return err;
};
}
}
test "formatTest" {
var buffer: [1024]u8 = undefined;
var fba = std.heap.FixedBufferAllocator.init(&buffer);
const allocator = fba.allocator();
const test_date = 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 = test_date,
.fmt = "YYYY-MM-DDTHH:mm:ss.SSSSSSSSS",
.result = "1970-01-01T00:00:00.123456789",
},
.{
.datetime = test_date,
.fmt = "YYYY-MM-DDTHH:mm:ss.SSSSSSSS",
.result = "1970-01-01T00:00:00.12345678",
},
.{
.datetime = test_date,
.fmt = "YYYY-MM-DDTHH:mm:ss.SSSSSSS",
.result = "1970-01-01T00:00:00.1234567",
},
.{
.datetime = test_date,
.fmt = "YYYY-MM-DDTHH:mm:ss.SSSSSS",
.result = "1970-01-01T00:00:00.123456",
},
.{
.datetime = test_date,
.fmt = "YYYY-MM-DDTHH:mm:ss.SSSSS",
.result = "1970-01-01T00:00:00.12345",
},
.{
.datetime = test_date,
.fmt = "YYYY-MM-DDTHH:mm:ss.SSSS",
.result = "1970-01-01T00:00:00.1234",
},
.{
.datetime = test_date,
.fmt = "YYYY-MM-DDTHH:mm:ss.SSS",
.result = "1970-01-01T00:00:00.123",
},
.{
.datetime = test_date,
.fmt = "YYYY-MM-DDTHH:mm:ss.SS",
.result = "1970-01-01T00:00:00.12",
},
.{
.datetime = test_date,
.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.formatX(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 = microSecondsPerSecond * 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 secondsPerDay = hoursPerDay * minutesPerHour * secondsPerMinute;
pub const Instant = struct {
timestamp: i128,
timezone: []const u8,
const Self = @This();
pub fn now() Self {
return Self{
.timestamp = std.time.nanoTimestamp(),
.timezone = "UTC",
};
}
pub fn utc() Self {
return Self{
.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,
secondsPerDay,
),
secondsPerHour,
));
seconds -= @as(u32, hour) * secondsPerHour;
const days: i32 = @intCast(@divTrunc(
seconds,
secondsPerDay,
));
const date = civilFromDays(days);
return DateTime{
.year = date.year,
.month = date.month,
.day = date.day,
.hour = hour,
.minute = minute,
.second = second,
.nanosecond = nanosecond,
.weekday = DayOfWeek.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 });
// }
};
test "instantTest" {
const cases = [_]struct {
instant: Instant,
datetime: DateTime,
}{
.{
.instant = Instant{
.timestamp = 0,
.timezone = "UTC",
},
.datetime = DateTime{
.year = 1970,
.month = .Jan,
.day = 1,
.hour = 0,
.minute = 0,
.second = 0,
.nanosecond = 0,
.weekday = .Thu,
},
},
.{
.instant = Instant{
.timestamp = 1697316872549526016,
.timezone = "UTC",
},
.datetime = DateTime{
.year = 2023,
.month = .Oct,
.day = 14,
.hour = 20,
.minute = 54,
.second = 32,
.nanosecond = 549526016,
.weekday = .Sat,
},
},
};
inline for (cases) |case| {
const datetime = case.instant.asDateTime();
try std.testing.expectEqual(case.datetime, datetime);
}
}
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.expectEqual(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);
}
pub fn isoWeekdayNumber(self: Self) u3 {
return if (self == .Sun) 7 else @intFromEnum(self);
}
pub fn weekdayFromDays(days: i32) DayOfWeek {
return @as(
DayOfWeek,
@enumFromInt(if (days >= -4) @rem(days + 4, 7) else @rem(days + 5, 7) + 6),
);
}
pub fn dayOfWeek(year: Year, month: Month, day: Day) Self {
const days = daysFromCivil(year, month, day);
return Self.weekdayFromDays(days);
}
};
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]);
}
}
}
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 = DayOfWeek.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);
const 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 {
const tmp = val % at;
return if (tmp == 0) at else tmp;
}
test "asDateTime" {
_ = Instant.utc().asDateTime();
}
pub fn isLeap(year: Year) bool {
// taken from https://github.com/ziglang/zig/pull/18451
// In the western Gregorian Calendar leap a year is
// a multiple of 4, excluding multiples of 100, and
// adding multiples of 400. In code:
//
// if (@mod(year, 4) != 0)
// return false;
// if (@mod(year, 100) != 0)
// return true;
// return (0 == @mod(year, 400));
// The following is equivalent to the above
// but uses bitwise operations when testing
// for divisibility, masking with 3 as test
// for multiples of 4 and with 15 as a test
// for multiples of 16. Multiples of 16 and
// 100 are, conveniently, multiples of 400.
const mask: Year = switch (year % 100) {
0 => 0b1111,
else => 0b11,
};
return 0 == year & mask;
// 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 = DayOfWeek.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 = DayOfWeek.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;
// }
// }