From f8e7b01e9cdac08795762ee3eeb599f05d4d3633 Mon Sep 17 00:00:00 2001 From: "Lucas F." Date: Sat, 17 Jan 2026 16:04:30 -0300 Subject: [PATCH] update: time --- src/time.zig | 874 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 874 insertions(+) create mode 100644 src/time.zig diff --git a/src/time.zig b/src/time.zig new file mode 100644 index 0000000..a8a803b --- /dev/null +++ b/src/time.zig @@ -0,0 +1,874 @@ +// https://github.com/cztomsik/tokamak +const std = @import("std"); +const util = @import("util.zig"); +const testing = std.testing; +const meta = @import("meta.zig"); +const dlt = @import("delta.zig"); + +const RATA_MIN = date_to_rata(Date.MIN); +const RATA_MAX = date_to_rata(Date.MAX); +const RATA_TO_UNIX = 719468; +const EOD = 86_400 - 1; + +pub const DAY_NAMES_SHORT = [_][]const u8{ "Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat" }; +pub const DAY_NAMES_LONG = [_][]const u8{ "Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday" }; + +pub const MONTH_NAMES_SHORT = [_][]const u8{ "", "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec" }; +pub const MONTH_NAMES_LONG = [_][]const u8{ "", "January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December" }; + +pub const MONTH_DAYS = [12]u8{ 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 }; + +pub const TIME_CHUNKS = [4]u32{ + 60 * 60 * 24 * 7, //week + 60 * 60 * 24, //day + 60 * 60, //hour + 60, //minute +}; + +pub const TIME_STRINGS = [6][]const u8{ "year", "month", "week", "day", "hour", "minute" }; + +// TODO: Decide if we want to use std.debug.assert(), @panic() or just throw an error +fn checkRange(num: anytype, min: @TypeOf(num), max: @TypeOf(num)) void { + if (util.lt(num, min) or util.gt(num, max)) { + // TODO: fix later (we can't use {f} and {any} is also wrong) + // std.log.warn("Value {} is not in range [{}, {}]", .{ num, min, max }); + std.log.warn("Value not in range", .{}); + } +} + +pub const TimeUnit = enum { second, minute, hour, day, month, year }; +pub const DateUnit = enum { day, month, year }; + +// pub const SECS_PER_DAY: i64 = 86_400; +// pub const SECS_PER_HOUR: i64 = 3_600; +// pub const SECS_PER_MIN: i64 = 60; + +pub const TimeError = error{ + Eof, + ExpectedNull, + ExpectedValue, + InvalidCharacter, + InvalidFormat, + Overflow, + OutOfMemory, +}; + +// https://www.youtube.com/watch?v=0s9F4QWAl-E&t=2120 +pub fn isLeapYear(year: i32) bool { + const d: i32 = if (@mod(year, 100) != 0) 4 else 16; + return (year & (d - 1)) == 0; +} + +// https://www.youtube.com/watch?v=0s9F4QWAl-E&t=2257 +pub fn daysInMonth(year: i32, month: u8) u8 { + if (month == 2) { + return if (isLeapYear(year)) 29 else 28; + } + + return 30 | (month ^ (month >> 3)); +} + +pub fn formatDateTime(alloc: std.mem.Allocator, t: Time, format_str: []const u8) ![]u8 { + var result = std.ArrayList(u8){}; + defer result.deinit(alloc); + var writer = result.writer(alloc); + + var i: usize = 0; + while (i < format_str.len) : (i += 1) { + const c = format_str[i]; + + if (c == '\\') { + i += 1; + if (i >= format_str.len) break; + try writer.writeByte(format_str[i]); + continue; + } + + // Todos os códigos do date + time que já implementamos + switch (c) { + // === Códigos de data (do filtro date) === + 'Y' => try writer.print("{d:0>4}", .{@as(u16, @intCast(t.year()))}), + 'm' => try writer.print("{d:0>2}", .{t.month()}), + 'n' => try writer.print("{d}", .{t.month()}), + 'd' => try writer.print("{d:0>2}", .{t.day()}), + 'j' => try writer.print("{d}", .{t.day()}), + 'F' => try writer.writeAll(t.monthNameLong()), + 'M' => try writer.writeAll(t.monthNameShort()), + 'l' => try writer.writeAll(t.weekdayNameLong()), + 'D' => try writer.writeAll(t.weekdayNameShort()), + + // === Códigos de tempo (do filtro time) === + 'H' => try writer.print("{d:0>2}", .{t.hour()}), + 'G' => try writer.print("{d}", .{t.hour()}), + 'i' => try writer.print("{d:0>2}", .{t.minute()}), + 's' => try writer.print("{d:0>2}", .{t.second()}), + 'a' => try writer.writeAll(if (t.hour() < 12) "a.m." else "p.m."), + 'A' => try writer.writeAll(if (t.hour() < 12) "AM" else "PM"), + 'P' => { + const hr24 = t.hour(); + const min = t.minute(); + if (hr24 == 0 and min == 0) { + try writer.writeAll("midnight"); + } else if (hr24 == 12 and min == 0) { + try writer.writeAll("noon"); + } else { + var hr12 = @mod(hr24, 12); + if (hr12 == 0) hr12 = 12; + try writer.print("{d}", .{hr12}); + if (min > 0) try writer.print(":{d:0>2}", .{min}); + try writer.writeAll(if (hr24 < 12) " a.m." else " p.m."); + } + }, + 'u' => try writer.writeAll("000000"), + + else => try writer.writeByte(c), + } + } + + return try result.toOwnedSlice(alloc); +} + +pub const Date = struct { + year: i32, + month: u8, + day: u8, + + pub const MIN = Date.ymd(-1467999, 1, 1); + pub const MAX = Date.ymd(1471744, 12, 31); + + pub fn cmp(a: Date, b: Date) std.math.Order { + if (a.year != b.year) return util.cmp(a.year, b.year); + if (a.month != b.month) return util.cmp(a.month, b.month); + return util.cmp(a.day, b.day); + } + + pub fn parse(str: []const u8) TimeError!Date { + var it = std.mem.splitScalar(u8, str, '-'); + return ymd( + try std.fmt.parseInt(i32, it.next() orelse return error.Eof, 10), + try std.fmt.parseInt(u8, it.next() orelse return error.Eof, 10), + try std.fmt.parseInt(u8, it.next() orelse return error.Eof, 10), + ); + } + + pub fn ymd(year: i32, month: u8, day: u8) Date { + return .{ + .year = year, + .month = month, + .day = day, + }; + } + + pub fn today() Date { + return Time.now().date(); + } + + pub fn yesterday() Date { + return today().add(.day, -1); + } + + pub fn tomorrow() Date { + return today().add(.day, 1); + } + + pub fn startOf(unit: DateUnit) Date { + return today().setStartOf(unit); + } + + pub fn endOf(unit: DateUnit) Date { + return today().setEndOf(unit); + } + + pub fn setStartOf(self: Date, unit: DateUnit) Date { + return switch (unit) { + .day => self, + .month => ymd(self.year, self.month, 1), + .year => ymd(self.year, 1, 1), + }; + } + + pub fn setEndOf(self: Date, unit: DateUnit) Date { + return switch (unit) { + .day => self, + .month => ymd(self.year, self.month, daysInMonth(self.year, self.month)), + .year => ymd(self.year, 12, 31), + }; + } + + pub fn add(self: Date, part: DateUnit, amount: i64) Date { + return switch (part) { + .day => Time.unix(0).setDate(self).add(.days, amount).date(), + .month => { + const total_months = @as(i32, self.month) + @as(i32, @intCast(amount)); + const new_year = self.year + @divFloor(total_months - 1, 12); + const new_month = @as(u8, @intCast(@mod(total_months - 1, 12) + 1)); + return ymd( + new_year, + new_month, + @min(self.day, daysInMonth(new_year, new_month)), + ); + }, + .year => { + const new_year = self.year + @as(i32, @intCast(amount)); + return ymd( + new_year, + self.month, + @min(self.day, daysInMonth(new_year, self.month)), + ); + }, + }; + } + + pub fn dayOfWeek(self: Date) u8 { + const rata_day = date_to_rata(self); + return @intCast(@mod(rata_day + 3, 7)); + } + + pub fn format(self: Date, writer: anytype) !void { + try writer.print("{d}-{d:0>2}-{d:0>2}", .{ + @as(u32, @intCast(self.year)), + self.month, + self.day, + }); + } + + pub fn ordinal(self: Date) usize { + const days_before_month = [_]u16{ 0, 0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334 }; + + var days: usize = days_before_month[self.month]; + days += self.day; + + if (self.month > 2 and isLeapYear(self.year)) { + days += 1; + } + + return days; + } + + pub fn weekday(self: Date) u8 { + const y: i32 = self.year; + const m: u8 = self.month; + const d: u8 = self.day; + + const t = [_]i32{ 0, 3, 2, 5, 0, 3, 5, 1, 4, 6, 2, 4 }; + var year_adj: i32 = y; + if (m < 3) year_adj -= 1; + + var wd: i32 = year_adj + @divFloor(year_adj, 4) - @divFloor(year_adj, 100) + @divFloor(year_adj, 400) + t[m - 1] + @as(i32, d); + wd = @mod(wd, 7); + if (wd < 0) wd += 7; + + return @intCast(if (wd == 6) 7 else wd + 1); + } + + pub fn isoWeek(self: Date) u8 { + const iso_y = self.isoWeekYear(); + const jan4 = Date{ .year = iso_y, .month = 1, .day = 4 }; + const jan4_ord: i32 = @intCast(jan4.ordinal()); + const self_ord: i32 = @intCast(self.ordinal()); + + const days_diff = self_ord - jan4_ord; + const week = @divFloor(days_diff + 4, 7) + 1; // +4 corrige o offset (testado em Python) + + return @intCast(@max(1, @min(53, week))); + } + + pub fn isoWeekYear(self: Date) i32 { + const wd = self.weekday(); // 1=Seg ... 7=Dom + const ord = @as(i32, @intCast(self.ordinal())); + const thursday_ord = ord + (4 - (wd - 1)); // quinta da semana + + var y = self.year; + + var days_in_year: i32 = 365; + if (isLeapYear(y)) days_in_year = 366; + + if (thursday_ord <= 0) { + y -= 1; + } else if (thursday_ord > days_in_year) { + y += 1; + } + return y; + } +}; + +pub const TimeTime = struct { + hour: u32, + minute: u32, + second: u32, + + fn cmp(a: TimeTime, b: TimeTime) std.math.Order { + if (a.hour < b.hour) return .lt; + if (a.hour > b.hour) return .gt; + if (a.minute < b.minute) return .lt; + if (a.minute > b.minute) return .gt; + if (a.second < b.second) return .lt; + if (a.second > b.second) return .gt; + return .eq; + } +}; + +pub const Time = struct { + epoch: i64, + + pub fn parse(str: []const u8) !Time { + if (std.mem.indexOfScalar(u8, str, ' ')) |space| { + // Datetime: "YYYY-MM-DD HH:MM:SS" + const date_str = str[0..space]; + const time_str = str[space + 1 ..]; + + const d = try Date.parse(date_str); + + var it = std.mem.splitScalar(u8, time_str, ':'); + const h = try std.fmt.parseInt(u8, it.next() orelse return error.InvalidFormat, 10); + const m = try std.fmt.parseInt(u8, it.next() orelse return error.InvalidFormat, 10); + const s = try std.fmt.parseInt(u8, it.next() orelse return error.InvalidFormat, 10); + + var t = Time.unix(0).setDate(d); + t = t.setHour(h).setMinute(m).setSecond(s); + return t; + } else { + const d = try Date.parse(str); + return Time.unix(0).setDate(d); + } + return Time.now(); + } + + pub fn unix(epoch: i64) Time { + return .{ .epoch = epoch }; + } + + pub fn new(y: i32, m: u8, d: u8, h: ?u32, min: ?u32, sec: ?u32) Time { + var t = unix(0).setDate(.ymd(y, m, d)); + if (h) |h_| t = t.setHour(h_); + if (min) |min_| t = t.setMinute(min_); + if (sec) |sec_| t = t.setSecond(sec_); + return t; + } + + pub fn now() Time { + return unix(std.time.timestamp()); + } + + pub fn today() Time { + return unix(0).setDate(.today()); + } + + pub fn tomorrow() Time { + return unix(0).setDate(.tomorrow()); + } + + pub fn startOf(unit: TimeUnit) Time { + return Time.now().setStartOf(unit); + } + + pub fn endOf(unit: TimeUnit) Time { + return Time.now().setEndOf(unit); + } + + pub fn second(self: Time) u32 { + return @intCast(@mod(self.total(.seconds), 60)); + } + + pub fn setSecond(self: Time, sec: u32) Time { + return self.add(.seconds, @as(i64, sec) - self.second()); + } + + pub fn minute(self: Time) u32 { + return @intCast(@mod(self.total(.minutes), 60)); + } + + pub fn setMinute(self: Time, min: u32) Time { + return self.add(.minutes, @as(i64, min) - self.minute()); + } + + pub fn hour(self: Time) u32 { + return @intCast(@mod(self.total(.hours), 24)); + } + + pub fn setHour(self: Time, hr: u32) Time { + return self.add(.hours, @as(i64, hr) - self.hour()); + } + + pub fn date(self: Time) Date { + return rata_to_date(@divTrunc(self.epoch, std.time.s_per_day) + RATA_TO_UNIX); + } + + pub fn setDate(self: Time, dat: Date) Time { + var res: i64 = @mod(self.epoch, std.time.s_per_day); + res += (date_to_rata(dat) - RATA_TO_UNIX) * std.time.s_per_day; + return unix(res); + } + + pub fn time(self: Time) TimeTime { + return .{ + .hour = self.hour(), + .minute = self.minute(), + .second = self.second(), + }; + } + + pub fn setStartOf(self: Time, unit: TimeUnit) Time { + // TODO: continue :label? + return switch (unit) { + .second => self, + .minute => self.setSecond(0), + .hour => self.setSecond(0).setMinute(0), + .day => self.setSecond(0).setMinute(0).setHour(0), + .month => { + const d = self.date(); + return unix(0).setDate(.ymd(d.year, d.month, 1)); + }, + .year => { + const d = self.date(); + return unix(0).setDate(.ymd(d.year, 1, 1)); + }, + }; + } + + // TODO: rename to startOfNext? + pub fn next(self: Time, unit: enum { second, minute, hour, day }) Time { + return switch (unit) { + .second => self.add(.seconds, 1), + .minute => self.setSecond(0).add(.minutes, 1), + .hour => self.setSecond(0).setMinute(0).add(.hours, 1), + .day => self.setSecond(0).setMinute(0).setHour(0).add(.hours, 24), + }; + } + + pub fn setEndOf(self: Time, unit: TimeUnit) Time { + // TODO: continue :label? + return switch (unit) { + .second => self, + .minute => self.setSecond(59), + .hour => self.setSecond(59).setMinute(59), + .day => self.setSecond(59).setMinute(59).setHour(23), + .month => { + const d = self.date(); + return unix(EOD).setDate(.ymd(d.year, d.month, daysInMonth(d.year, d.month))); + }, + .year => { + const d = self.date(); + return unix(EOD).setDate(.ymd(d.year, 12, 31)); + }, + }; + } + + pub fn add(self: Time, part: enum { seconds, minutes, hours, days, months, years }, amount: i64) Time { + const n = switch (part) { + .seconds => amount, + .minutes => amount * std.time.s_per_min, + .hours => amount * std.time.s_per_hour, + .days => amount * std.time.s_per_day, + .months => return self.setDate(self.date().add(.month, amount)), + .years => return self.setDate(self.date().add(.year, amount)), + }; + + return .{ .epoch = self.epoch + n }; + } + + fn total(self: Time, part: enum { seconds, minutes, hours }) i64 { + return switch (part) { + .seconds => self.epoch, + .minutes => @divTrunc(self.epoch, std.time.s_per_min), + .hours => @divTrunc(self.epoch, std.time.s_per_hour), + }; + } + + fn year(self: Time) i32 { + return self.date().year; + } + + fn month(self: Time) u8 { + return self.date().month; + } + + fn day(self: Time) u8 { + return self.date().day; + } + + fn monthNameLong(self: Time) []const u8 { + return MONTH_NAMES_LONG[self.date().month]; + } + + fn monthNameShort(self: Time) []const u8 { + return MONTH_NAMES_SHORT[self.date().month]; + } + + fn weekdayNameLong(self: Time) []const u8 { + return DAY_NAMES_LONG[self.date().weekday()]; + } + + fn weekdayNameShort(self: Time) []const u8 { + return DAY_NAMES_SHORT[self.date().weekday()]; + } + + pub fn format(self: Time, writer: anytype) !void { + try writer.print("{f} {d:0>2}:{d:0>2}:{d:0>2} UTC", .{ + self.date(), + self.hour(), + self.minute(), + self.second(), + }); + } + + pub fn toStringAlloc(self: Time, alloc: std.mem.Allocator, format_str: ?[]const u8) TimeError![]u8 { + const fmt = format_str orelse "Y-m-d H:i:s"; + return try formatDateTime(alloc, self, fmt); + } + + pub fn toString(self: Time, format_str: ?[]const u8) TimeError![]const u8 { + return try self.toStringAlloc(std.heap.page_allocator, format_str); + } + + pub fn addRelative(self: Time, delta: dlt.RelativeDelta) Time { + var d = delta; + d.normalize(); // garante que meses/horas/etc estejam normalizados + + // 1. Parte calendáríca (anos + meses + dias) + var dt = self.date(); + + // anos primeiro (mais estável) + if (d.years != 0) { + dt = dt.add(.year, d.years); + } + + // depois meses (respeita dias-in-mês) + if (d.months != 0) { + dt = dt.add(.month, d.months); + } + + // por fim dias normais + if (d.days != 0) { + dt = dt.add(.day, d.days); + } + + // 2. Parte do relógio (horas, minutos, segundos) + var result = self.setDate(dt); + + // Podemos usar o .add() existente para segundos/minutos/horas + if (d.seconds != 0) { + result = result.add(.seconds, d.seconds); + } + if (d.minutes != 0) { + result = result.add(.minutes, d.minutes); + } + if (d.hours != 0) { + result = result.add(.hours, d.hours); + } + + return result; + } + + pub fn subRelative(self: Time, other: Time) dlt.RelativeDelta { + var delta = dlt.RelativeDelta{}; + + // Parte de tempo (horas, min, seg) – igual antes + var seconds_diff: i64 = self.epoch - other.epoch; + + delta.seconds = @as(i32, @intCast(@rem(seconds_diff, 60))); + seconds_diff = @divTrunc(seconds_diff, 60); + + delta.minutes = @as(i32, @intCast(@rem(seconds_diff, 60))); + seconds_diff = @divTrunc(seconds_diff, 60); + + delta.hours = @as(i32, @intCast(@rem(seconds_diff, 24))); + seconds_diff = @divTrunc(seconds_diff, 24); + + // Parte calendárica + var later = self.date(); + var earlier = other.date(); + + const swapped = later.cmp(earlier) == .lt; + + if (swapped) { + // const temp = later; + // later = earlier; + // earlier = temp; + std.mem.swap(i32, &later.year, &earlier.year); + } + + var years: i32 = later.year - earlier.year; + var months: i32 = @as(i32, later.month) - @as(i32, earlier.month); + + if (months < 0) { + years -= 1; + months += 12; + } + + var days: i32 = @as(i32, later.day) - @as(i32, earlier.day); + + // Ajuste rigoroso para borrow (com handling de bissexto) + if (days < 0) { + const days_in_target = daysInMonth(later.year, later.month); + if (later.day == days_in_target) { + // Caso especial (ex: 28/fev não-bissexto vs 29/fev bissexto): ignora borrow, trata como período completo + days = 0; + } else { + // Borrow normal, mas ajusta se earlier.day > dias no mês anterior (para casos como 29 > 28) + const prev_month = later.add(.month, -1); + var days_borrow: i32 = @as(i32, daysInMonth(prev_month.year, prev_month.month)); + if (earlier.day > @as(u8, @intCast(days_borrow))) { + days_borrow = @as(i32, earlier.day); + } + std.debug.print("days_borrow em subRelative {d}\n", .{days_borrow}); + days += days_borrow; + months -= 1; + if (months < 0) { + years -= 1; + months += 12; + } + } + } + + // Atribui com sinal + delta.years = if (swapped) -years else years; + delta.months = if (swapped) -months else months; + delta.days = if (swapped) -days else days; + + // Normaliza (já lida com excessos, mas aqui é só para meses/anos) + delta.normalize(); + + return delta; + } + + pub fn timeSince(self: Time, alloc: std.mem.Allocator, then: Time) TimeError![]u8 { + if (self.epoch >= then.epoch) { + return try alloc.dupe(u8, "0 minutes"); + } + var total_months: i64 = 0; + const delta_year: i64 = (then.year() - self.year()) * 12; + const delta_month: i32 = @as(i32, @intCast(then.month())) - @as(i32, @intCast(self.month())); + + total_months = delta_year + delta_month; + + if (self.day() > then.day() or (self.day() == then.day() and self.time().cmp(then.time()) == .gt)) { + total_months -= 1; + } + const months = @rem(total_months, 12); + const years = @divTrunc(total_months, 12); + var pivot_year: i64 = 0; + var pivot_month: i64 = 0; + var pivot: Time = undefined; + + if (years > 0 or months > 0) { + pivot_year = @as(i64, self.year()) + years; + pivot_month = @as(i64, self.month()) + months; + if (pivot_month > 12) { + pivot_year += 1; + pivot_month -= 12; + } + const d: u8 = @min(MONTH_DAYS[@intCast(pivot_month - 1)], self.day()); + pivot = Time.new( + @as(i32, @intCast(pivot_year)), + @as(u8, @intCast(pivot_month)), + d, + self.hour(), + self.minute(), + self.second(), + ); + } else { + pivot = self; + } + var remaining_time = then.epoch - pivot.epoch; + + var partials = std.ArrayList(i64){}; + errdefer partials.deinit(alloc); + + try partials.append(alloc, years); + try partials.append(alloc, months); + + for (TIME_CHUNKS) |chunk| { + const count: i32 = @intCast(@divFloor(remaining_time, chunk)); + try partials.append(alloc, count); + remaining_time -= count * @as(i32, @intCast(chunk)); + } + + const min: i64 = std.mem.min(i64, partials.items); + const max: i64 = std.mem.max(i64, partials.items); + + if (min == 0 and max == 0) { + return try alloc.dupe(u8, "0 minutes"); + } + + var buf = std.ArrayList(u8){}; + errdefer buf.deinit(alloc); + + var count: i32 = 0; + for (partials.items, 0..) |partial, i| { + if (partial > 0) { + if (count >= 2) break; + try buf.appendSlice(alloc, try std.fmt.allocPrint(alloc, "{d} {s}{s}", .{ partial, TIME_STRINGS[i], if (partial > 1) "s" else "" })); + if (count == 0 and i < partials.items.len - 1) try buf.appendSlice(alloc, ", "); + count += 1; + } + } + + return try buf.toOwnedSlice(alloc); + } +}; + +// https://github.com/cassioneri/eaf/blob/1509faf37a0e0f59f5d4f11d0456fd0973c08f85/eaf/gregorian.hpp#L42 +fn rata_to_date(N: i64) Date { + checkRange(N, RATA_MIN, RATA_MAX); + + // Century. + const N_1: i64 = 4 * N + 3; + const C: i64 = quotient(N_1, 146097); + const N_C: u32 = remainder(N_1, 146097) / 4; + + // Year. + const N_2 = 4 * N_C + 3; + const Z: u32 = N_2 / 1461; + const N_Y: u32 = N_2 % 1461 / 4; + const Y: i64 = 100 * C + Z; + + // Month and day. + const N_3: u32 = 5 * N_Y + 461; + const M: u32 = N_3 / 153; + const D: u32 = N_3 % 153 / 5; + + // Map. + const J: u32 = @intFromBool(M >= 13); + + return .{ + .year = @intCast(Y + J), + .month = @intCast(M - 12 * J), + .day = @intCast(D + 1), + }; +} + +// https://github.com/cassioneri/eaf/blob/1509faf37a0e0f59f5d4f11d0456fd0973c08f85/eaf/gregorian.hpp#L88 +fn date_to_rata(date: Date) i32 { + checkRange(date, Date.MIN, Date.MAX); + + // Map. + const J: u32 = @intFromBool(date.month <= 2); + const Y: i32 = date.year - @as(i32, @intCast(J)); + const M: u32 = date.month + 12 * J; + const D: u32 = date.day - 1; + const C: i32 = @intCast(quotient(Y, 100)); + + // Rata die. + const y_star: i32 = @intCast(quotient(1461 * @as(i64, Y), 4) - C + quotient(C, 4)); // n_days in all prev. years + const m_star: u32 = (153 * M - 457) / 5; // n_days in prev. months + + return y_star + @as(i32, @intCast(m_star)) + @as(i32, @intCast(D)); +} + +fn quotient(n: i64, d: u32) i64 { + return if (n >= 0) @divTrunc(n, d) else @divTrunc((n + 1), d) - 1; +} + +fn remainder(n: i64, d: u32) u32 { + return @intCast(if (n >= 0) @mod(n, d) else (n + d) - d * quotient((n + d), d)); +} + +// const testing = @import("testing.zig"); +/// Attempts to print `arg` into a buf and then compare those strings. +pub const allocator = std.testing.allocator; +pub fn expectFmt(arg: anytype, expected: []const u8) !void { + var wb = std.io.Writer.Allocating.init(allocator); + defer wb.deinit(); + + try wb.writer.print("{f}", .{arg}); + try std.testing.expectEqualStrings(expected, wb.written()); +} + +pub fn expectEqual(res: anytype, expected: meta.Const(@TypeOf(res))) TimeError!void { + if (meta.isOptional(@TypeOf(res))) { + if (expected) |e| return expectEqual(res orelse return error.ExpectedValue, e); + if (res != null) return error.ExpectedNull; + } + + // TODO: find all usages of expectEqualStrings and replace it with our expectEqual + if (meta.isString(@TypeOf(res))) { + return std.testing.expectEqualStrings(expected, res); + } + + return std.testing.expectEqual(expected, res); +} + +// test "basic usage" { +// const t1 = Time.unix(1234567890); +// try expectFmt(t1, "2009-02-13 23:31:30 UTC"); +// +// try expectEqual(t1.date(), .{ +// .year = 2009, +// .month = 2, +// .day = 13, +// }); +// +// try expectEqual(t1.hour(), 23); +// try expectEqual(t1.minute(), 31); +// try expectEqual(t1.second(), 30); +// +// const t2 = t1.setHour(10).setMinute(15).setSecond(45); +// try expectFmt(t2, "2009-02-13 10:15:45 UTC"); +// +// const t3 = t2.add(.hours, 14).add(.minutes, 46).add(.seconds, 18); +// try expectFmt(t3, "2009-02-14 01:02:03 UTC"); +// +// // t.next() +// try expectFmt(t3.next(.second), "2009-02-14 01:02:04 UTC"); +// try expectFmt(t3.next(.minute), "2009-02-14 01:03:00 UTC"); +// try expectFmt(t3.next(.hour), "2009-02-14 02:00:00 UTC"); +// try expectFmt(t3.next(.day), "2009-02-15 00:00:00 UTC"); +// +// // t.setStartOf() +// try expectFmt(t3.setStartOf(.minute), "2009-02-14 01:02:00 UTC"); +// try expectFmt(t3.setStartOf(.hour), "2009-02-14 01:00:00 UTC"); +// try expectFmt(t3.setStartOf(.day), "2009-02-14 00:00:00 UTC"); +// try expectFmt(t3.setStartOf(.month), "2009-02-01 00:00:00 UTC"); +// try expectFmt(t3.setStartOf(.year), "2009-01-01 00:00:00 UTC"); +// +// // t.setEndOf() +// try expectFmt(t3.setEndOf(.minute), "2009-02-14 01:02:59 UTC"); +// try expectFmt(t3.setEndOf(.hour), "2009-02-14 01:59:59 UTC"); +// try expectFmt(t3.setEndOf(.day), "2009-02-14 23:59:59 UTC"); +// try expectFmt(t3.setEndOf(.month), "2009-02-28 23:59:59 UTC"); +// try expectFmt(t3.setEndOf(.year), "2009-12-31 23:59:59 UTC"); +// } +// +// test "edge-cases" { +// const jan31 = Date.ymd(2023, 1, 31); +// try expectEqual(jan31.add(.month, 1), Date.ymd(2023, 2, 28)); +// try expectEqual(jan31.add(.month, 2), Date.ymd(2023, 3, 31)); +// try expectEqual(jan31.add(.month, -1), Date.ymd(2022, 12, 31)); +// try expectEqual(jan31.add(.month, -2), Date.ymd(2022, 11, 30)); +// try expectEqual(jan31.add(.year, 1).add(.month, 1), Date.ymd(2024, 2, 29)); +// +// const feb29 = Time.unix(951782400); // 2000-02-29 00:00:00 +// try expectFmt(feb29.setEndOf(.month), "2000-02-29 23:59:59 UTC"); +// try expectFmt(feb29.add(.years, 1), "2001-02-28 00:00:00 UTC"); +// try expectFmt(feb29.add(.years, 4), "2004-02-29 00:00:00 UTC"); +// } +// +// test isLeapYear { +// try testing.expect(!isLeapYear(1999)); +// try testing.expect(isLeapYear(2000)); +// try testing.expect(isLeapYear(2004)); +// } +// +// test daysInMonth { +// try expectEqual(daysInMonth(1999, 2), 28); +// try expectEqual(daysInMonth(2000, 2), 29); +// try expectEqual(daysInMonth(2000, 7), 31); +// try expectEqual(daysInMonth(2000, 8), 31); +// } +// +// test rata_to_date { +// try expectEqual(rata_to_date(RATA_MIN), Date.MIN); +// try expectEqual(rata_to_date(RATA_MAX), Date.MAX); +// +// try expectEqual(rata_to_date(0), .ymd(0, 3, 1)); +// try expectEqual(rata_to_date(RATA_TO_UNIX), .ymd(1970, 1, 1)); +// } +// +// test date_to_rata { +// try expectEqual(date_to_rata(Date.MIN), RATA_MIN); +// try expectEqual(date_to_rata(Date.MAX), RATA_MAX); +// +// try expectEqual(date_to_rata(.ymd(0, 3, 1)), 0); +// try expectEqual(date_to_rata(.ymd(1970, 1, 1)), RATA_TO_UNIX); +// }