From 69069d39dbc803f7ecd4ae7aefdedbe8a5f3b1c5 Mon Sep 17 00:00:00 2001 From: MrBounty Date: Thu, 17 Oct 2024 21:43:50 +0200 Subject: [PATCH] Base date type --- src/types/date.zig | 499 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 499 insertions(+) create mode 100644 src/types/date.zig diff --git a/src/types/date.zig b/src/types/date.zig new file mode 100644 index 0000000..ab08bb3 --- /dev/null +++ b/src/types/date.zig @@ -0,0 +1,499 @@ +// Thanks to: https://github.com/nektro/zig-time/blob/master/time.zig + +const std = @import("std"); +const string = []const u8; +const extras = @import("extras"); +const time = @This(); + +pub const DateTime = struct { + ms: u16, + seconds: u8, + minutes: u8, + hours: u8, + days: u8, + months: u8, + years: u16, + timezone: TimeZone, + + const Self = @This(); + + pub fn initUnixMs(unix: u64) Self { + return epoch_unix.addMs(unix); + } + + pub fn initUnix(unix: u64) Self { + return epoch_unix.addSecs(unix); + } + + /// Caller asserts that this is > epoch + pub fn init(year: u16, month: u16, day: u16, hr: u16, min: u16, sec: u16) Self { + return epoch_unix + .addYears(year - epoch_unix.years) + .addMonths(month) + .addDays(day) + .addHours(hr) + .addMins(min) + .addSecs(sec); + } + + pub fn now() Self { + return initUnixMs(@intCast(std.time.milliTimestamp())); + } + + pub const epoch_unix = Self{ + .ms = 0, + .seconds = 0, + .minutes = 0, + .hours = 0, + .days = 0, + .months = 0, + .years = 1970, // Why ? + .timezone = .UTC, + }; + + pub fn eql(self: Self, other: Self) bool { + return self.ms == other.ms and + self.seconds == other.seconds and + self.minutes == other.minutes and + self.hours == other.hours and + self.days == other.days and + self.months == other.months and + self.years == other.years and + self.timezone == other.timezone and + self.weekday == other.weekday; + } + + // So as long as the count / unit, it continue adding the next unit, smart + pub fn addMs(self: Self, count: u64) Self { + if (count == 0) return self; + var result = self; + result.ms += @intCast(count % 1000); + return result.addSecs(count / 1000); + } + + pub fn addSecs(self: Self, count: u64) Self { + if (count == 0) return self; + var result = self; + result.seconds += @intCast(count % 60); + return result.addMins(count / 60); + } + + pub fn addMins(self: Self, count: u64) Self { + if (count == 0) return self; + var result = self; + result.minutes += @intCast(count % 60); + return result.addHours(count / 60); + } + + pub fn addHours(self: Self, count: u64) Self { + if (count == 0) return self; + var result = self; + result.hours += @intCast(count % 24); + return result.addDays(count / 24); + } + + pub fn addDays(self: Self, count: u64) Self { + if (count == 0) return self; + var result = self; + var input = count; + + while (true) { + const year_len = result.daysThisYear(); + if (input >= year_len) { + result.years += 1; + input -= year_len; + continue; + } + break; + } + while (true) { + const month_len = result.daysThisMonth(); + if (input >= month_len) { + result.months += 1; + input -= month_len; + + if (result.months == 12) { + result.years += 1; + result.months = 0; + } + continue; + } + break; + } + { + const month_len = result.daysThisMonth(); + if (result.days + input > month_len) { + const left = month_len - result.days; + input -= left; + result.months += 1; + result.days = 0; + } + result.days += @intCast(input); + + if (result.days == result.daysThisMonth()) { + result.months += 1; + result.days = 0; + } + if (result.months == 12) { + result.years += 1; + result.months = 0; + } + } + + std.debug.assert(result.ms < 1000); + std.debug.assert(result.seconds < 60); + std.debug.assert(result.minutes < 60); + std.debug.assert(result.hours < 24); + std.debug.assert(result.days < result.daysThisMonth()); + std.debug.assert(result.months < 12); + return result; + } + + pub fn addMonths(self: Self, count: u64) Self { + if (count == 0) return self; + var result = self; + var input = count; + while (input > 0) { + const new = result.addDays(result.daysThisMonth()); + result = new; + input -= 1; + } + return result; + } + + pub fn addYears(self: Self, count: u64) Self { + var result = self; + for (0..count) |_| { + result = result.addDays(result.daysThisYear()); + } + return result; + } + + pub fn isLeapYear(self: Self) bool { + return time.isLeapYear(self.years); + } + + pub fn daysThisYear(self: Self) u16 { + return time.daysInYear(self.years); + } + + pub fn daysThisMonth(self: Self) u16 { + return self.daysInMonth(self.months); + } + + fn daysInMonth(self: Self, month: u16) u16 { + return time.daysInMonth(self.years, month); + } + + pub fn dayOfThisYear(self: Self) u16 { + var ret: u16 = 0; + for (0..self.months) |item| { + ret += self.daysInMonth(@intCast(item)); + } + ret += self.days; + return ret; + } + + pub fn toUnix(self: Self) u64 { + const x = self.toUnixMilli(); + return x / 1000; + } + + pub fn toUnixMilli(self: Self) u64 { + var res: u64 = 0; + res += self.ms; + res += @as(u64, self.seconds) * std.time.ms_per_s; + res += @as(u64, self.minutes) * std.time.ms_per_min; + res += @as(u64, self.hours) * std.time.ms_per_hour; + res += self.daysSinceEpoch() * std.time.ms_per_day; + return res; + } + + fn daysSinceEpoch(self: Self) u64 { + var res: u64 = 0; + res += self.days; + for (0..self.years - epoch_unix.years) |i| res += time.daysInYear(@intCast(i)); + for (0..self.months) |i| res += self.daysInMonth(@intCast(i)); + return res; + } + + /// fmt is based on https://momentjs.com/docs/#/displaying/format/ + pub fn format(self: Self, comptime fmt: string, options: std.fmt.FormatOptions, writer: anytype) !void { + _ = options; + + if (fmt.len == 0) @compileError("DateTime: format string can't be empty"); + + @setEvalBranchQuota(100000); + + comptime var s = 0; + comptime var e = 0; + comptime var next: ?FormatSeq = null; + inline for (fmt, 0..) |c, i| { + e = i + 1; + + if (comptime std.meta.stringToEnum(FormatSeq, fmt[s..e])) |tag| { + next = tag; + if (i < fmt.len - 1) continue; + } + + if (next) |tag| { + switch (tag) { + .MM => try writer.print("{:0>2}", .{self.months + 1}), + .M => try writer.print("{}", .{self.months + 1}), + .Mo => try printOrdinal(writer, self.months + 1), + .MMM => try printLongName(writer, self.months, &[_]string{ "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec" }), + .MMMM => try printLongName(writer, self.months, &[_]string{ "January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December" }), + + .Q => try writer.print("{}", .{self.months / 3 + 1}), + .Qo => try printOrdinal(writer, self.months / 3 + 1), + + .D => try writer.print("{}", .{self.days + 1}), + .Do => try printOrdinal(writer, self.days + 1), + .DD => try writer.print("{:0>2}", .{self.days + 1}), + + .DDD => try writer.print("{}", .{self.dayOfThisYear() + 1}), + .DDDo => try printOrdinal(writer, self.dayOfThisYear() + 1), + .DDDD => try writer.print("{:0>3}", .{self.dayOfThisYear() + 1}), + + .d => try writer.print("{}", .{@intFromEnum(self.weekday())}), + .do => try printOrdinal(writer, @intFromEnum(self.weekday())), + .dd => try writer.writeAll(@tagName(self.weekday())[0..2]), + .ddd => try writer.writeAll(@tagName(self.weekday())), + .dddd => try printLongName(writer, @intFromEnum(self.weekday()), &[_]string{ "Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday" }), + .e => try writer.print("{}", .{@intFromEnum(self.weekday())}), + .E => try writer.print("{}", .{@intFromEnum(self.weekday()) + 1}), + + .w => try writer.print("{}", .{self.dayOfThisYear() / 7 + 1}), + .wo => try printOrdinal(writer, self.dayOfThisYear() / 7 + 1), + .ww => try writer.print("{:0>2}", .{self.dayOfThisYear() / 7 + 1}), + + .Y => try writer.print("{}", .{self.years + 10000}), + .YY => try writer.print("{:0>2}", .{self.years % 100}), + .YYY => try writer.print("{}", .{self.years}), + .YYYY => try writer.print("{:0>4}", .{self.years}), + + .N => try writer.writeAll(@tagName(self.era())), + .NN => try writer.writeAll("Anno Domini"), + + .A => try printLongName(writer, self.hours / 12, &[_]string{ "AM", "PM" }), + .a => try printLongName(writer, self.hours / 12, &[_]string{ "am", "pm" }), + + .H => try writer.print("{}", .{self.hours}), + .HH => try writer.print("{:0>2}", .{self.hours}), + .h => try writer.print("{}", .{wrap(self.hours, 12)}), + .hh => try writer.print("{:0>2}", .{wrap(self.hours, 12)}), + .k => try writer.print("{}", .{wrap(self.hours, 24)}), + .kk => try writer.print("{:0>2}", .{wrap(self.hours, 24)}), + + .m => try writer.print("{}", .{self.minutes}), + .mm => try writer.print("{:0>2}", .{self.minutes}), + + .s => try writer.print("{}", .{self.seconds}), + .ss => try writer.print("{:0>2}", .{self.seconds}), + + .S => try writer.print("{}", .{self.ms / 100}), + .SS => try writer.print("{:0>2}", .{self.ms / 10}), + .SSS => try writer.print("{:0>3}", .{self.ms}), + + .z => try writer.writeAll(@tagName(self.timezone)), + .Z => try writer.writeAll("+00:00"), + .ZZ => try writer.writeAll("+0000"), + + .x => try writer.print("{}", .{self.toUnixMilli()}), + .X => try writer.print("{}", .{self.toUnix()}), + } + next = null; + s = i; + } + + switch (c) { + ',', + ' ', + ':', + '-', + '.', + 'T', + 'W', + '/', + => { + try writer.writeAll(&.{c}); + s = i + 1; + continue; + }, + else => {}, + } + } + } + + pub fn formatAlloc(self: Self, alloc: std.mem.Allocator, comptime fmt: string) !string { + var list = std.ArrayList(u8).init(alloc); + defer list.deinit(); + + try self.format(fmt, .{}, list.writer()); + return list.toOwnedSlice(); + } + + const FormatSeq = enum { + M, // 1 2 ... 11 12 + Mo, // 1st 2nd ... 11th 12th + MM, // 01 02 ... 11 12 + MMM, // Jan Feb ... Nov Dec + MMMM, // January February ... November December + Q, // 1 2 3 4 + Qo, // 1st 2nd 3rd 4th + D, // 1 2 ... 30 31 + Do, // 1st 2nd ... 30th 31st + DD, // 01 02 ... 30 31 + DDD, // 1 2 ... 364 365 + DDDo, // 1st 2nd ... 364th 365th + DDDD, // 001 002 ... 364 365 + d, // 0 1 ... 5 6 + do, // 0th 1st ... 5th 6th + dd, // Su Mo ... Fr Sa + ddd, // Sun Mon ... Fri Sat + dddd, // Sunday Monday ... Friday Saturday + e, // 0 1 ... 5 6 (locale) + E, // 1 2 ... 6 7 (ISO) + w, // 1 2 ... 52 53 + wo, // 1st 2nd ... 52nd 53rd + ww, // 01 02 ... 52 53 + Y, // 11970 11971 ... 19999 20000 20001 (Holocene calendar) + YY, // 70 71 ... 29 30 + YYY, // 1 2 ... 1970 1971 ... 2029 2030 + YYYY, // 0001 0002 ... 1970 1971 ... 2029 2030 + N, // BC AD + NN, // Before Christ ... Anno Domini + A, // AM PM + a, // am pm + H, // 0 1 ... 22 23 + HH, // 00 01 ... 22 23 + h, // 1 2 ... 11 12 + hh, // 01 02 ... 11 12 + k, // 1 2 ... 23 24 + kk, // 01 02 ... 23 24 + m, // 0 1 ... 58 59 + mm, // 00 01 ... 58 59 + s, // 0 1 ... 58 59 + ss, // 00 01 ... 58 59 + S, // 0 1 ... 8 9 (second fraction) + SS, // 00 01 ... 98 99 + SSS, // 000 001 ... 998 999 + z, // EST CST ... MST PST + Z, // -07:00 -06:00 ... +06:00 +07:00 + ZZ, // -0700 -0600 ... +0600 +0700 + x, // unix milli + X, // unix + }; + + pub fn since(self: Self, other_in_the_past: Self) Duration { + return Duration{ + .ms = self.toUnixMilli() - other_in_the_past.toUnixMilli(), + }; + } + + pub fn era(self: Self) Era { + if (self.years >= 0) return .AD; + @compileError("TODO"); + } + + pub fn weekday(self: Self) WeekDay { + var i = self.daysSinceEpoch() % 7; + var result = WeekDay.Thu; // weekday of epoch_unix + while (i > 0) : (i -= 1) { + result = result.next(); + } + return result; + } +}; + +pub const format = struct { + pub const LT = "h:mm A"; + pub const LTS = "h:mm:ss A"; + pub const L = "MM/DD/YYYY"; + pub const l = "M/D/YYY"; + pub const LL = "MMMM D, YYYY"; + pub const ll = "MMM D, YYY"; + pub const LLL = LL ++ " " ++ LT; + pub const lll = ll ++ " " ++ LT; + pub const LLLL = "dddd, " ++ LLL; + pub const llll = "ddd, " ++ lll; +}; + +pub const TimeZone = enum { + UTC, + + usingnamespace extras.TagNameJsonStringifyMixin(@This()); +}; + +pub const WeekDay = enum { + Sun, + Mon, + Tue, + Wed, + Thu, + Fri, + Sat, + + pub fn next(self: WeekDay) WeekDay { + return switch (self) { + .Sun => .Mon, + .Mon => .Tue, + .Tue => .Wed, + .Wed => .Thu, + .Thu => .Fri, + .Fri => .Sat, + .Sat => .Sun, + }; + } + + usingnamespace extras.TagNameJsonStringifyMixin(@This()); +}; + +pub const Era = enum { + // BC, + AD, + + usingnamespace extras.TagNameJsonStringifyMixin(@This()); +}; + +pub fn isLeapYear(year: u16) bool { + var ret = false; + if (year % 4 == 0) ret = true; + if (year % 100 == 0) ret = false; + if (year % 400 == 0) ret = true; + return ret; +} + +pub fn daysInYear(year: u16) u16 { + return if (isLeapYear(year)) 366 else 365; +} + +fn daysInMonth(year: u16, month: u16) u16 { + const norm = [12]u16{ 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 }; + const leap = [12]u16{ 31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 }; + const month_days = if (!isLeapYear(year)) norm else leap; + return month_days[month]; +} + +fn printOrdinal(writer: anytype, num: u16) !void { + try writer.print("{}", .{num}); + try writer.writeAll(switch (num) { + 1 => "st", + 2 => "nd", + 3 => "rd", + else => "th", + }); +} + +fn printLongName(writer: anytype, index: u16, names: []const string) !void { + try writer.writeAll(names[index]); +} + +fn wrap(val: u16, at: u16) u16 { + const tmp = val % at; + return if (tmp == 0) at else tmp; +} + +pub const Duration = struct { + ms: u64, +};