From ef9a7d06e49a55469465caa04e7fbe4f175a0182 Mon Sep 17 00:00:00 2001 From: Nathan Anderson Date: Wed, 2 Aug 2023 01:12:32 -0600 Subject: [PATCH] Added better datetime and fixed transaction fetching for beginning of month, I think, added edit and delete of transactions and categories --- src/.deps/datetime.zig | 1702 +++++++++++++++++++++++++++++++++++ src/.deps/timezones.zig | 677 ++++++++++++++ src/db/db.zig | 9 + src/db/models.zig | 17 + src/http_handler.zig | 19 +- src/routes/budget.zig | 35 + src/routes/transactions.zig | 47 +- src/utils.zig | 11 + 8 files changed, 2507 insertions(+), 10 deletions(-) create mode 100644 src/.deps/datetime.zig create mode 100644 src/.deps/timezones.zig diff --git a/src/.deps/datetime.zig b/src/.deps/datetime.zig new file mode 100644 index 0000000..721f342 --- /dev/null +++ b/src/.deps/datetime.zig @@ -0,0 +1,1702 @@ +// -------------------------------------------------------------------------- // +// Copyright (c) 2019-2022, Jairus Martin. // +// Distributed under the terms of the MIT License. // +// The full license is in the file LICENSE, distributed with this software. // +// -------------------------------------------------------------------------- // + +// Some of this is ported from cpython's datetime module +const std = @import("std"); +const time = std.time; +const math = std.math; +const ascii = std.ascii; +const Allocator = std.mem.Allocator; +const Order = std.math.Order; + +pub const timezones = @import("timezones.zig"); + +const testing = std.testing; +const assert = std.debug.assert; + +// Number of days in each month not accounting for leap year +pub const Weekday = enum(u3) { + Monday = 1, + Tuesday, + Wednesday, + Thursday, + Friday, + Saturday, + Sunday, +}; + +pub const Month = enum(u4) { + January = 1, + February, + March, + April, + May, + June, + July, + August, + September, + October, + November, + December, + + // Convert an abbreviation, eg Jan to the enum value + pub fn parseAbbr(month: []const u8) !Month { + if (month.len == 3) { + inline for (std.meta.fields(Month)) |f| { + if (ascii.eqlIgnoreCase(f.name[0..3], month)) { + return @intToEnum(Month, f.value); + } + } + } + return error.InvalidFormat; + } + + pub fn parseName(month: []const u8) !Month { + inline for (std.meta.fields(Month)) |f| { + if (ascii.eqlIgnoreCase(f.name, month)) { + return @intToEnum(Month, f.value); + } + } + return error.InvalidFormat; + } +}; + +test "month-parse-abbr" { + try testing.expectEqual(try Month.parseAbbr("Jan"), .January); + try testing.expectEqual(try Month.parseAbbr("Oct"), .October); + try testing.expectEqual(try Month.parseAbbr("sep"), .September); + try testing.expectError(error.InvalidFormat, Month.parseAbbr("cra")); +} + +test "month-parse" { + try testing.expectEqual(try Month.parseName("January"), .January); + try testing.expectEqual(try Month.parseName("OCTOBER"), .October); + try testing.expectEqual(try Month.parseName("july"), .July); + try testing.expectError(error.InvalidFormat, Month.parseName("NoShaveNov")); +} + +pub const MIN_YEAR: u16 = 1; +pub const MAX_YEAR: u16 = 9999; +pub const MAX_ORDINAL: u32 = 3652059; + +const DAYS_IN_MONTH = [12]u8{ 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 }; +const DAYS_BEFORE_MONTH = [12]u16{ 0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334 }; + +pub fn isLeapYear(year: u32) bool { + return year % 4 == 0 and (year % 100 != 0 or year % 400 == 0); +} + +pub fn isLeapDay(year: u32, month: u32, day: u32) bool { + return isLeapYear(year) and month == 2 and day == 29; +} + +test "leapyear" { + try testing.expect(isLeapYear(2019) == false); + try testing.expect(isLeapYear(2018) == false); + try testing.expect(isLeapYear(2017) == false); + try testing.expect(isLeapYear(2016) == true); + try testing.expect(isLeapYear(2000) == true); + try testing.expect(isLeapYear(1900) == false); +} + +// Number of days before Jan 1st of year +pub fn daysBeforeYear(year: u32) u32 { + var y: u32 = year - 1; + return y * 365 + @divFloor(y, 4) - @divFloor(y, 100) + @divFloor(y, 400); +} + +// Days before 1 Jan 1970 +const EPOCH = daysBeforeYear(1970) + 1; + +test "daysBeforeYear" { + try testing.expect(daysBeforeYear(1996) == 728658); + try testing.expect(daysBeforeYear(2019) == 737059); +} + +// Number of days in that month for the year +pub fn daysInMonth(year: u32, month: u32) u8 { + assert(1 <= month and month <= 12); + if (month == 2 and isLeapYear(year)) return 29; + return DAYS_IN_MONTH[month - 1]; +} + +test "daysInMonth" { + try testing.expect(daysInMonth(2019, 1) == 31); + try testing.expect(daysInMonth(2019, 2) == 28); + try testing.expect(daysInMonth(2016, 2) == 29); +} + +// Number of days in year preceding the first day of month +pub fn daysBeforeMonth(year: u32, month: u32) u32 { + assert(month >= 1 and month <= 12); + var d = DAYS_BEFORE_MONTH[month - 1]; + if (month > 2 and isLeapYear(year)) d += 1; + return d; +} + +// Return number of days since 01-Jan-0001 +fn ymd2ord(year: u16, month: u8, day: u8) u32 { + assert(month >= 1 and month <= 12); + assert(day >= 1 and day <= daysInMonth(year, month)); + return daysBeforeYear(year) + daysBeforeMonth(year, month) + day; +} + +test "ymd2ord" { + try testing.expect(ymd2ord(1970, 1, 1) == 719163); + try testing.expect(ymd2ord(28, 2, 29) == 9921); + try testing.expect(ymd2ord(2019, 11, 27) == 737390); + try testing.expect(ymd2ord(2019, 11, 28) == 737391); +} + +test "days-before-year" { + const DI400Y = daysBeforeYear(401); // Num of days in 400 years + const DI100Y = daysBeforeYear(101); // Num of days in 100 years + const DI4Y = daysBeforeYear(5); // Num of days in 4 years + + // A 4-year cycle has an extra leap day over what we'd get from pasting + // together 4 single years. + try testing.expect(DI4Y == 4 * 365 + 1); + + // Similarly, a 400-year cycle has an extra leap day over what we'd get from + // pasting together 4 100-year cycles. + try testing.expect(DI400Y == 4 * DI100Y + 1); + + // OTOH, a 100-year cycle has one fewer leap day than we'd get from + // pasting together 25 4-year cycles. + try testing.expect(DI100Y == 25 * DI4Y - 1); +} + +// Calculate the number of days of the first monday for week 1 iso calendar +// for the given year since 01-Jan-0001 +pub fn daysBeforeFirstMonday(year: u16) u32 { + // From cpython/datetime.py _isoweek1monday + const THURSDAY = 3; + const first_day = ymd2ord(year, 1, 1); + const first_weekday = (first_day + 6) % 7; + var week1_monday = first_day - first_weekday; + if (first_weekday > THURSDAY) { + week1_monday += 7; + } + return week1_monday; +} + +test "iso-first-monday" { + // Created using python + const years = [20]u16{ 1816, 1823, 1839, 1849, 1849, 1870, 1879, 1882, 1909, 1910, 1917, 1934, 1948, 1965, 1989, 2008, 2064, 2072, 2091, 2096 }; + const output = [20]u32{ 662915, 665470, 671315, 674969, 674969, 682641, 685924, 687023, 696886, 697250, 699805, 706014, 711124, 717340, 726104, 733041, 753495, 756421, 763358, 765185 }; + for (years, 0..) |year, i| { + try testing.expectEqual(daysBeforeFirstMonday(year), output[i]); + } +} + +pub const ISOCalendar = struct { + year: u16, + week: u6, // Week of year 1-53 + weekday: u3, // Day of week 1-7 +}; + +pub const Date = struct { + year: u16, + month: u4 = 1, // Month of year + day: u8 = 1, // Day of month + + // Create and validate the date + pub fn create(year: u32, month: u32, day: u32) !Date { + if (year < MIN_YEAR or year > MAX_YEAR) return error.InvalidDate; + if (month < 1 or month > 12) return error.InvalidDate; + if (day < 1 or day > daysInMonth(year, month)) return error.InvalidDate; + // Since we just validated the ranges we can now savely cast + return Date{ + .year = @intCast(u16, year), + .month = @intCast(u4, month), + .day = @intCast(u8, day), + }; + } + + // Return a copy of the date + pub fn copy(self: Date) !Date { + return Date.create(self.year, self.month, self.day); + } + + // Create a Date from the number of days since 01-Jan-0001 + pub fn fromOrdinal(ordinal: u32) Date { + // n is a 1-based index, starting at 1-Jan-1. The pattern of leap years + // repeats exactly every 400 years. The basic strategy is to find the + // closest 400-year boundary at or before n, then work with the offset + // from that boundary to n. Life is much clearer if we subtract 1 from + // n first -- then the values of n at 400-year boundaries are exactly + // those divisible by DI400Y: + // + // D M Y n n-1 + // -- --- ---- ---------- ---------------- + // 31 Dec -400 -DI400Y -DI400Y -1 + // 1 Jan -399 -DI400Y +1 -DI400Y 400-year boundary + // ... + // 30 Dec 000 -1 -2 + // 31 Dec 000 0 -1 + // 1 Jan 001 1 0 400-year boundary + // 2 Jan 001 2 1 + // 3 Jan 001 3 2 + // ... + // 31 Dec 400 DI400Y DI400Y -1 + // 1 Jan 401 DI400Y +1 DI400Y 400-year boundary + assert(ordinal >= 1 and ordinal <= MAX_ORDINAL); + + var n = ordinal - 1; + const DI400Y = comptime daysBeforeYear(401); // Num of days in 400 years + const DI100Y = comptime daysBeforeYear(101); // Num of days in 100 years + const DI4Y = comptime daysBeforeYear(5); // Num of days in 4 years + const n400 = @divFloor(n, DI400Y); + n = @mod(n, DI400Y); + var year = n400 * 400 + 1; // ..., -399, 1, 401, ... + + // Now n is the (non-negative) offset, in days, from January 1 of year, to + // the desired date. Now compute how many 100-year cycles precede n. + // Note that it's possible for n100 to equal 4! In that case 4 full + // 100-year cycles precede the desired day, which implies the desired + // day is December 31 at the end of a 400-year cycle. + const n100 = @divFloor(n, DI100Y); + n = @mod(n, DI100Y); + + // Now compute how many 4-year cycles precede it. + const n4 = @divFloor(n, DI4Y); + n = @mod(n, DI4Y); + + // And now how many single years. Again n1 can be 4, and again meaning + // that the desired day is December 31 at the end of the 4-year cycle. + const n1 = @divFloor(n, 365); + n = @mod(n, 365); + + year += n100 * 100 + n4 * 4 + n1; + + if (n1 == 4 or n100 == 4) { + assert(n == 0); + return Date.create(year - 1, 12, 31) catch unreachable; + } + + // Now the year is correct, and n is the offset from January 1. We find + // the month via an estimate that's either exact or one too large. + var leapyear = (n1 == 3) and (n4 != 24 or n100 == 3); + assert(leapyear == isLeapYear(year)); + var month = (n + 50) >> 5; + if (month == 0) month = 12; // Loop around + var preceding = daysBeforeMonth(year, month); + + if (preceding > n) { // estimate is too large + month -= 1; + if (month == 0) month = 12; // Loop around + preceding -= daysInMonth(year, month); + } + n -= preceding; + // assert(n > 0 and n < daysInMonth(year, month)); + + // Now the year and month are correct, and n is the offset from the + // start of that month: we're done! + return Date.create(year, month, n + 1) catch unreachable; + } + + // Return proleptic Gregorian ordinal for the year, month and day. + // January 1 of year 1 is day 1. Only the year, month and day values + // contribute to the result. + pub fn toOrdinal(self: Date) u32 { + return ymd2ord(self.year, self.month, self.day); + } + + // Returns todays date + pub fn now() Date { + return Date.fromTimestamp(time.milliTimestamp()); + } + + // Create a date from the number of seconds since 1 Jan 1970 + pub fn fromSeconds(seconds: f64) Date { + const r = math.modf(seconds); + const timestamp = @floatToInt(i64, r.ipart); // Seconds + const days = @divFloor(timestamp, time.s_per_day) + @as(i64, EPOCH); + assert(days >= 0 and days <= MAX_ORDINAL); + return Date.fromOrdinal(@intCast(u32, days)); + } + + // Return the number of seconds since 1 Jan 1970 + pub fn toSeconds(self: Date) f64 { + const days = @intCast(i64, self.toOrdinal()) - @as(i64, EPOCH); + return @intToFloat(f64, days * time.s_per_day); + } + + // Create a date from a UTC timestamp in milliseconds relative to Jan 1st 1970 + pub fn fromTimestamp(timestamp: i64) Date { + const days = @divFloor(timestamp, time.ms_per_day) + @as(i64, EPOCH); + assert(days >= 0 and days <= MAX_ORDINAL); + return Date.fromOrdinal(@intCast(u32, days)); + } + + // Create a UTC timestamp in milliseconds relative to Jan 1st 1970 + pub fn toTimestamp(self: Date) i64 { + const d = @intCast(i64, daysBeforeYear(self.year)); + const days = d - @as(i64, EPOCH) + @intCast(i64, self.dayOfYear()); + return @intCast(i64, days) * time.ms_per_day; + } + + // Convert to an ISOCalendar date containing the year, week number, and + // weekday. First week is 1. Monday is 1, Sunday is 7. + pub fn isoCalendar(self: Date) ISOCalendar { + // Ported from python's isocalendar. + var y = self.year; + var first_monday = daysBeforeFirstMonday(y); + const today = ymd2ord(self.year, self.month, self.day); + if (today < first_monday) { + y -= 1; + first_monday = daysBeforeFirstMonday(y); + } + const days_between = today - first_monday; + var week = @divFloor(days_between, 7); + var day = @mod(days_between, 7); + if (week >= 52 and today >= daysBeforeFirstMonday(y + 1)) { + y += 1; + week = 0; + } + assert(week >= 0 and week < 53); + assert(day >= 0 and day < 8); + return ISOCalendar{ .year = y, .week = @intCast(u6, week + 1), .weekday = @intCast(u3, day + 1) }; + } + + // ------------------------------------------------------------------------ + // Comparisons + // ------------------------------------------------------------------------ + pub fn eql(self: Date, other: Date) bool { + return self.cmp(other) == .eq; + } + + pub fn cmp(self: Date, other: Date) Order { + if (self.year > other.year) return .gt; + if (self.year < other.year) return .lt; + if (self.month > other.month) return .gt; + if (self.month < other.month) return .lt; + if (self.day > other.day) return .gt; + if (self.day < other.day) return .lt; + return .eq; + } + + pub fn gt(self: Date, other: Date) bool { + return self.cmp(other) == .gt; + } + + pub fn gte(self: Date, other: Date) bool { + const r = self.cmp(other); + return r == .eq or r == .gt; + } + + pub fn lt(self: Date, other: Date) bool { + return self.cmp(other) == .lt; + } + + pub fn lte(self: Date, other: Date) bool { + const r = self.cmp(other); + return r == .eq or r == .lt; + } + + // ------------------------------------------------------------------------ + // Parsing + // ------------------------------------------------------------------------ + // Parse date in format YYYY-MM-DD. Numbers must be zero padded. + pub fn parseIso(ymd: []const u8) !Date { + const value = std.mem.trim(u8, ymd, " "); + if (value.len != 10) return error.InvalidFormat; + const year = std.fmt.parseInt(u16, value[0..4], 10) catch return error.InvalidFormat; + const month = std.fmt.parseInt(u8, value[5..7], 10) catch return error.InvalidFormat; + const day = std.fmt.parseInt(u8, value[8..10], 10) catch return error.InvalidFormat; + return Date.create(year, month, day); + } + + // TODO: Parsing + + // ------------------------------------------------------------------------ + // Formatting + // ------------------------------------------------------------------------ + + // Return date in ISO format YYYY-MM-DD + const ISO_DATE_FMT = "{:0>4}-{:0>2}-{:0>2}"; + + pub fn formatIso(self: Date, allocator: Allocator) ![]u8 { + return std.fmt.allocPrint(allocator, ISO_DATE_FMT, .{ self.year, self.month, self.day }); + } + + pub fn formatIsoBuf(self: Date, buf: []u8) ![]u8 { + return std.fmt.bufPrint(buf, ISO_DATE_FMT, .{ self.year, self.month, self.day }); + } + + pub fn writeIso(self: Date, writer: anytype) !void { + try std.fmt.format(writer, ISO_DATE_FMT, .{ self.year, self.month, self.day }); + } + + // ------------------------------------------------------------------------ + // Properties + // ------------------------------------------------------------------------ + + // Return day of year starting with 1 + pub fn dayOfYear(self: Date) u16 { + var d = self.toOrdinal() - daysBeforeYear(self.year); + assert(d >= 1 and d <= 366); + return @intCast(u16, d); + } + + // Return day of week starting with Monday = 1 and Sunday = 7 + pub fn dayOfWeek(self: Date) Weekday { + const dow = @intCast(u3, self.toOrdinal() % 7); + return @intToEnum(Weekday, if (dow == 0) 7 else dow); + } + + // Return the ISO calendar based week of year. With 1 being the first week. + pub fn weekOfYear(self: Date) u8 { + return self.isoCalendar().week; + } + + // Return day of week starting with Monday = 0 and Sunday = 6 + pub fn weekday(self: Date) u4 { + return @enumToInt(self.dayOfWeek()) - 1; + } + + // Return whether the date is a weekend (Saturday or Sunday) + pub fn isWeekend(self: Date) bool { + return self.weekday() >= 5; + } + + // Return the name of the day of the week, eg "Sunday" + pub fn weekdayName(self: Date) []const u8 { + return @tagName(self.dayOfWeek()); + } + + // Return the name of the day of the month, eg "January" + pub fn monthName(self: Date) []const u8 { + assert(self.month >= 1 and self.month <= 12); + return @tagName(@intToEnum(Month, self.month)); + } + + // ------------------------------------------------------------------------ + // Operations + // ------------------------------------------------------------------------ + + // Return a copy of the date shifted by the given number of days + pub fn shiftDays(self: Date, days: i32) Date { + return self.shift(Delta{ .days = days }); + } + + // Return a copy of the date shifted by the given number of years + pub fn shiftYears(self: Date, years: i16) Date { + return self.shift(Delta{ .years = years }); + } + + pub const Delta = struct { + years: i16 = 0, + days: i32 = 0, + }; + + // Return a copy of the date shifted in time by the delta + pub fn shift(self: Date, delta: Delta) Date { + if (delta.years == 0 and delta.days == 0) { + return self.copy() catch unreachable; + } + + // Shift year + var year = self.year; + if (delta.years < 0) { + year -= @intCast(u16, -delta.years); + } else { + year += @intCast(u16, delta.years); + } + var ord = daysBeforeYear(year); + var days = self.dayOfYear(); + const from_leap = isLeapYear(self.year); + const to_leap = isLeapYear(year); + if (days == 59 and from_leap and to_leap) { + // No change before leap day + } else if (days < 59) { + // No change when jumping from leap day to leap day + } else if (to_leap and !from_leap) { + // When jumping to a leap year to non-leap year + // we have to add a leap day to the day of year + days += 1; + } else if (from_leap and !to_leap) { + // When jumping from leap year to non-leap year we have to undo + // the leap day added to the day of yearear + days -= 1; + } + ord += days; + + // Shift days + if (delta.days < 0) { + ord -= @intCast(u32, -delta.days); + } else { + ord += @intCast(u32, delta.days); + } + return Date.fromOrdinal(ord); + } +}; + +test "date-now" { + _ = Date.now(); +} + +test "date-compare" { + var d1 = try Date.create(2019, 7, 3); + var d2 = try Date.create(2019, 7, 3); + var d3 = try Date.create(2019, 6, 3); + var d4 = try Date.create(2020, 7, 3); + try testing.expect(d1.eql(d2)); + try testing.expect(d1.gt(d3)); + try testing.expect(d3.lt(d2)); + try testing.expect(d4.gt(d2)); +} + +test "date-from-ordinal" { + var date = Date.fromOrdinal(9921); + try testing.expectEqual(date.year, 28); + try testing.expectEqual(date.month, 2); + try testing.expectEqual(date.day, 29); + try testing.expectEqual(date.toOrdinal(), 9921); + + date = Date.fromOrdinal(737390); + try testing.expectEqual(date.year, 2019); + try testing.expectEqual(date.month, 11); + try testing.expectEqual(date.day, 27); + try testing.expectEqual(date.toOrdinal(), 737390); + + date = Date.fromOrdinal(719163); + try testing.expectEqual(date.year, 1970); + try testing.expectEqual(date.month, 1); + try testing.expectEqual(date.day, 1); + try testing.expectEqual(date.toOrdinal(), 719163); +} + +test "date-from-seconds" { + var seconds: f64 = 0; + var date = Date.fromSeconds(seconds); + try testing.expectEqual(date, try Date.create(1970, 1, 1)); + try testing.expectEqual(date.toSeconds(), seconds); + + seconds = -@as(f64, EPOCH - 1) * time.s_per_day; + date = Date.fromSeconds(seconds); + try testing.expectEqual(date, try Date.create(1, 1, 1)); + try testing.expectEqual(date.toSeconds(), seconds); + + seconds = @as(f64, MAX_ORDINAL - EPOCH) * time.s_per_day; + date = Date.fromSeconds(seconds); + try testing.expectEqual(date, try Date.create(9999, 12, 31)); + try testing.expectEqual(date.toSeconds(), seconds); + // + // + // const t = 63710928000.000; + // date = Date.fromSeconds(t); + // try testing.expectEqual(date.year, 2019); + // try testing.expectEqual(date.month, 12); + // try testing.expectEqual(date.day, 3); + // try testing.expectEqual(date.toSeconds(), t); + // + // Max check + // var max_date = try Date.create(9999, 12, 31); + // const tmax: f64 = @intToFloat(f64, MAX_ORDINAL-1) * time.s_per_day; + // date = Date.fromSeconds(tmax); + // try testing.expect(date.eql(max_date)); + // try testing.expectEqual(date.toSeconds(), tmax); +} + +test "date-day-of-year" { + var date = try Date.create(1970, 1, 1); + try testing.expect(date.dayOfYear() == 1); +} + +test "date-day-of-week" { + var date = try Date.create(2019, 11, 27); + try testing.expectEqual(date.weekday(), 2); + try testing.expectEqual(date.dayOfWeek(), .Wednesday); + try testing.expectEqualSlices(u8, date.monthName(), "November"); + try testing.expectEqualSlices(u8, date.weekdayName(), "Wednesday"); + try testing.expect(!date.isWeekend()); + + date = try Date.create(1776, 6, 4); + try testing.expectEqual(date.weekday(), 1); + try testing.expectEqual(date.dayOfWeek(), .Tuesday); + try testing.expectEqualSlices(u8, date.monthName(), "June"); + try testing.expectEqualSlices(u8, date.weekdayName(), "Tuesday"); + try testing.expect(!date.isWeekend()); + + date = try Date.create(2019, 12, 1); + try testing.expectEqualSlices(u8, date.monthName(), "December"); + try testing.expectEqualSlices(u8, date.weekdayName(), "Sunday"); + try testing.expect(date.isWeekend()); +} + +test "date-shift-days" { + var date = try Date.create(2019, 11, 27); + var d = date.shiftDays(-2); + try testing.expectEqual(d.day, 25); + try testing.expectEqualSlices(u8, d.weekdayName(), "Monday"); + + // Ahead one week + d = date.shiftDays(7); + try testing.expectEqualSlices(u8, d.weekdayName(), date.weekdayName()); + try testing.expectEqual(d.month, 12); + try testing.expectEqualSlices(u8, d.monthName(), "December"); + try testing.expectEqual(d.day, 4); + + d = date.shiftDays(0); + try testing.expect(date.eql(d)); +} + +test "date-shift-years" { + // Shift including a leap year + var date = try Date.create(2019, 11, 27); + var d = date.shiftYears(-4); + try testing.expect(d.eql(try Date.create(2015, 11, 27))); + + d = date.shiftYears(15); + try testing.expect(d.eql(try Date.create(2034, 11, 27))); + + // Shifting from leap day + var leap_day = try Date.create(2020, 2, 29); + d = leap_day.shiftYears(1); + try testing.expect(d.eql(try Date.create(2021, 2, 28))); + + // Before leap day + date = try Date.create(2020, 2, 2); + d = date.shiftYears(1); + try testing.expect(d.eql(try Date.create(2021, 2, 2))); + + // After leap day + date = try Date.create(2020, 3, 1); + d = date.shiftYears(1); + try testing.expect(d.eql(try Date.create(2021, 3, 1))); + + // From leap day to leap day + d = leap_day.shiftYears(4); + try testing.expect(d.eql(try Date.create(2024, 2, 29))); +} + +test "date-create" { + try testing.expectError(error.InvalidDate, Date.create(2019, 2, 29)); + + var date = Date.fromTimestamp(1574908586928); + try testing.expect(date.eql(try Date.create(2019, 11, 28))); +} + +test "date-copy" { + var d1 = try Date.create(2020, 1, 1); + var d2 = try d1.copy(); + try testing.expect(d1.eql(d2)); +} + +test "date-parse-iso" { + try testing.expectEqual(try Date.parseIso("2018-12-15"), try Date.create(2018, 12, 15)); + try testing.expectEqual(try Date.parseIso("2021-01-07"), try Date.create(2021, 1, 7)); + try testing.expectError(error.InvalidDate, Date.parseIso("2021-13-01")); + try testing.expectError(error.InvalidFormat, Date.parseIso("20-01-01")); + try testing.expectError(error.InvalidFormat, Date.parseIso("2000-1-1")); +} + +test "date-format-iso" { + var date_strs = [_][]const u8{ + "0959-02-05", + "2018-12-15", + }; + + for (date_strs) |date_str| { + var d = try Date.parseIso(date_str); + const parsed_date_str = try d.formatIso(std.testing.allocator); + defer std.testing.allocator.free(parsed_date_str); + try testing.expectEqualStrings(date_str, parsed_date_str); + } +} + +test "date-format-iso-buf" { + var date_strs = [_][]const u8{ + "0959-02-05", + "2018-12-15", + }; + + for (date_strs) |date_str| { + var d = try Date.parseIso(date_str); + var buf: [32]u8 = undefined; + try testing.expectEqualStrings(date_str, try d.formatIsoBuf(buf[0..])); + } +} + +test "date-write-iso" { + var date_strs = [_][]const u8{ + "0959-02-05", + "2018-12-15", + }; + + for (date_strs) |date_str| { + var buf: [32]u8 = undefined; + var stream = std.io.fixedBufferStream(buf[0..]); + var d = try Date.parseIso(date_str); + try d.writeIso(stream.writer()); + try testing.expectEqualStrings(date_str, stream.getWritten()); + } +} + +test "date-isocalendar" { + const today = try Date.create(2021, 8, 12); + try testing.expectEqual(today.isoCalendar(), ISOCalendar{ .year = 2021, .week = 32, .weekday = 4 }); + + // Some random dates and outputs generated with python + const dates = [15][]const u8{ + "2018-12-15", + "2019-01-19", + "2019-10-14", + "2020-09-26", + + // Border cases + "2020-12-27", + "2020-12-30", + "2020-12-31", + + "2021-01-01", + "2021-01-03", + "2021-01-04", + "2021-01-10", + + "2021-09-14", + "2022-09-12", + "2023-04-10", + "2024-01-16", + }; + + const expect = [15]ISOCalendar{ + ISOCalendar{ .year = 2018, .week = 50, .weekday = 6 }, + ISOCalendar{ .year = 2019, .week = 3, .weekday = 6 }, + ISOCalendar{ .year = 2019, .week = 42, .weekday = 1 }, + ISOCalendar{ .year = 2020, .week = 39, .weekday = 6 }, + + ISOCalendar{ .year = 2020, .week = 52, .weekday = 7 }, + ISOCalendar{ .year = 2020, .week = 53, .weekday = 3 }, + ISOCalendar{ .year = 2020, .week = 53, .weekday = 4 }, + + ISOCalendar{ .year = 2020, .week = 53, .weekday = 5 }, + ISOCalendar{ .year = 2020, .week = 53, .weekday = 7 }, + ISOCalendar{ .year = 2021, .week = 1, .weekday = 1 }, + ISOCalendar{ .year = 2021, .week = 1, .weekday = 7 }, + + ISOCalendar{ .year = 2021, .week = 37, .weekday = 2 }, + ISOCalendar{ .year = 2022, .week = 37, .weekday = 1 }, + ISOCalendar{ .year = 2023, .week = 15, .weekday = 1 }, + ISOCalendar{ .year = 2024, .week = 3, .weekday = 2 }, + }; + + for (dates, 0..) |d, i| { + const date = try Date.parseIso(d); + const cal = date.isoCalendar(); + try testing.expectEqual(cal, expect[i]); + try testing.expectEqual(date.weekOfYear(), expect[i].week); + } +} + +pub const Timezone = struct { + offset: i16, // In minutes + name: []const u8, + + // Auto register timezones + pub fn create(name: []const u8, offset: i16) Timezone { + const self = Timezone{ .offset = offset, .name = name }; + return self; + } + + pub fn offsetSeconds(self: Timezone) i32 { + return @as(i32, self.offset) * time.s_per_min; + } +}; + +pub const Time = struct { + hour: u8 = 0, // 0 to 23 + minute: u8 = 0, // 0 to 59 + second: u8 = 0, // 0 to 59 + nanosecond: u32 = 0, // 0 to 999999999 TODO: Should this be u20? + + // ------------------------------------------------------------------------ + // Constructors + // ------------------------------------------------------------------------ + pub fn now() Time { + return Time.fromTimestamp(time.milliTimestamp()); + } + + // Create a Time struct and validate that all fields are in range + pub fn create(hour: u32, minute: u32, second: u32, nanosecond: u32) !Time { + if (hour > 23 or minute > 59 or second > 59 or nanosecond > 999999999) { + return error.InvalidTime; + } + return Time{ + .hour = @intCast(u8, hour), + .minute = @intCast(u8, minute), + .second = @intCast(u8, second), + .nanosecond = nanosecond, + }; + } + + // Create a copy of the Time + pub fn copy(self: Time) !Time { + return Time.create(self.hour, self.minute, self.second, self.nanosecond); + } + + // Create Time from a UTC Timestamp in milliseconds + pub fn fromTimestamp(timestamp: i64) Time { + const remainder = @mod(timestamp, time.ms_per_day); + var t = @intCast(u64, math.absInt(remainder) catch unreachable); + // t is now only the time part of the day + const h = @intCast(u32, @divFloor(t, time.ms_per_hour)); + t -= h * time.ms_per_hour; + const m = @intCast(u32, @divFloor(t, time.ms_per_min)); + t -= m * time.ms_per_min; + const s = @intCast(u32, @divFloor(t, time.ms_per_s)); + t -= s * time.ms_per_s; + const ns = @intCast(u32, t * time.ns_per_ms); + return Time.create(h, m, s, ns) catch unreachable; + } + + // From seconds since the start of the day + pub fn fromSeconds(seconds: f64) Time { + assert(seconds >= 0); + // Convert to s and us + const r = math.modf(seconds); + var s = @floatToInt(u32, @mod(r.ipart, time.s_per_day)); // s + const h = @divFloor(s, time.s_per_hour); + s -= h * time.s_per_hour; + const m = @divFloor(s, time.s_per_min); + s -= m * time.s_per_min; + + // Rounding seems to only be accurate to within 100ns + // for normal timestamps + var frac = math.round(r.fpart * time.ns_per_s / 100) * 100; + if (frac >= time.ns_per_s) { + s += 1; + frac -= time.ns_per_s; + } else if (frac < 0) { + s -= 1; + frac += time.ns_per_s; + } + const ns = @floatToInt(u32, frac); + return Time.create(h, m, s, ns) catch unreachable; // If this fails it's a bug + } + + // Convert to a time in seconds relative to the UTC timezones + // including the nanosecond component + pub fn toSeconds(self: Time) f64 { + const s = @intToFloat(f64, self.totalSeconds()); + const ns = @intToFloat(f64, self.nanosecond) / time.ns_per_s; + return s + ns; + } + + // Convert to a timestamp in milliseconds from UTC + pub fn toTimestamp(self: Time) i64 { + const h = @intCast(i64, self.hour) * time.ms_per_hour; + const m = @intCast(i64, self.minute) * time.ms_per_min; + const s = @intCast(i64, self.second) * time.ms_per_s; + const ms = @intCast(i64, self.nanosecond / time.ns_per_ms); + return h + m + s + ms; + } + + // Total seconds from the start of day + pub fn totalSeconds(self: Time) i32 { + const h = @intCast(i32, self.hour) * time.s_per_hour; + const m = @intCast(i32, self.minute) * time.s_per_min; + const s = @intCast(i32, self.second); + return h + m + s; + } + + // ----------------------------------------------------------------------- + // Comparisons + // ----------------------------------------------------------------------- + pub fn eql(self: Time, other: Time) bool { + return self.cmp(other) == .eq; + } + + pub fn cmp(self: Time, other: Time) Order { + const t1 = self.totalSeconds(); + const t2 = other.totalSeconds(); + if (t1 > t2) return .gt; + if (t1 < t2) return .lt; + if (self.nanosecond > other.nanosecond) return .gt; + if (self.nanosecond < other.nanosecond) return .lt; + return .eq; + } + + pub fn gt(self: Time, other: Time) bool { + return self.cmp(other) == .gt; + } + + pub fn gte(self: Time, other: Time) bool { + const r = self.cmp(other); + return r == .eq or r == .gt; + } + + pub fn lt(self: Time, other: Time) bool { + return self.cmp(other) == .lt; + } + + pub fn lte(self: Time, other: Time) bool { + const r = self.cmp(other); + return r == .eq or r == .lt; + } + + // ----------------------------------------------------------------------- + // Methods + // ----------------------------------------------------------------------- + pub fn amOrPm(self: Time) []const u8 { + return if (self.hour > 12) return "PM" else "AM"; + } + + // ----------------------------------------------------------------------- + // Formatting Methods + // ----------------------------------------------------------------------- + const ISO_HM_FORMAT = "T{d:0>2}:{d:0>2}"; + const ISO_HMS_FORMAT = "T{d:0>2}:{d:0>2}:{d:0>2}"; + + pub fn writeIsoHM(self: Time, writer: anytype) !void { + try std.fmt.format(writer, ISO_HM_FORMAT, .{ self.hour, self.minute }); + } + + pub fn writeIsoHMS(self: Time, writer: anytype) !void { + try std.fmt.format(writer, ISO_HMS_FORMAT, .{ self.hour, self.minute, self.second }); + } +}; + +test "time-create" { + const t = Time.fromTimestamp(1574908586928); + try testing.expect(t.hour == 2); + try testing.expect(t.minute == 36); + try testing.expect(t.second == 26); + try testing.expect(t.nanosecond == 928000000); + + try testing.expectError(error.InvalidTime, Time.create(25, 1, 1, 0)); + try testing.expectError(error.InvalidTime, Time.create(1, 60, 1, 0)); + try testing.expectError(error.InvalidTime, Time.create(12, 30, 281, 0)); + try testing.expectError(error.InvalidTime, Time.create(12, 30, 28, 1000000000)); +} + +test "time-now" { + _ = Time.now(); +} + +test "time-from-seconds" { + var seconds: f64 = 15.12; + var t = Time.fromSeconds(seconds); + try testing.expect(t.hour == 0); + try testing.expect(t.minute == 0); + try testing.expect(t.second == 15); + try testing.expect(t.nanosecond == 120000000); + try testing.expect(t.toSeconds() == seconds); + + seconds = 315.12; // + 5 min + t = Time.fromSeconds(seconds); + try testing.expect(t.hour == 0); + try testing.expect(t.minute == 5); + try testing.expect(t.second == 15); + try testing.expect(t.nanosecond == 120000000); + try testing.expect(t.toSeconds() == seconds); + + seconds = 36000 + 315.12; // + 10 hr + t = Time.fromSeconds(seconds); + try testing.expect(t.hour == 10); + try testing.expect(t.minute == 5); + try testing.expect(t.second == 15); + try testing.expect(t.nanosecond == 120000000); + try testing.expect(t.toSeconds() == seconds); + + seconds = 108000 + 315.12; // + 30 hr + t = Time.fromSeconds(seconds); + try testing.expect(t.hour == 6); + try testing.expect(t.minute == 5); + try testing.expect(t.second == 15); + try testing.expect(t.nanosecond == 120000000); + try testing.expectEqual(t.totalSeconds(), 6 * 3600 + 315); + //testing.expectAlmostEqual(t.toSeconds(), seconds-time.s_per_day); +} + +test "time-copy" { + var t1 = try Time.create(8, 30, 0, 0); + var t2 = try t1.copy(); + try testing.expect(t1.eql(t2)); +} + +test "time-compare" { + var t1 = try Time.create(8, 30, 0, 0); + var t2 = try Time.create(9, 30, 0, 0); + var t3 = try Time.create(8, 0, 0, 0); + var t4 = try Time.create(9, 30, 17, 0); + + try testing.expect(t1.lt(t2)); + try testing.expect(t1.gt(t3)); + try testing.expect(t2.lt(t4)); + try testing.expect(t3.lt(t4)); +} + +test "time-write-iso-hm" { + const t = Time.fromTimestamp(1574908586928); + + var buf: [6]u8 = undefined; + var fbs = std.io.fixedBufferStream(buf[0..]); + + try t.writeIsoHM(fbs.writer()); + + try testing.expectEqualSlices(u8, "T02:36", fbs.getWritten()); +} + +test "time-write-iso-hms" { + const t = Time.fromTimestamp(1574908586928); + + var buf: [9]u8 = undefined; + var fbs = std.io.fixedBufferStream(buf[0..]); + + try t.writeIsoHMS(fbs.writer()); + + try testing.expectEqualSlices(u8, "T02:36:26", fbs.getWritten()); +} + +pub const Datetime = struct { + date: Date, + time: Time, + zone: *const Timezone, + + // An absolute or relative delta + // if years is defined a date is date + // TODO: Validate years before allowing it to be created + pub const Delta = struct { + years: i16 = 0, + days: i32 = 0, + seconds: i64 = 0, + nanoseconds: i32 = 0, + relative_to: ?Datetime = null, + + pub fn sub(self: Delta, other: Delta) Delta { + return Delta{ + .years = self.years - other.years, + .days = self.days - other.days, + .seconds = self.seconds - other.seconds, + .nanoseconds = self.nanoseconds - other.nanoseconds, + .relative_to = self.relative_to, + }; + } + + pub fn add(self: Delta, other: Delta) Delta { + return Delta{ + .years = self.years + other.years, + .days = self.days + other.days, + .seconds = self.seconds + other.seconds, + .nanoseconds = self.nanoseconds + other.nanoseconds, + .relative_to = self.relative_to, + }; + } + + // Total seconds in the duration ignoring the nanoseconds fraction + pub fn totalSeconds(self: Delta) i64 { + // Calculate the total number of days we're shifting + var days = self.days; + if (self.relative_to) |dt| { + if (self.years != 0) { + const a = daysBeforeYear(dt.date.year); + // Must always subtract greater of the two + if (self.years > 0) { + const y = @intCast(u32, self.years); + const b = daysBeforeYear(dt.date.year + y); + days += @intCast(i32, b - a); + } else { + const y = @intCast(u32, -self.years); + assert(y < dt.date.year); // Does not work below year 1 + const b = daysBeforeYear(dt.date.year - y); + days -= @intCast(i32, a - b); + } + } + } else { + // Cannot use years without a relative to date + // otherwise any leap days will screw up results + assert(self.years == 0); + } + var s = self.seconds; + var ns = self.nanoseconds; + if (ns >= time.ns_per_s) { + const ds = @divFloor(ns, time.ns_per_s); + ns -= ds * time.ns_per_s; + s += ds; + } else if (ns <= -time.ns_per_s) { + const ds = @divFloor(ns, -time.ns_per_s); + ns += ds * time.us_per_s; + s -= ds; + } + return (days * time.s_per_day + s); + } + }; + + // ------------------------------------------------------------------------ + // Constructors + // ------------------------------------------------------------------------ + pub fn now() Datetime { + return Datetime.fromTimestamp(time.milliTimestamp()); + } + + pub fn create(year: u32, month: u32, day: u32, hour: u32, minute: u32, second: u32, nanosecond: u32, zone: ?*const Timezone) !Datetime { + return Datetime{ + .date = try Date.create(year, month, day), + .time = try Time.create(hour, minute, second, nanosecond), + .zone = zone orelse &timezones.UTC, + }; + } + + // Return a copy + pub fn copy(self: Datetime) !Datetime { + return Datetime{ + .date = try self.date.copy(), + .time = try self.time.copy(), + .zone = self.zone, + }; + } + + pub fn fromDate(year: u16, month: u8, day: u8) !Datetime { + return Datetime{ + .date = try Date.create(year, month, day), + .time = try Time.create(0, 0, 0, 0), + .zone = &timezones.UTC, + }; + } + + // From seconds since 1 Jan 1970 + pub fn fromSeconds(seconds: f64) Datetime { + return Datetime{ + .date = Date.fromSeconds(seconds), + .time = Time.fromSeconds(seconds), + .zone = &timezones.UTC, + }; + } + + // Seconds since 1 Jan 0001 including nanoseconds + pub fn toSeconds(self: Datetime) f64 { + return self.date.toSeconds() + self.time.toSeconds(); + } + + // From POSIX timestamp in milliseconds relative to 1 Jan 1970 + pub fn fromTimestamp(timestamp: i64) Datetime { + const t = @divFloor(timestamp, time.ms_per_day); + const d = @intCast(u64, math.absInt(t) catch unreachable); + const days = if (timestamp >= 0) d + EPOCH else EPOCH - d; + assert(days >= 0 and days <= MAX_ORDINAL); + return Datetime{ + .date = Date.fromOrdinal(@intCast(u32, days)), + .time = Time.fromTimestamp(timestamp - @intCast(i64, d) * time.ns_per_day), + .zone = &timezones.UTC, + }; + } + + // From a file modified time in ns + pub fn fromModifiedTime(mtime: i128) Datetime { + const ts = @intCast(i64, @divFloor(mtime, time.ns_per_ms)); + return Datetime.fromTimestamp(ts); + } + + // To a UTC POSIX timestamp in milliseconds relative to 1 Jan 1970 + pub fn toTimestamp(self: Datetime) i128 { + const ds = self.date.toTimestamp(); + const ts = self.time.toTimestamp(); + const zs = self.zone.offsetSeconds() * time.ms_per_s; + return ds + ts - zs; + } + + // ----------------------------------------------------------------------- + // Comparisons + // ----------------------------------------------------------------------- + pub fn eql(self: Datetime, other: Datetime) bool { + return self.cmp(other) == .eq; + } + + pub fn cmpSameTimezone(self: Datetime, other: Datetime) Order { + assert(self.zone.offset == other.zone.offset); + const r = self.date.cmp(other.date); + if (r != .eq) return r; + return self.time.cmp(other.time); + } + + pub fn cmp(self: Datetime, other: Datetime) Order { + if (self.zone.offset == other.zone.offset) { + return self.cmpSameTimezone(other); + } + // Shift both to utc + const a = self.shiftTimezone(&timezones.UTC); + const b = other.shiftTimezone(&timezones.UTC); + return a.cmpSameTimezone(b); + } + + pub fn gt(self: Datetime, other: Datetime) bool { + return self.cmp(other) == .gt; + } + + pub fn gte(self: Datetime, other: Datetime) bool { + const r = self.cmp(other); + return r == .eq or r == .gt; + } + + pub fn lt(self: Datetime, other: Datetime) bool { + return self.cmp(other) == .lt; + } + + pub fn lte(self: Datetime, other: Datetime) bool { + const r = self.cmp(other); + return r == .eq or r == .lt; + } + + // ----------------------------------------------------------------------- + // Methods + // ----------------------------------------------------------------------- + + // Return a Datetime.Delta relative to this date + pub fn sub(self: Datetime, other: Datetime) Delta { + var days = @intCast(i32, self.date.toOrdinal()) - @intCast(i32, other.date.toOrdinal()); + const offset = (self.zone.offset - other.zone.offset) * time.s_per_min; + var seconds = (self.time.totalSeconds() - other.time.totalSeconds()) + offset; + var ns = @intCast(i32, self.time.nanosecond) - @intCast(i32, other.time.nanosecond); + while (seconds > 0 and ns < 0) { + seconds -= 1; + ns += time.ns_per_s; + } + while (days > 0 and seconds < 0) { + days -= 1; + seconds += time.s_per_day; + } + return Delta{ .days = days, .seconds = seconds, .nanoseconds = ns }; + } + + // Create a Datetime shifted by the given number of years + pub fn shiftYears(self: Datetime, years: i16) Datetime { + return self.shift(Delta{ .years = years }); + } + + // Create a Datetime shifted by the given number of days + pub fn shiftDays(self: Datetime, days: i32) Datetime { + return self.shift(Delta{ .days = days }); + } + + // Create a Datetime shifted by the given number of hours + pub fn shiftHours(self: Datetime, hours: i32) Datetime { + return self.shift(Delta{ .seconds = hours * time.s_per_hour }); + } + + // Create a Datetime shifted by the given number of minutes + pub fn shiftMinutes(self: Datetime, minutes: i32) Datetime { + return self.shift(Delta{ .seconds = minutes * time.s_per_min }); + } + + // Convert to the given timeszone + pub fn shiftTimezone(self: Datetime, zone: *const Timezone) Datetime { + var dt = + if (self.zone.offset == zone.offset) + (self.copy() catch unreachable) + else + self.shiftMinutes(zone.offset - self.zone.offset); + dt.zone = zone; + return dt; + } + + // Create a Datetime shifted by the given number of seconds + pub fn shiftSeconds(self: Datetime, seconds: i64) Datetime { + return self.shift(Delta{ .seconds = seconds }); + } + + // Create a Datetime shifted by the given Delta + pub fn shift(self: Datetime, delta: Delta) Datetime { + var days = delta.days; + var s = delta.seconds + self.time.totalSeconds(); + + // Rollover ns to s + var ns = delta.nanoseconds + @intCast(i32, self.time.nanosecond); + if (ns >= time.ns_per_s) { + s += 1; + ns -= time.ns_per_s; + } else if (ns < -time.ns_per_s) { + s -= 1; + ns += time.ns_per_s; + } + assert(ns >= 0 and ns < time.ns_per_s); + const nanosecond = @intCast(u32, ns); + + // Rollover s to days + if (s >= time.s_per_day) { + const d = @divFloor(s, time.s_per_day); + days += @intCast(i32, d); + s -= d * time.s_per_day; + } else if (s < 0) { + if (s < -time.s_per_day) { // Wrap multiple + const d = @divFloor(s, -time.s_per_day); + days -= @intCast(i32, d); + s += d * time.s_per_day; + } + days -= 1; + s = time.s_per_day + s; + } + assert(s >= 0 and s < time.s_per_day); + + var second = @intCast(u32, s); + const hour = @divFloor(second, time.s_per_hour); + second -= hour * time.s_per_hour; + const minute = @divFloor(second, time.s_per_min); + second -= minute * time.s_per_min; + + return Datetime{ + .date = self.date.shift(Date.Delta{ .years = delta.years, .days = days }), + .time = Time.create(hour, minute, second, nanosecond) catch unreachable, // Error here would mean a bug + .zone = self.zone, + }; + } + + // ------------------------------------------------------------------------ + // Formatting methods + // ------------------------------------------------------------------------ + + // Formats a timestamp in the format used by HTTP. + // eg "Tue, 15 Nov 1994 08:12:31 GMT" + pub fn formatHttp(self: Datetime, allocator: Allocator) ![]const u8 { + return try std.fmt.allocPrint(allocator, "{s}, {d} {s} {d} {d:0>2}:{d:0>2}:{d:0>2} {s}", .{ + self.date.weekdayName()[0..3], + self.date.day, + self.date.monthName()[0..3], + self.date.year, + self.time.hour, + self.time.minute, + self.time.second, + self.zone.name, // TODO: Should be GMT + }); + } + + pub fn formatHttpBuf(self: Datetime, buf: []u8) ![]const u8 { + return try std.fmt.bufPrint(buf, "{s}, {d} {s} {d} {d:0>2}:{d:0>2}:{d:0>2} {s}", .{ + self.date.weekdayName()[0..3], + self.date.day, + self.date.monthName()[0..3], + self.date.year, + self.time.hour, + self.time.minute, + self.time.second, + self.zone.name, // TODO: Should be GMT + }); + } + + // Formats a timestamp in the format used by HTTP. + // eg "Tue, 15 Nov 1994 08:12:31 GMT" + pub fn formatHttpFromTimestamp(buf: []u8, timestamp: i64) ![]const u8 { + return Datetime.fromTimestamp(timestamp).formatHttpBuf(buf); + } + + // From time in nanoseconds + pub fn formatHttpFromModifiedDate(buf: []u8, mtime: i128) ![]const u8 { + const ts = @intCast(i64, @divFloor(mtime, time.ns_per_ms)); + return Datetime.formatHttpFromTimestamp(buf, ts); + } + + /// Format datetime to ISO8601 format + /// e.g. "2023-06-10T14:06:40.015006+08:00" + pub fn formatISO8601(self: Datetime, allocator: Allocator, with_micro: bool) ![]const u8 { + var sign: u8 = '+'; + if (self.zone.offset < 0) { + sign = '-'; + } + const offset = std.math.absCast(self.zone.offset); + + var micro_part_len: u3 = 0; + var micro_part: [7]u8 = undefined; + if (with_micro) { + _ = try std.fmt.bufPrint(µ_part, ".{:0>6}", .{self.time.nanosecond / 1000}); + micro_part_len = 7; + } + + return try std.fmt.allocPrint( + allocator, + "{d:0>4}-{d:0>2}-{d:0>2}T{d:0>2}:{d:0>2}:{d:0>2}{s}{c}{d:0>2}:{d:0>2}", + .{ + self.date.year, + self.date.month, + self.date.day, + self.time.hour, + self.time.minute, + self.time.second, + micro_part[0..micro_part_len], + sign, + @divFloor(offset, 60), + @mod(offset, 60), + }, + ); + } + + pub fn formatISO8601Buf(self: Datetime, buf: []u8, with_micro: bool) ![]const u8 { + var sign: u8 = '+'; + if (self.zone.offset < 0) { + sign = '-'; + } + const offset = std.math.absCast(self.zone.offset); + + var micro_part_len: usize = 0; + var micro_part: [7]u8 = undefined; + if (with_micro) { + _ = try std.fmt.bufPrint(µ_part, ".{:0>6}", .{self.time.nanosecond / 1000}); + micro_part_len = 7; + } + + return try std.fmt.bufPrint( + buf, + "{d:0>4}-{d:0>2}-{d:0>2}T{d:0>2}:{d:0>2}:{d:0>2}{s}{c}{d:0>2}:{d:0>2}", + .{ + self.date.year, + self.date.month, + self.date.day, + self.time.hour, + self.time.minute, + self.time.second, + micro_part[0..micro_part_len], + sign, + @divFloor(offset, 60), + @mod(offset, 60), + }, + ); + } + + // ------------------------------------------------------------------------ + // Parsing methods + // ------------------------------------------------------------------------ + + // Parse a HTTP If-Modified-Since header + // in the format ", :: GMT" + // eg, "Wed, 21 Oct 2015 07:28:00 GMT" + pub fn parseModifiedSince(ims: []const u8) !Datetime { + const value = std.mem.trim(u8, ims, " "); + if (value.len < 29) return error.InvalidFormat; + const day = std.fmt.parseInt(u8, value[5..7], 10) catch return error.InvalidFormat; + const month = @enumToInt(try Month.parseAbbr(value[8..11])); + const year = std.fmt.parseInt(u16, value[12..16], 10) catch return error.InvalidFormat; + const hour = std.fmt.parseInt(u8, value[17..19], 10) catch return error.InvalidFormat; + const minute = std.fmt.parseInt(u8, value[20..22], 10) catch return error.InvalidFormat; + const second = std.fmt.parseInt(u8, value[23..25], 10) catch return error.InvalidFormat; + return Datetime.create(year, month, day, hour, minute, second, 0, &timezones.GMT); + } +}; + +test "datetime-now" { + _ = Datetime.now(); +} + +test "datetime-create-timestamp" { + //var t = Datetime.now(); + const ts = 1574908586928; + const t = Datetime.fromTimestamp(ts); + try testing.expect(t.date.eql(try Date.create(2019, 11, 28))); + try testing.expect(t.time.eql(try Time.create(2, 36, 26, 928000000))); + try testing.expectEqualSlices(u8, t.zone.name, "UTC"); + try testing.expectEqual(t.toTimestamp(), ts); +} + +test "datetime-from-seconds" { + // datetime.utcfromtimestamp(1592417521.9326444) + // datetime.datetime(2020, 6, 17, 18, 12, 1, 932644) + const ts: f64 = 1592417521.9326444; + const t = Datetime.fromSeconds(ts); + try testing.expect(t.date.year == 2020); + try testing.expectEqual(t.date, try Date.create(2020, 6, 17)); + try testing.expectEqual(t.time, try Time.create(18, 12, 1, 932644400)); + try testing.expectEqual(t.toSeconds(), ts); +} + +test "datetime-shift-timezones" { + const ts = 1574908586928; + const utc = Datetime.fromTimestamp(ts); + var t = utc.shiftTimezone(&timezones.America.New_York); + + try testing.expect(t.date.eql(try Date.create(2019, 11, 27))); + try testing.expectEqual(t.time.hour, 21); + try testing.expectEqual(t.time.minute, 36); + try testing.expectEqual(t.time.second, 26); + try testing.expectEqual(t.time.nanosecond, 928000000); + try testing.expectEqualSlices(u8, t.zone.name, "America/New_York"); + try testing.expectEqual(t.toTimestamp(), ts); + + // Shifting to same timezone has no effect + const same = t.shiftTimezone(&timezones.America.New_York); + try testing.expectEqual(t, same); + + // Shift back works + const original = t.shiftTimezone(&timezones.UTC); + //std.log.warn("\nutc={}\n", .{utc}); + //std.log.warn("original={}\n", .{original}); + try testing.expect(utc.date.eql(original.date)); + try testing.expect(utc.time.eql(original.time)); + try testing.expect(utc.eql(original)); +} + +test "datetime-shift" { + var dt = try Datetime.create(2019, 12, 2, 11, 51, 13, 466545, null); + + try testing.expect(dt.shiftYears(0).eql(dt)); + try testing.expect(dt.shiftDays(0).eql(dt)); + try testing.expect(dt.shiftHours(0).eql(dt)); + + var t = dt.shiftDays(7); + try testing.expect(t.date.eql(try Date.create(2019, 12, 9))); + try testing.expect(t.time.eql(dt.time)); + + t = dt.shiftDays(-3); + try testing.expect(t.date.eql(try Date.create(2019, 11, 29))); + try testing.expect(t.time.eql(dt.time)); + + t = dt.shiftHours(18); + try testing.expect(t.date.eql(try Date.create(2019, 12, 3))); + try testing.expect(t.time.eql(try Time.create(5, 51, 13, 466545))); + + t = dt.shiftHours(-36); + try testing.expect(t.date.eql(try Date.create(2019, 11, 30))); + try testing.expect(t.time.eql(try Time.create(23, 51, 13, 466545))); + + t = dt.shiftYears(1); + try testing.expect(t.date.eql(try Date.create(2020, 12, 2))); + try testing.expect(t.time.eql(dt.time)); + + t = dt.shiftYears(-3); + try testing.expect(t.date.eql(try Date.create(2016, 12, 2))); + try testing.expect(t.time.eql(dt.time)); +} + +test "datetime-shift-seconds" { + // Issue 1 + const midnight_utc = try Datetime.create(2020, 12, 17, 0, 0, 0, 0, null); + const midnight_copenhagen = try Datetime.create(2020, 12, 17, 1, 0, 0, 0, &timezones.Europe.Copenhagen); + try testing.expect(midnight_utc.eql(midnight_copenhagen)); + + // Check rollover issues + var hour: u8 = 0; + while (hour < 24) : (hour += 1) { + var minute: u8 = 0; + while (minute < 60) : (minute += 1) { + var sec: u8 = 0; + while (sec < 60) : (sec += 1) { + const dt_utc = try Datetime.create(2020, 12, 17, hour, minute, sec, 0, null); + const dt_cop = dt_utc.shiftTimezone(&timezones.Europe.Copenhagen); + const dt_nyc = dt_utc.shiftTimezone(&timezones.America.New_York); + try testing.expect(dt_utc.eql(dt_cop)); + try testing.expect(dt_utc.eql(dt_nyc)); + try testing.expect(dt_nyc.eql(dt_cop)); + } + } + } +} + +test "datetime-compare" { + var dt1 = try Datetime.create(2019, 12, 2, 11, 51, 13, 466545, null); + var dt2 = try Datetime.fromDate(2016, 12, 2); + try testing.expect(dt2.lt(dt1)); + + var dt3 = Datetime.now(); + try testing.expect(dt3.gt(dt2)); + + var dt4 = try dt3.copy(); + try testing.expect(dt3.eql(dt4)); + + var dt5 = dt1.shiftTimezone(&timezones.America.Louisville); + try testing.expect(dt5.eql(dt1)); +} + +test "datetime-subtract" { + var a = try Datetime.create(2019, 12, 2, 11, 51, 13, 466545, null); + var b = try Datetime.create(2019, 12, 5, 11, 51, 13, 466545, null); + var delta = a.sub(b); + try testing.expectEqual(delta.days, -3); + try testing.expectEqual(delta.totalSeconds(), -3 * time.s_per_day); + delta = b.sub(a); + try testing.expectEqual(delta.days, 3); + try testing.expectEqual(delta.totalSeconds(), 3 * time.s_per_day); + + b = try Datetime.create(2019, 12, 2, 11, 0, 0, 466545, null); + delta = a.sub(b); + try testing.expectEqual(delta.totalSeconds(), 13 + 51 * time.s_per_min); +} + +test "datetime-subtract-delta" { + var now = Datetime.fromSeconds(1686183930); + var future = Datetime.fromSeconds(1686268800); + var delta = future.sub(now); + try testing.expect(delta.days == 0); + try testing.expect(delta.seconds == 84870); + + delta = now.sub(future); + try testing.expect(delta.days == -1); + try testing.expect(delta.seconds == 1530); + + now = Datetime.fromSeconds(1686183930); + future = Datetime.fromSeconds(1686270330); + delta = future.sub(now); + try testing.expect(delta.days == 1); + try testing.expect(delta.seconds == 0); +} + +test "datetime-parse-modified-since" { + const str = " Wed, 21 Oct 2015 07:28:00 GMT "; + try testing.expectEqual(try Datetime.parseModifiedSince(str), try Datetime.create(2015, 10, 21, 7, 28, 0, 0, &timezones.GMT)); + + try testing.expectError(error.InvalidFormat, Datetime.parseModifiedSince("21/10/2015")); +} + +test "file-modified-date" { + var f = try std.fs.cwd().openFile("README.md", .{}); + var stat = try f.stat(); + var buf: [32]u8 = undefined; + var str = try Datetime.formatHttpFromModifiedDate(&buf, stat.mtime); + std.log.warn("Modtime: {s}\n", .{str}); +} + +test "readme-example" { + const allocator = std.testing.allocator; + const date = try Date.create(2019, 12, 25); + const next_year = date.shiftDays(7); + assert(next_year.year == 2020); + assert(next_year.month == 1); + assert(next_year.day == 1); + + // In UTC + const now = Datetime.now(); + const now_str = try now.formatHttp(allocator); + defer allocator.free(now_str); + std.log.warn("The time is now: {s}\n", .{now_str}); + // The time is now: Fri, 20 Dec 2019 22:03:02 UTC + +} + +test "datetime-format-ISO8601" { + const allocator = std.testing.allocator; + + var dt = try Datetime.create(2023, 6, 10, 9, 12, 52, 49612000, null); + var dt_str = try dt.formatISO8601(allocator, false); + try testing.expectEqualStrings("2023-06-10T09:12:52+00:00", dt_str); + allocator.free(dt_str); + + // test positive tz + dt = try Datetime.create(2023, 6, 10, 18, 12, 52, 49612000, &timezones.Japan); + dt_str = try dt.formatISO8601(allocator, false); + try testing.expectEqualStrings("2023-06-10T18:12:52+09:00", dt_str); + allocator.free(dt_str); + + // test negative tz + dt = try Datetime.create(2023, 6, 10, 6, 12, 52, 49612000, &timezones.Atlantic.Stanley); + dt_str = try dt.formatISO8601(allocator, false); + try testing.expectEqualStrings("2023-06-10T06:12:52-03:00", dt_str); + allocator.free(dt_str); + + // test tz offset div and mod + dt = try Datetime.create(2023, 6, 10, 22, 57, 52, 49612000, &timezones.Pacific.Chatham); + dt_str = try dt.formatISO8601(allocator, false); + try testing.expectEqualStrings("2023-06-10T22:57:52+12:45", dt_str); + allocator.free(dt_str); + + // test microseconds + dt = try Datetime.create(2023, 6, 10, 5, 57, 52, 49612000, &timezones.America.Aruba); + dt_str = try dt.formatISO8601(allocator, true); + try testing.expectEqualStrings("2023-06-10T05:57:52.049612-04:00", dt_str); + allocator.free(dt_str); + + // test format buf + var buf: [64]u8 = undefined; + dt = try Datetime.create(2023, 6, 10, 14, 6, 40, 15006000, &timezones.Hongkong); + dt_str = try dt.formatISO8601Buf(&buf, true); + try testing.expectEqualStrings("2023-06-10T14:06:40.015006+08:00", dt_str); +} diff --git a/src/.deps/timezones.zig b/src/.deps/timezones.zig new file mode 100644 index 0000000..f028977 --- /dev/null +++ b/src/.deps/timezones.zig @@ -0,0 +1,677 @@ +// -------------------------------------------------------------------------- // +// Copyright (c) 2019, Jairus Martin. // +// Distributed under the terms of the MIT License. // +// The full license is in the file LICENSE, distributed with this software. // +// -------------------------------------------------------------------------- // + +const std = @import("std"); + +const Timezone = @import("datetime.zig").Timezone; +const create = Timezone.create; + +// Timezones +pub const Africa = struct { + pub const Abidjan = create("Africa/Abidjan", 0); + pub const Accra = create("Africa/Accra", 0); + pub const Addis_Ababa = create("Africa/Addis_Ababa", 180); + pub const Algiers = create("Africa/Algiers", 60); + pub const Asmara = create("Africa/Asmara", 180); + pub const Bamako = create("Africa/Bamako", 0); + pub const Bangui = create("Africa/Bangui", 60); + pub const Banjul = create("Africa/Banjul", 0); + pub const Bissau = create("Africa/Bissau", 0); + pub const Blantyre = create("Africa/Blantyre", 120); + pub const Brazzaville = create("Africa/Brazzaville", 60); + pub const Bujumbura = create("Africa/Bujumbura", 120); + pub const Cairo = create("Africa/Cairo", 120); + pub const Casablanca = create("Africa/Casablanca", 60); + pub const Ceuta = create("Africa/Ceuta", 60); + pub const Conakry = create("Africa/Conakry", 0); + pub const Dakar = create("Africa/Dakar", 0); + pub const Dar_es_Salaam = create("Africa/Dar_es_Salaam", 180); + pub const Djibouti = create("Africa/Djibouti", 180); + pub const Douala = create("Africa/Douala", 60); + pub const El_Aaiun = create("Africa/El_Aaiun", 0); + pub const Freetown = create("Africa/Freetown", 0); + pub const Gaborone = create("Africa/Gaborone", 120); + pub const Harare = create("Africa/Harare", 120); + pub const Johannesburg = create("Africa/Johannesburg", 120); + pub const Juba = create("Africa/Juba", 180); + pub const Kampala = create("Africa/Kampala", 180); + pub const Khartoum = create("Africa/Khartoum", 120); + pub const Kigali = create("Africa/Kigali", 120); + pub const Kinshasa = create("Africa/Kinshasa", 60); + pub const Lagos = create("Africa/Lagos", 60); + pub const Libreville = create("Africa/Libreville", 60); + pub const Lome = create("Africa/Lome", 0); + pub const Luanda = create("Africa/Luanda", 60); + pub const Lubumbashi = create("Africa/Lubumbashi", 120); + pub const Lusaka = create("Africa/Lusaka", 120); + pub const Malabo = create("Africa/Malabo", 60); + pub const Maputo = create("Africa/Maputo", 120); + pub const Maseru = create("Africa/Maseru", 120); + pub const Mbabane = create("Africa/Mbabane", 120); + pub const Mogadishu = create("Africa/Mogadishu", 180); + pub const Monrovia = create("Africa/Monrovia", 0); + pub const Nairobi = create("Africa/Nairobi", 180); + pub const Ndjamena = create("Africa/Ndjamena", 60); + pub const Niamey = create("Africa/Niamey", 60); + pub const Nouakchott = create("Africa/Nouakchott", 0); + pub const Ouagadougou = create("Africa/Ouagadougou", 0); + pub const Porto_Novo = create("Africa/Porto-Novo", 60); + pub const Sao_Tome = create("Africa/Sao_Tome", 0); + pub const Timbuktu = create("Africa/Timbuktu", 0); + pub const Tripoli = create("Africa/Tripoli", 120); + pub const Tunis = create("Africa/Tunis", 60); + pub const Windhoek = create("Africa/Windhoek", 120); +}; + +pub const America = struct { + pub const Adak = create("America/Adak", -600); + pub const Anchorage = create("America/Anchorage", -540); + pub const Anguilla = create("America/Anguilla", -240); + pub const Antigua = create("America/Antigua", -240); + pub const Araguaina = create("America/Araguaina", -180); + pub const Argentina = struct { + pub const Buenos_Aires = create("America/Argentina/Buenos_Aires", -180); + pub const Catamarca = create("America/Argentina/Catamarca", -180); + pub const ComodRivadavia = create("America/Argentina/ComodRivadavia", -180); + pub const Cordoba = create("America/Argentina/Cordoba", -180); + pub const Jujuy = create("America/Argentina/Jujuy", -180); + pub const La_Rioja = create("America/Argentina/La_Rioja", -180); + pub const Mendoza = create("America/Argentina/Mendoza", -180); + pub const Rio_Gallegos = create("America/Argentina/Rio_Gallegos", -180); + pub const Salta = create("America/Argentina/Salta", -180); + pub const San_Juan = create("America/Argentina/San_Juan", -180); + pub const San_Luis = create("America/Argentina/San_Luis", -180); + pub const Tucuman = create("America/Argentina/Tucuman", -180); + pub const Ushuaia = create("America/Argentina/Ushuaia", -180); + }; + pub const Aruba = create("America/Aruba", -240); + pub const Asuncion = create("America/Asuncion", -240); + pub const Atikokan = create("America/Atikokan", -300); + pub const Atka = create("America/Atka", -600); + pub const Bahia = create("America/Bahia", -180); + pub const Bahia_Banderas = create("America/Bahia_Banderas", -360); + pub const Barbados = create("America/Barbados", -240); + pub const Belem = create("America/Belem", -180); + pub const Belize = create("America/Belize", -360); + pub const Blanc_Sablon = create("America/Blanc-Sablon", -240); + pub const Boa_Vista = create("America/Boa_Vista", -240); + pub const Bogota = create("America/Bogota", -300); + pub const Boise = create("America/Boise", -420); + pub const Buenos_Aires = create("America/Buenos_Aires", -180); + pub const Cambridge_Bay = create("America/Cambridge_Bay", -420); + pub const Campo_Grande = create("America/Campo_Grande", -240); + pub const Cancun = create("America/Cancun", -300); + pub const Caracas = create("America/Caracas", -240); + pub const Catamarca = create("America/Catamarca", -180); + pub const Cayenne = create("America/Cayenne", -180); + pub const Cayman = create("America/Cayman", -300); + pub const Chicago = create("America/Chicago", -360); + pub const Chihuahua = create("America/Chihuahua", -420); + pub const Coral_Harbour = create("America/Coral_Harbour", -300); + pub const Cordoba = create("America/Cordoba", -180); + pub const Costa_Rica = create("America/Costa_Rica", -360); + pub const Creston = create("America/Creston", -420); + pub const Cuiaba = create("America/Cuiaba", -240); + pub const Curacao = create("America/Curacao", -240); + pub const Danmarkshavn = create("America/Danmarkshavn", 0); + pub const Dawson = create("America/Dawson", -480); + pub const Dawson_Creek = create("America/Dawson_Creek", -420); + pub const Denver = create("America/Denver", -420); + pub const Detroit = create("America/Detroit", -300); + pub const Dominica = create("America/Dominica", -240); + pub const Edmonton = create("America/Edmonton", -420); + pub const Eirunepe = create("America/Eirunepe", -300); + pub const El_Salvador = create("America/El_Salvador", -360); + pub const Ensenada = create("America/Ensenada", -480); + pub const Fort_Nelson = create("America/Fort_Nelson", -420); + pub const Fort_Wayne = create("America/Fort_Wayne", -300); + pub const Fortaleza = create("America/Fortaleza", -180); + pub const Glace_Bay = create("America/Glace_Bay", -240); + pub const Godthab = create("America/Godthab", -180); + pub const Goose_Bay = create("America/Goose_Bay", -240); + pub const Grand_Turk = create("America/Grand_Turk", -300); + pub const Grenada = create("America/Grenada", -240); + pub const Guadeloupe = create("America/Guadeloupe", -240); + pub const Guatemala = create("America/Guatemala", -360); + pub const Guayaquil = create("America/Guayaquil", -300); + pub const Guyana = create("America/Guyana", -240); + pub const Halifax = create("America/Halifax", -240); + pub const Havana = create("America/Havana", -300); + pub const Hermosillo = create("America/Hermosillo", -420); + pub const Indiana = struct { + // FIXME: Name conflict + pub const Indianapolis_ = create("America/Indiana/Indianapolis", -300); + pub const Knox = create("America/Indiana/Knox", -360); + pub const Marengo = create("America/Indiana/Marengo", -300); + pub const Petersburg = create("America/Indiana/Petersburg", -300); + pub const Tell_City = create("America/Indiana/Tell_City", -360); + pub const Vevay = create("America/Indiana/Vevay", -300); + pub const Vincennes = create("America/Indiana/Vincennes", -300); + pub const Winamac = create("America/Indiana/Winamac", -300); + }; + pub const Indianapolis = create("America/Indianapolis", -300); + pub const Inuvik = create("America/Inuvik", -420); + pub const Iqaluit = create("America/Iqaluit", -300); + pub const Jamaica = create("America/Jamaica", -300); + pub const Jujuy = create("America/Jujuy", -180); + pub const Juneau = create("America/Juneau", -540); + pub const Kentucky = struct { + // FIXME: Name conflict + pub const Louisville_ = create("America/Kentucky/Louisville", -300); + pub const Monticello = create("America/Kentucky/Monticello", -300); + }; + pub const Knox_IN = create("America/Knox_IN", -360); + pub const Kralendijk = create("America/Kralendijk", -240); + pub const La_Paz = create("America/La_Paz", -240); + pub const Lima = create("America/Lima", -300); + pub const Los_Angeles = create("America/Los_Angeles", -480); + pub const Louisville = create("America/Louisville", -300); + pub const Lower_Princes = create("America/Lower_Princes", -240); + pub const Maceio = create("America/Maceio", -180); + pub const Managua = create("America/Managua", -360); + pub const Manaus = create("America/Manaus", -240); + pub const Marigot = create("America/Marigot", -240); + pub const Martinique = create("America/Martinique", -240); + pub const Matamoros = create("America/Matamoros", -360); + pub const Mazatlan = create("America/Mazatlan", -420); + pub const Mendoza = create("America/Mendoza", -180); + pub const Menominee = create("America/Menominee", -360); + pub const Merida = create("America/Merida", -360); + pub const Metlakatla = create("America/Metlakatla", -540); + pub const Mexico_City = create("America/Mexico_City", -360); + pub const Miquelon = create("America/Miquelon", -180); + pub const Moncton = create("America/Moncton", -240); + pub const Monterrey = create("America/Monterrey", -360); + pub const Montevideo = create("America/Montevideo", -180); + pub const Montreal = create("America/Montreal", -300); + pub const Montserrat = create("America/Montserrat", -240); + pub const Nassau = create("America/Nassau", -300); + pub const New_York = create("America/New_York", -300); + pub const Nipigon = create("America/Nipigon", -300); + pub const Nome = create("America/Nome", -540); + pub const Noronha = create("America/Noronha", -120); + pub const North_Dakota = struct { + pub const Beulah = create("America/North_Dakota/Beulah", -360); + pub const Center = create("America/North_Dakota/Center", -360); + pub const New_Salem = create("America/North_Dakota/New_Salem", -360); + }; + pub const Ojinaga = create("America/Ojinaga", -420); + pub const Panama = create("America/Panama", -300); + pub const Pangnirtung = create("America/Pangnirtung", -300); + pub const Paramaribo = create("America/Paramaribo", -180); + pub const Phoenix = create("America/Phoenix", -420); + pub const Port_of_Spain = create("America/Port_of_Spain", -240); + pub const Port_au_Prince = create("America/Port-au-Prince", -300); + pub const Porto_Acre = create("America/Porto_Acre", -300); + pub const Porto_Velho = create("America/Porto_Velho", -240); + pub const Puerto_Rico = create("America/Puerto_Rico", -240); + pub const Punta_Arenas = create("America/Punta_Arenas", -180); + pub const Rainy_River = create("America/Rainy_River", -360); + pub const Rankin_Inlet = create("America/Rankin_Inlet", -360); + pub const Recife = create("America/Recife", -180); + pub const Regina = create("America/Regina", -360); + pub const Resolute = create("America/Resolute", -360); + pub const Rio_Branco = create("America/Rio_Branco", -300); + pub const Rosario = create("America/Rosario", -180); + pub const Santa_Isabel = create("America/Santa_Isabel", -480); + pub const Santarem = create("America/Santarem", -180); + pub const Santiago = create("America/Santiago", -240); + pub const Santo_Domingo = create("America/Santo_Domingo", -240); + pub const Sao_Paulo = create("America/Sao_Paulo", -180); + pub const Scoresbysund = create("America/Scoresbysund", -60); + pub const Shiprock = create("America/Shiprock", -420); + pub const Sitka = create("America/Sitka", -540); + pub const St_Barthelemy = create("America/St_Barthelemy", -240); + pub const St_Johns = create("America/St_Johns", -210); + pub const St_Kitts = create("America/St_Kitts", -240); + pub const St_Lucia = create("America/St_Lucia", -240); + pub const St_Thomas = create("America/St_Thomas", -240); + pub const St_Vincent = create("America/St_Vincent", -240); + pub const Swift_Current = create("America/Swift_Current", -360); + pub const Tegucigalpa = create("America/Tegucigalpa", -360); + pub const Thule = create("America/Thule", -240); + pub const Thunder_Bay = create("America/Thunder_Bay", -300); + pub const Tijuana = create("America/Tijuana", -480); + pub const Toronto = create("America/Toronto", -300); + pub const Tortola = create("America/Tortola", -240); + pub const Vancouver = create("America/Vancouver", -480); + pub const Virgin = create("America/Virgin", -240); + pub const Whitehorse = create("America/Whitehorse", -480); + pub const Winnipeg = create("America/Winnipeg", -360); + pub const Yakutat = create("America/Yakutat", -540); + pub const Yellowknife = create("America/Yellowknife", -420); +}; + +pub const Antarctica = struct { + pub const Casey = create("Antarctica/Casey", 660); + pub const Davis = create("Antarctica/Davis", 420); + pub const DumontDUrville = create("Antarctica/DumontDUrville", 600); + pub const Macquarie = create("Antarctica/Macquarie", 660); + pub const Mawson = create("Antarctica/Mawson", 300); + pub const McMurdo = create("Antarctica/McMurdo", 720); + pub const Palmer = create("Antarctica/Palmer", -180); + pub const Rothera = create("Antarctica/Rothera", -180); + pub const South_Pole = create("Antarctica/South_Pole", 720); + pub const Syowa = create("Antarctica/Syowa", 180); + pub const Troll = create("Antarctica/Troll", 0); + pub const Vostok = create("Antarctica/Vostok", 360); +}; + +pub const Arctic = struct { + pub const Longyearbyen = create("Arctic/Longyearbyen", 60); +}; + +pub const Asia = struct { + pub const Aden = create("Asia/Aden", 180); + pub const Almaty = create("Asia/Almaty", 360); + pub const Amman = create("Asia/Amman", 120); + pub const Anadyr = create("Asia/Anadyr", 720); + pub const Aqtau = create("Asia/Aqtau", 300); + pub const Aqtobe = create("Asia/Aqtobe", 300); + pub const Ashgabat = create("Asia/Ashgabat", 300); + pub const Ashkhabad = create("Asia/Ashkhabad", 300); + pub const Atyrau = create("Asia/Atyrau", 300); + pub const Baghdad = create("Asia/Baghdad", 180); + pub const Bahrain = create("Asia/Bahrain", 180); + pub const Baku = create("Asia/Baku", 240); + pub const Bangkok = create("Asia/Bangkok", 420); + pub const Barnaul = create("Asia/Barnaul", 420); + pub const Beirut = create("Asia/Beirut", 120); + pub const Bishkek = create("Asia/Bishkek", 360); + pub const Brunei = create("Asia/Brunei", 480); + pub const Calcutta = create("Asia/Calcutta", 330); + pub const Chita = create("Asia/Chita", 540); + pub const Choibalsan = create("Asia/Choibalsan", 480); + pub const Chongqing = create("Asia/Chongqing", 480); + pub const Chungking = create("Asia/Chungking", 480); + pub const Colombo = create("Asia/Colombo", 330); + pub const Dacca = create("Asia/Dacca", 360); + pub const Damascus = create("Asia/Damascus", 120); + pub const Dhaka = create("Asia/Dhaka", 360); + pub const Dili = create("Asia/Dili", 540); + pub const Dubai = create("Asia/Dubai", 240); + pub const Dushanbe = create("Asia/Dushanbe", 300); + pub const Famagusta = create("Asia/Famagusta", 120); + pub const Gaza = create("Asia/Gaza", 120); + pub const Harbin = create("Asia/Harbin", 480); + pub const Hebron = create("Asia/Hebron", 120); + pub const Ho_Chi_Minh = create("Asia/Ho_Chi_Minh", 420); + pub const Hong_Kong = create("Asia/Hong_Kong", 480); + pub const Hovd = create("Asia/Hovd", 420); + pub const Irkutsk = create("Asia/Irkutsk", 480); + pub const Istanbul = create("Asia/Istanbul", 180); + pub const Jakarta = create("Asia/Jakarta", 420); + pub const Jayapura = create("Asia/Jayapura", 540); + pub const Jerusalem = create("Asia/Jerusalem", 120); + pub const Kabul = create("Asia/Kabul", 270); + pub const Kamchatka = create("Asia/Kamchatka", 720); + pub const Karachi = create("Asia/Karachi", 300); + pub const Kashgar = create("Asia/Kashgar", 360); + pub const Kathmandu = create("Asia/Kathmandu", 345); + pub const Katmandu = create("Asia/Katmandu", 345); + pub const Khandyga = create("Asia/Khandyga", 540); + pub const Kolkata = create("Asia/Kolkata", 330); + pub const Krasnoyarsk = create("Asia/Krasnoyarsk", 420); + pub const Kuala_Lumpur = create("Asia/Kuala_Lumpur", 480); + pub const Kuching = create("Asia/Kuching", 480); + pub const Kuwait = create("Asia/Kuwait", 180); + pub const Macao = create("Asia/Macao", 480); + pub const Macau = create("Asia/Macau", 480); + pub const Magadan = create("Asia/Magadan", 660); + pub const Makassar = create("Asia/Makassar", 480); + pub const Manila = create("Asia/Manila", 480); + pub const Muscat = create("Asia/Muscat", 240); + pub const Nicosia = create("Asia/Nicosia", 120); + pub const Novokuznetsk = create("Asia/Novokuznetsk", 420); + pub const Novosibirsk = create("Asia/Novosibirsk", 420); + pub const Omsk = create("Asia/Omsk", 360); + pub const Oral = create("Asia/Oral", 300); + pub const Phnom_Penh = create("Asia/Phnom_Penh", 420); + pub const Pontianak = create("Asia/Pontianak", 420); + pub const Pyongyang = create("Asia/Pyongyang", 540); + pub const Qatar = create("Asia/Qatar", 180); + pub const Qyzylorda = create("Asia/Qyzylorda", 300); + pub const Rangoon = create("Asia/Rangoon", 390); + pub const Riyadh = create("Asia/Riyadh", 180); + pub const Saigon = create("Asia/Saigon", 420); + pub const Sakhalin = create("Asia/Sakhalin", 660); + pub const Samarkand = create("Asia/Samarkand", 300); + pub const Seoul = create("Asia/Seoul", 540); + pub const Shanghai = create("Asia/Shanghai", 480); + pub const Singapore = create("Asia/Singapore", 480); + pub const Srednekolymsk = create("Asia/Srednekolymsk", 660); + pub const Taipei = create("Asia/Taipei", 480); + pub const Tashkent = create("Asia/Tashkent", 300); + pub const Tbilisi = create("Asia/Tbilisi", 240); + pub const Tehran = create("Asia/Tehran", 210); + pub const Tel_Aviv = create("Asia/Tel_Aviv", 120); + pub const Thimbu = create("Asia/Thimbu", 360); + pub const Thimphu = create("Asia/Thimphu", 360); + pub const Tokyo = create("Asia/Tokyo", 540); + pub const Tomsk = create("Asia/Tomsk", 420); + pub const Ujung_Pandang = create("Asia/Ujung_Pandang", 480); + pub const Ulaanbaatar = create("Asia/Ulaanbaatar", 480); + pub const Ulan_Bator = create("Asia/Ulan_Bator", 480); + pub const Urumqi = create("Asia/Urumqi", 360); + pub const Ust_Nera = create("Asia/Ust-Nera", 600); + pub const Vientiane = create("Asia/Vientiane", 420); + pub const Vladivostok = create("Asia/Vladivostok", 600); + pub const Yakutsk = create("Asia/Yakutsk", 540); + pub const Yangon = create("Asia/Yangon", 390); + pub const Yekaterinburg = create("Asia/Yekaterinburg", 300); + pub const Yerevan = create("Asia/Yerevan", 240); +}; + +pub const Atlantic = struct { + pub const Azores = create("Atlantic/Azores", -60); + pub const Bermuda = create("Atlantic/Bermuda", -240); + pub const Canary = create("Atlantic/Canary", 0); + pub const Cape_Verde = create("Atlantic/Cape_Verde", -60); + pub const Faeroe = create("Atlantic/Faeroe", 0); + pub const Faroe = create("Atlantic/Faroe", 0); + pub const Jan_Mayen = create("Atlantic/Jan_Mayen", 60); + pub const Madeira = create("Atlantic/Madeira", 0); + pub const Reykjavik = create("Atlantic/Reykjavik", 0); + pub const South_Georgia = create("Atlantic/South_Georgia", -120); + pub const St_Helena = create("Atlantic/St_Helena", 0); + pub const Stanley = create("Atlantic/Stanley", -180); +}; + +pub const Australia = struct { + pub const ACT = create("Australia/ACT", 600); + pub const Adelaide = create("Australia/Adelaide", 570); + pub const Brisbane = create("Australia/Brisbane", 600); + pub const Broken_Hill = create("Australia/Broken_Hill", 570); + pub const Canberra = create("Australia/Canberra", 600); + pub const Currie = create("Australia/Currie", 600); + pub const Darwin = create("Australia/Darwin", 570); + pub const Eucla = create("Australia/Eucla", 525); + pub const Hobart = create("Australia/Hobart", 600); + pub const LHI = create("Australia/LHI", 630); + pub const Lindeman = create("Australia/Lindeman", 600); + pub const Lord_Howe = create("Australia/Lord_Howe", 630); + pub const Melbourne = create("Australia/Melbourne", 600); + pub const North = create("Australia/North", 570); + pub const NSW = create("Australia/NSW", 600); + pub const Perth = create("Australia/Perth", 480); + pub const Queensland = create("Australia/Queensland", 600); + pub const South = create("Australia/South", 570); + pub const Sydney = create("Australia/Sydney", 600); + pub const Tasmania = create("Australia/Tasmania", 600); + pub const Victoria = create("Australia/Victoria", 600); + pub const West = create("Australia/West", 480); + pub const Yancowinna = create("Australia/Yancowinna", 570); +}; + +pub const Brazil = struct { + pub const Acre = create("Brazil/Acre", -300); + pub const DeNoronha = create("Brazil/DeNoronha", -120); + pub const East = create("Brazil/East", -180); + pub const West = create("Brazil/West", -240); +}; + +pub const Canada = struct { + pub const Atlantic = create("Canada/Atlantic", -240); + pub const Central = create("Canada/Central", -360); + pub const Eastern = create("Canada/Eastern", -300); + pub const Mountain = create("Canada/Mountain", -420); + pub const Newfoundland = create("Canada/Newfoundland", -210); + pub const Pacific = create("Canada/Pacific", -480); + pub const Saskatchewan = create("Canada/Saskatchewan", -360); + pub const Yukon = create("Canada/Yukon", -480); +}; +pub const CET = create("CET", 60); + +pub const Chile = struct { + pub const Continental = create("Chile/Continental", -240); + pub const EasterIsland = create("Chile/EasterIsland", -360); +}; +pub const CST6CDT = create("CST6CDT", -360); +pub const Cuba = create("Cuba", -300); +pub const EET = create("EET", 120); +pub const Egypt = create("Egypt", 120); +pub const Eire = create("Eire", 0); +pub const EST = create("EST", -300); +pub const EST5EDT = create("EST5EDT", -300); + +pub const Etc = struct { + // NOTE: The signs are intentionally inverted. See the Etc area description. + pub const GMT = create("Etc/GMT", 0); + pub const GMTp0 = create("Etc/GMT+0", 0); + pub const GMTp1 = create("Etc/GMT+1", -60); + pub const GMTp10 = create("Etc/GMT+10", -600); + pub const GMTp11 = create("Etc/GMT+11", -660); + pub const GMTp12 = create("Etc/GMT+12", -720); + pub const GMTp2 = create("Etc/GMT+2", -120); + pub const GMTp3 = create("Etc/GMT+3", -180); + pub const GMTp4 = create("Etc/GMT+4", -240); + pub const GMTp5 = create("Etc/GMT+5", -300); + pub const GMTp6 = create("Etc/GMT+6", -360); + pub const GMTp7 = create("Etc/GMT+7", -420); + pub const GMTp8 = create("Etc/GMT+8", -480); + pub const GMTp9 = create("Etc/GMT+9", -540); + pub const GMT0 = create("Etc/GMT0", 0); + pub const GMTm0 = create("Etc/GMT-0", 0); + pub const GMTm1 = create("Etc/GMT-1", 60); + pub const GMTm10 = create("Etc/GMT-10", 600); + pub const GMTm11 = create("Etc/GMT-11", 660); + pub const GMTm12 = create("Etc/GMT-12", 720); + pub const GMTm13 = create("Etc/GMT-13", 780); + pub const GMTm14 = create("Etc/GMT-14", 840); + pub const GMTm2 = create("Etc/GMT-2", 120); + pub const GMTm3 = create("Etc/GMT-3", 180); + pub const GMTm4 = create("Etc/GMT-4", 240); + pub const GMTm5 = create("Etc/GMT-5", 300); + pub const GMTm6 = create("Etc/GMT-6", 360); + pub const GMTm7 = create("Etc/GMT-7", 420); + pub const GMTm8 = create("Etc/GMT-8", 480); + pub const GMTm9 = create("Etc/GMT-9", 540); + pub const Greenwich = create("Etc/Greenwich", 0); + pub const UCT = create("Etc/UCT", 0); + pub const Universal = create("Etc/Universal", 0); + pub const UTC = create("Etc/UTC", 0); + pub const Zulu = create("Etc/Zulu", 0); +}; + +pub const Europe = struct { + pub const Amsterdam = create("Europe/Amsterdam", 60); + pub const Andorra = create("Europe/Andorra", 60); + pub const Astrakhan = create("Europe/Astrakhan", 240); + pub const Athens = create("Europe/Athens", 120); + pub const Belfast = create("Europe/Belfast", 0); + pub const Belgrade = create("Europe/Belgrade", 60); + pub const Berlin = create("Europe/Berlin", 60); + pub const Bratislava = create("Europe/Bratislava", 60); + pub const Brussels = create("Europe/Brussels", 60); + pub const Bucharest = create("Europe/Bucharest", 120); + pub const Budapest = create("Europe/Budapest", 60); + pub const Busingen = create("Europe/Busingen", 60); + pub const Chisinau = create("Europe/Chisinau", 120); + pub const Copenhagen = create("Europe/Copenhagen", 60); + pub const Dublin = create("Europe/Dublin", 0); + pub const Gibraltar = create("Europe/Gibraltar", 60); + pub const Guernsey = create("Europe/Guernsey", 0); + pub const Helsinki = create("Europe/Helsinki", 120); + pub const Isle_of_Man = create("Europe/Isle_of_Man", 0); + pub const Istanbul = create("Europe/Istanbul", 180); + pub const Jersey = create("Europe/Jersey", 0); + pub const Kaliningrad = create("Europe/Kaliningrad", 120); + pub const Kiev = create("Europe/Kiev", 120); + pub const Kirov = create("Europe/Kirov", 180); + pub const Lisbon = create("Europe/Lisbon", 0); + pub const Ljubljana = create("Europe/Ljubljana", 60); + pub const London = create("Europe/London", 0); + pub const Luxembourg = create("Europe/Luxembourg", 60); + pub const Madrid = create("Europe/Madrid", 60); + pub const Malta = create("Europe/Malta", 60); + pub const Mariehamn = create("Europe/Mariehamn", 120); + pub const Minsk = create("Europe/Minsk", 180); + pub const Monaco = create("Europe/Monaco", 60); + pub const Moscow = create("Europe/Moscow", 180); + pub const Oslo = create("Europe/Oslo", 60); + pub const Paris = create("Europe/Paris", 60); + pub const Podgorica = create("Europe/Podgorica", 60); + pub const Prague = create("Europe/Prague", 60); + pub const Riga = create("Europe/Riga", 120); + pub const Rome = create("Europe/Rome", 60); + pub const Samara = create("Europe/Samara", 240); + pub const San_Marino = create("Europe/San_Marino", 60); + pub const Sarajevo = create("Europe/Sarajevo", 60); + pub const Saratov = create("Europe/Saratov", 240); + pub const Simferopol = create("Europe/Simferopol", 180); + pub const Skopje = create("Europe/Skopje", 60); + pub const Sofia = create("Europe/Sofia", 120); + pub const Stockholm = create("Europe/Stockholm", 60); + pub const Tallinn = create("Europe/Tallinn", 120); + pub const Tirane = create("Europe/Tirane", 60); + pub const Tiraspol = create("Europe/Tiraspol", 120); + pub const Ulyanovsk = create("Europe/Ulyanovsk", 240); + pub const Uzhgorod = create("Europe/Uzhgorod", 120); + pub const Vaduz = create("Europe/Vaduz", 60); + pub const Vatican = create("Europe/Vatican", 60); + pub const Vienna = create("Europe/Vienna", 60); + pub const Vilnius = create("Europe/Vilnius", 120); + pub const Volgograd = create("Europe/Volgograd", 240); + pub const Warsaw = create("Europe/Warsaw", 60); + pub const Zagreb = create("Europe/Zagreb", 60); + pub const Zaporozhye = create("Europe/Zaporozhye", 120); + pub const Zurich = create("Europe/Zurich", 60); +}; +pub const GB = create("GB", 0); +pub const GB_Eire = create("GB-Eire", 0); +pub const GMT = create("GMT", 0); +pub const GMTp0 = create("GMT+0", 0); +pub const GMT0 = create("GMT0", 0); +pub const GMTm0 = create("GMT-0", 0); +pub const Greenwich = create("Greenwich", 0); +pub const Hongkong = create("Hongkong", 480); +pub const HST = create("HST", -600); +pub const Iceland = create("Iceland", 0); + +pub const Indian = struct { + pub const Antananarivo = create("Indian/Antananarivo", 180); + pub const Chagos = create("Indian/Chagos", 360); + pub const Christmas = create("Indian/Christmas", 420); + pub const Cocos = create("Indian/Cocos", 390); + pub const Comoro = create("Indian/Comoro", 180); + pub const Kerguelen = create("Indian/Kerguelen", 300); + pub const Mahe = create("Indian/Mahe", 240); + pub const Maldives = create("Indian/Maldives", 300); + pub const Mauritius = create("Indian/Mauritius", 240); + pub const Mayotte = create("Indian/Mayotte", 180); + pub const Reunion = create("Indian/Reunion", 240); +}; +pub const Iran = create("Iran", 210); +pub const Israel = create("Israel", 120); +pub const Jamaica = create("Jamaica", -300); +pub const Japan = create("Japan", 540); +pub const Kwajalein = create("Kwajalein", 720); +pub const Libya = create("Libya", 120); +pub const MET = create("MET", 60); + +pub const Mexico = struct { + pub const BajaNorte = create("Mexico/BajaNorte", -480); + pub const BajaSur = create("Mexico/BajaSur", -420); + pub const General = create("Mexico/General", -360); +}; +pub const MST = create("MST", -420); +pub const MST7MDT = create("MST7MDT", -420); +pub const Navajo = create("Navajo", -420); +pub const NZ = create("NZ", 720); +pub const NZ_CHAT = create("NZ-CHAT", 765); + +pub const Pacific = struct { + pub const Apia = create("Pacific/Apia", 780); + pub const Auckland = create("Pacific/Auckland", 720); + pub const Bougainville = create("Pacific/Bougainville", 660); + pub const Chatham = create("Pacific/Chatham", 765); + pub const Chuuk = create("Pacific/Chuuk", 600); + pub const Easter = create("Pacific/Easter", -360); + pub const Efate = create("Pacific/Efate", 660); + pub const Enderbury = create("Pacific/Enderbury", 780); + pub const Fakaofo = create("Pacific/Fakaofo", 780); + pub const Fiji = create("Pacific/Fiji", 720); + pub const Funafuti = create("Pacific/Funafuti", 720); + pub const Galapagos = create("Pacific/Galapagos", -360); + pub const Gambier = create("Pacific/Gambier", -540); + pub const Guadalcanal = create("Pacific/Guadalcanal", 660); + pub const Guam = create("Pacific/Guam", 600); + pub const Honolulu = create("Pacific/Honolulu", -600); + pub const Johnston = create("Pacific/Johnston", -600); + pub const Kiritimati = create("Pacific/Kiritimati", 840); + pub const Kosrae = create("Pacific/Kosrae", 660); + pub const Kwajalein = create("Pacific/Kwajalein", 720); + pub const Majuro = create("Pacific/Majuro", 720); + pub const Marquesas = create("Pacific/Marquesas", -570); + pub const Midway = create("Pacific/Midway", -660); + pub const Nauru = create("Pacific/Nauru", 720); + pub const Niue = create("Pacific/Niue", -660); + pub const Norfolk = create("Pacific/Norfolk", 660); + pub const Noumea = create("Pacific/Noumea", 660); + pub const Pago_Pago = create("Pacific/Pago_Pago", -660); + pub const Palau = create("Pacific/Palau", 540); + pub const Pitcairn = create("Pacific/Pitcairn", -480); + pub const Pohnpei = create("Pacific/Pohnpei", 660); + pub const Ponape = create("Pacific/Ponape", 660); + pub const Port_Moresby = create("Pacific/Port_Moresby", 600); + pub const Rarotonga = create("Pacific/Rarotonga", -600); + pub const Saipan = create("Pacific/Saipan", 600); + pub const Samoa = create("Pacific/Samoa", -660); + pub const Tahiti = create("Pacific/Tahiti", -600); + pub const Tarawa = create("Pacific/Tarawa", 720); + pub const Tongatapu = create("Pacific/Tongatapu", 780); + pub const Truk = create("Pacific/Truk", 600); + pub const Wake = create("Pacific/Wake", 720); + pub const Wallis = create("Pacific/Wallis", 720); + pub const Yap = create("Pacific/Yap", 600); +}; +pub const Poland = create("Poland", 60); +pub const Portugal = create("Portugal", 0); +pub const PRC = create("PRC", 480); +pub const PST8PDT = create("PST8PDT", -480); +pub const ROC = create("ROC", 480); +pub const ROK = create("ROK", 540); +pub const Singapore = create("Singapore", 480); +pub const Turkey = create("Turkey", 180); +pub const UCT = create("UCT", 0); +pub const Universal = create("Universal", 0); + +pub const US = struct { + pub const Alaska = create("US/Alaska", -540); + pub const Aleutian = create("US/Aleutian", -600); + pub const Arizona = create("US/Arizona", -420); + pub const Central = create("US/Central", -360); + pub const Eastern = create("US/Eastern", -300); + pub const East_Indiana = create("US/East-Indiana", -300); + pub const Hawaii = create("US/Hawaii", -600); + pub const Indiana_Starke = create("US/Indiana-Starke", -360); + pub const Michigan = create("US/Michigan", -300); + pub const Mountain = create("US/Mountain", -420); + pub const Pacific = create("US/Pacific", -480); + pub const Pacific_New = create("US/Pacific-New", -480); + pub const Samoa = create("US/Samoa", -660); +}; +pub const UTC = create("UTC", 0); +pub const WET = create("WET", 0); +pub const W_SU = create("W-SU", 180); +pub const Zulu = create("Zulu", 0); + +// TODO: Allow lookup by name +//pub fn getAll() []*const Timezone { +// for (comptime std.meta.fields(@This())) |field { +// +// } +//} + +//pub fn get(name: []const u8) ?*const Timezone { +// return ALL_TIMEZONES.getValue(name); +//} + +test "timezone-get" { + const testing = std.testing; + //try testing.expect(get("America/New_York").? == America.New_York); + try testing.expect(America.New_York.offset == -300); +} diff --git a/src/db/db.zig b/src/db/db.zig index a7b3a71..d51df3b 100644 --- a/src/db/db.zig +++ b/src/db/db.zig @@ -152,6 +152,15 @@ pub const Db = struct { }; } + pub fn updateHideById(self: *Db, comptime Type: type, comptime hide: bool, id: u32) !void { + const hide_val = if (hide) 1 else 0; + const now = @intCast(u64, std.time.milliTimestamp()); + self._sql_db.exec(models.createHideQuery(Type), .{}, .{ .hide = hide_val, .updated_at = now, .id = id }) catch |err| { + std.log.err("Encountered error while updating hide on model {s}", .{@typeName(Type)}); + return err; + }; + } + pub fn insert(self: *Db, comptime Type: type, values: anytype) !void { comptime { const query = models.createInsertQuery(Type); diff --git a/src/db/models.zig b/src/db/models.zig index 6c14579..bccd53a 100644 --- a/src/db/models.zig +++ b/src/db/models.zig @@ -197,6 +197,23 @@ pub inline fn createUpdateQuery(comptime Type: type) []const u8 { } } +pub fn createHideQuery(comptime Type: type) []const u8 { + comptime { + var has_hide = false; + inline for (@typeInfo(Type).Struct.fields) |field| { + if (std.mem.eql(u8, field.name, "hide")) { + has_hide = true; + } + } + + if (!has_hide) { + @compileError("Model does not have hide field, cannot create hide query: " ++ @typeName(Type)); + } + + return "UPDATE " ++ getTypeTableName(Type) ++ " SET hide = ?, updated_at = ? WHERE id = ?;"; + } +} + pub inline fn createTableDeleteQuery(comptime Type: type) []const u8 { return "DROP TABLE IF EXISTS " ++ getTypeTableName(Type) ++ ";"; } diff --git a/src/http_handler.zig b/src/http_handler.zig index 35655bb..babbc86 100644 --- a/src/http_handler.zig +++ b/src/http_handler.zig @@ -73,19 +73,21 @@ pub fn startHttpServer() !void { router.put("/user", user.putUser); // router.delete("/user/:id", user.deleteUser); - router.get("/shared_notes/:limit", note.getSharedNotes); - router.put("/shared_notes", note.putSharedNote); - router.post("/shared_notes", note.postSharedNote); + router.get("/shared_note/:limit", note.getSharedNotes); + router.put("/shared_note", note.putSharedNote); + router.post("/shared_note", note.postSharedNote); // router.get("/budget/:id", budget.getBudget); // router.put("/budget", budget.putBudget); // router.post("/budget", budget.postBudget); - router.put("/budget_category", budget.putBudgetCategory); router.post("/budget_category", budget.postBudgetCategory); + router.put("/budget_category", budget.putBudgetCategory); + router.delete("/budget_category", budget.deleteBudgetCategory); - router.post("/transactions", trans.postTransaction); - router.put("/transactions", trans.putTransaction); + router.post("/transaction", trans.postTransaction); + router.put("/transaction", trans.putTransaction); + router.delete("/transaction", trans.deleteTransaction); router.get("/dashboard", dash.getDashboard); @@ -100,7 +102,10 @@ fn notFound(_: *httpz.Request, res: *httpz.Response) !void { // you can set the body directly to a []u8, but note that the memory // must be valid beyond your handler. Use the res.arena if you need to allocate // memory for the body. - res.body = "Not Found"; + try res.json( + .{ .success = false, .message = "Not Found" }, + .{}, + ); } // note that the error handler return `void` and not `!void` diff --git a/src/routes/budget.zig b/src/routes/budget.zig index c782d20..bcbf933 100644 --- a/src/routes/budget.zig +++ b/src/routes/budget.zig @@ -232,3 +232,38 @@ pub fn putBudgetCategory(req: *httpz.Request, res: *httpz.Response) !void { try handler.returnData(updated_budget_cat.?, res); return; } + +const deleteIdReq = struct { + id: u32, +}; + +pub fn deleteBudgetCategory(req: *httpz.Request, res: *httpz.Response) !void { + var db = handler.getDb(); + var gpa = std.heap.GeneralPurposeAllocator(.{}){}; + const allocator = gpa.allocator(); + + const body = handler.getReqJson(req, res, deleteIdReq) catch { + return; + }; + + const budget_category = try db.selectOneById(models.BudgetCategory, allocator, body.id); + if (budget_category == null) { + return handler.returnError("Cannot find budget", 404, res); + } + const budget = try db.selectOneById(models.Budget, allocator, budget_category.?.budget_id); + if (budget == null) { + return handler.returnError("Cannot find budget", 404, res); + } + + _ = auth.verifyRequest(req, res, null, budget.?.family_id) catch { + return; + }; + + try db.updateHideById(models.BudgetCategory, true, body.id); + + const updated_budget_category = try db.selectOneById(models.BudgetCategory, allocator, body.id); + if (budget_category == null) { + return handler.returnError("Could not delete category", 500, res); + } + return try handler.returnData(updated_budget_category.?, res); +} diff --git a/src/routes/transactions.zig b/src/routes/transactions.zig index f0e2d6e..1214a79 100644 --- a/src/routes/transactions.zig +++ b/src/routes/transactions.zig @@ -3,15 +3,21 @@ const httpz = @import("../.deps/http.zig/src/httpz.zig"); const models = @import("../db/models.zig"); const ztime = @import("../.deps/time.zig"); const utils = @import("../utils.zig"); +const time = @import("../.deps/datetime.zig"); +const tz = @import("../.deps/timezones.zig"); const auth = @import("auth.zig"); const handler = @import("../http_handler.zig"); pub fn fetchTransFromDb(allocator: std.mem.Allocator, family_id: u32) !?[]models.Transaction { var db = handler.getDb(); - const now = ztime.DateTime.now(); - const beginningOfMonth = ztime.DateTime.init(now.years, now.months, 0, 0, 0, 0); + const now = time.Datetime.now(); + var beginning_of_month = try time.Datetime.fromDate(now.date.year, now.date.month, 1); + const timezone_begin = beginning_of_month.shiftTimezone(&tz.US.Mountain); + // std.log.info("Beginning: {} Shifted: {}", .{ beginning_of_month.date.toTimestamp(), timezone_begin.date.toTimestamp() }); + const begin_time = @bitCast(u64, timezone_begin.date.toTimestamp()); + // std.log.info("Fetching transactions after beginning of month: unix {}", .{begin_time}); comptime { if (!std.mem.eql(u8, @typeInfo(models.Transaction).Struct.fields[7].name, "date")) { return error{TransactionModelError}; @@ -32,7 +38,7 @@ pub fn fetchTransFromDb(allocator: std.mem.Allocator, family_id: u32) !?[]models models.Transaction, allocator, "WHERE budget_id = ? AND date > ? AND hide = ?", - .{ .budget_id = budget.?.id, .date = beginningOfMonth.toUnixMilli(), .hide = 0 }, + .{ .budget_id = budget.?.id, .date = begin_time, .hide = 0 }, null, null, null, @@ -167,3 +173,38 @@ pub fn postTransaction(req: *httpz.Request, res: *httpz.Response) !void { } return; } + +const deleteIdReq = struct { + id: u32, +}; + +pub fn deleteTransaction(req: *httpz.Request, res: *httpz.Response) !void { + var db = handler.getDb(); + var gpa = std.heap.GeneralPurposeAllocator(.{}){}; + const allocator = gpa.allocator(); + + const body = handler.getReqJson(req, res, deleteIdReq) catch { + return; + }; + + const transaction = try db.selectOneById(models.Transaction, allocator, body.id); + if (transaction == null) { + return handler.returnError("Id invalid, not a transaction", 400, res); + } + + const budget = try db.selectOneById(models.Budget, allocator, transaction.?.budget_id); + if (budget == null) { + return handler.returnError("Cannot find associated budget", 404, res); + } + + _ = auth.verifyRequest(req, res, null, budget.?.family_id) catch { + return; + }; + + try db.updateHideById(models.Transaction, true, body.id); + const updated_transaction = try db.selectOneById(models.Transaction, allocator, body.id); + if (updated_transaction == null) { + return handler.returnError("Could not delete transaction", 500, res); + } + return try handler.returnData(updated_transaction.?, res); +} diff --git a/src/utils.zig b/src/utils.zig index 4a147d4..f0e7f39 100644 --- a/src/utils.zig +++ b/src/utils.zig @@ -6,6 +6,12 @@ fn SpreadResult(comptime Base: type, comptime Additional: type) type { // if (@Type(type_info) != std.builtin.Type.Struct) { // @compileError("Cannot have anything but struct but got: " ++ @typeName(Base)); // } + if (@typeInfo(Base) != .Struct) { + @compileError("Provided non struct to struct concat: " ++ @typeName(Base)); + } + if (@typeInfo(Additional) != .Struct) { + @compileError("Provided non struct to struct concat: " ++ @typeName(Additional)); + } // _ = std.fmt.comptimePrint("Passed in base: {} {}", .{ Base, type_info }); } var fields = @typeInfo(Base).Struct.fields; @@ -30,6 +36,11 @@ pub fn structConcatFields( base: anytype, additional: anytype, ) SpreadResult(@TypeOf(base), @TypeOf(additional)) { + // comptime { + // if (@typeInfo(@TypeOf(base)) != .Struct or @typeInfo(@TypeOf(additional)) != .Struct) { + // @compileError("Provided non struct to struct concat"); + // } + // } const Base = @TypeOf(base); const Additional = @TypeOf(additional); var result: SpreadResult(Base, Additional) = undefined;