zdt-prov/src/time.zig
2026-01-17 16:04:30 -03:00

874 lines
28 KiB
Zig
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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