update: time
This commit is contained in:
parent
e11d3fb034
commit
f8e7b01e9c
1 changed files with 874 additions and 0 deletions
874
src/time.zig
Normal file
874
src/time.zig
Normal 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 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);
|
||||||
|
// }
|
||||||
Loading…
Add table
Add a link
Reference in a new issue