update: time

This commit is contained in:
Lucas F. 2026-01-17 16:04:30 -03:00
parent e11d3fb034
commit f8e7b01e9c

874
src/time.zig Normal file
View file

@ -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 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 ( lida com excessos, mas aqui é 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);
// }