// 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); // }