refactor(date): replace most date parsing with zeit functions
This commit is contained in:
parent
e22b4246c1
commit
6028255aa3
1 changed files with 47 additions and 247 deletions
|
@ -4,137 +4,66 @@
|
||||||
|
|
||||||
const std = @import("std");
|
const std = @import("std");
|
||||||
const log = std.log.scoped(.date);
|
const log = std.log.scoped(.date);
|
||||||
|
const zeit = @import("zeit");
|
||||||
|
|
||||||
pub const DateTime = struct { day: u8, month: u8, year: u16, hour: u8, minute: u8, second: u8 };
|
pub const DateTime = struct {
|
||||||
|
day: u8,
|
||||||
|
month: u8,
|
||||||
|
year: u16,
|
||||||
|
hour: u8,
|
||||||
|
minute: u8,
|
||||||
|
second: u8,
|
||||||
|
|
||||||
|
pub fn fromInstant(val: zeit.Instant) DateTime {
|
||||||
|
return fromTime(val.time());
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn fromTime(val: zeit.Time) DateTime {
|
||||||
|
return DateTime{
|
||||||
|
.day = val.day,
|
||||||
|
.month = @intFromEnum(val.month),
|
||||||
|
.year = @intCast(val.year),
|
||||||
|
.hour = val.hour,
|
||||||
|
.minute = val.minute,
|
||||||
|
.second = val.second,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn time(self: DateTime) zeit.Time {
|
||||||
|
return zeit.Time{
|
||||||
|
.day = @intCast(self.day),
|
||||||
|
.month = @enumFromInt(self.month),
|
||||||
|
.year = self.year,
|
||||||
|
.hour = @intCast(self.hour),
|
||||||
|
.minute = @intCast(self.minute),
|
||||||
|
.second = @intCast(self.second),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn instant(self: DateTime) !zeit.Instant {
|
||||||
|
return try zeit.instant(.{ .source = .{ .time = self.time() } });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const SECONDS_PER_DAY = 86400; //* 24* 60 * 60 */
|
const SECONDS_PER_DAY = 86400; //* 24* 60 * 60 */
|
||||||
const DAYS_PER_YEAR = 365; //* Normal year (no leap year) */
|
const DAYS_PER_YEAR = 365; //* Normal year (no leap year) */
|
||||||
|
|
||||||
pub fn timestampToDateTime(timestamp: i64) DateTime {
|
pub fn timestampToDateTime(timestamp: zeit.Seconds) DateTime {
|
||||||
|
const ins = zeit.instant(.{ .source = .{ .unix_timestamp = timestamp } }) catch @panic("Failed to create instant from timestamp");
|
||||||
// aus https://de.wikipedia.org/wiki/Unixzeit
|
return DateTime.fromInstant(ins);
|
||||||
const unixtime = @as(u64, @intCast(timestamp));
|
|
||||||
const DAYS_IN_4_YEARS = 1461; //* 4*365 + 1 */
|
|
||||||
const DAYS_IN_100_YEARS = 36524; //* 100*365 + 25 - 1 */
|
|
||||||
const DAYS_IN_400_YEARS = 146097; //* 400*365 + 100 - 4 + 1 */
|
|
||||||
const DAY_NUMBER_ADJUSTED_1970_01_01 = 719468; //* Day number relates to March 1st */
|
|
||||||
|
|
||||||
var dayN: u64 = DAY_NUMBER_ADJUSTED_1970_01_01 + unixtime / SECONDS_PER_DAY;
|
|
||||||
const seconds_since_midnight: u64 = unixtime % SECONDS_PER_DAY;
|
|
||||||
var temp: u64 = 0;
|
|
||||||
|
|
||||||
// Leap year rules for Gregorian Calendars
|
|
||||||
// Any year divisible by 100 is not a leap year unless also divisible by 400
|
|
||||||
temp = 4 * (dayN + DAYS_IN_100_YEARS + 1) / DAYS_IN_400_YEARS - 1;
|
|
||||||
var year = @as(u16, @intCast(100 * temp));
|
|
||||||
dayN -= DAYS_IN_100_YEARS * temp + temp / 4;
|
|
||||||
|
|
||||||
// For Julian calendars, each year divisible by 4 is a leap year
|
|
||||||
temp = 4 * (dayN + DAYS_PER_YEAR + 1) / DAYS_IN_4_YEARS - 1;
|
|
||||||
year += @as(u16, @intCast(temp));
|
|
||||||
dayN -= DAYS_PER_YEAR * temp + temp / 4;
|
|
||||||
|
|
||||||
// dayN calculates the days of the year in relation to March 1
|
|
||||||
var month = @as(u8, @intCast((5 * dayN + 2) / 153));
|
|
||||||
const day = @as(u8, @intCast(dayN - (@as(u64, @intCast(month)) * 153 + 2) / 5 + 1));
|
|
||||||
// 153 = 31+30+31+30+31 Days for the 5 months from March through July
|
|
||||||
// 153 = 31+30+31+30+31 Days for the 5 months from August through December
|
|
||||||
// 31+28 Days for January and February (see below)
|
|
||||||
// +2: Rounding adjustment
|
|
||||||
// +1: The first day in March is March 1st (not March 0)
|
|
||||||
|
|
||||||
month += 3; // Convert from the day that starts on March 1st, to a human year */
|
|
||||||
if (month > 12) { // months 13 and 14 become 1 (January) und 2 (February) of the next year
|
|
||||||
month -= 12;
|
|
||||||
year += 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
const hours = @as(u8, @intCast(seconds_since_midnight / 3600));
|
|
||||||
const minutes = @as(u8, @intCast(seconds_since_midnight % 3600 / 60));
|
|
||||||
const seconds = @as(u8, @intCast(seconds_since_midnight % 60));
|
|
||||||
|
|
||||||
return DateTime{ .day = day, .month = month, .year = year, .hour = hours, .minute = minutes, .second = seconds };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn parseEnglishToTimestamp(data: []const u8) !i64 {
|
pub fn parseEnglishToTimestamp(data: []const u8) !i64 {
|
||||||
return try dateTimeToTimestamp(try parseEnglishToDateTime(data));
|
return try dateTimeToTimestamp(try parseEnglishToDateTime(data));
|
||||||
}
|
}
|
||||||
|
|
||||||
const EnglishParsingState = enum { Start, Day, Month, Year, Hour, Minute, Second, End };
|
|
||||||
/// Converts a string to a timestamp value. May not handle dates before the
|
/// Converts a string to a timestamp value. May not handle dates before the
|
||||||
/// epoch. Dates should look like "Fri, 03 Jun 2022 18:12:36 GMT"
|
/// epoch. Dates should look like "Fri, 03 Jun 2022 18:12:36 GMT"
|
||||||
pub fn parseEnglishToDateTime(data: []const u8) !DateTime {
|
pub fn parseEnglishToDateTime(data: []const u8) !DateTime {
|
||||||
// Fri, 03 Jun 2022 18:12:36 GMT
|
const ins = try zeit.instant(.{ .source = .{ .rfc1123 = data } });
|
||||||
if (!std.mem.endsWith(u8, data, "GMT")) return error.InvalidFormat;
|
return DateTime.fromInstant(ins);
|
||||||
|
|
||||||
var start: usize = 0;
|
|
||||||
var state = EnglishParsingState.Start;
|
|
||||||
// Anything not explicitly set by our string would be 0
|
|
||||||
var rc = DateTime{ .year = 0, .month = 0, .day = 0, .hour = 0, .minute = 0, .second = 0 };
|
|
||||||
for (data, 0..) |ch, i| {
|
|
||||||
switch (ch) {
|
|
||||||
',' => {},
|
|
||||||
' ', ':' => {
|
|
||||||
// State transition
|
|
||||||
|
|
||||||
// We're going to coerce and this might not go well, but we
|
|
||||||
// want the compiler to create checks, so we'll turn on
|
|
||||||
// runtime safety for this block, forcing checks in ReleaseSafe
|
|
||||||
// ReleaseFast modes.
|
|
||||||
const next_state = try endEnglishState(state, &rc, data[start..i]);
|
|
||||||
state = next_state;
|
|
||||||
start = i + 1;
|
|
||||||
},
|
|
||||||
else => {}, // We need to be pretty trusting on this format...
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return rc;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn endEnglishState(current_state: EnglishParsingState, date: *DateTime, prev_data: []const u8) !EnglishParsingState {
|
|
||||||
var next_state: EnglishParsingState = undefined;
|
|
||||||
log.debug("endEnglishState. Current state '{}', data: {s}", .{ current_state, prev_data });
|
|
||||||
|
|
||||||
// Using two switches is slightly less efficient, but more readable
|
|
||||||
switch (current_state) {
|
|
||||||
.End => return error.IllegalStateTransition,
|
|
||||||
.Start => next_state = .Day,
|
|
||||||
.Day => next_state = .Month,
|
|
||||||
.Month => next_state = .Year,
|
|
||||||
.Year => next_state = .Hour,
|
|
||||||
.Hour => next_state = .Minute,
|
|
||||||
.Minute => next_state = .Second,
|
|
||||||
.Second => next_state = .End,
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (current_state) {
|
|
||||||
.Year => date.year = try std.fmt.parseUnsigned(u16, prev_data, 10),
|
|
||||||
.Month => date.month = try parseEnglishMonth(prev_data),
|
|
||||||
.Day => date.day = try std.fmt.parseUnsigned(u8, prev_data, 10),
|
|
||||||
.Hour => date.hour = try std.fmt.parseUnsigned(u8, prev_data, 10),
|
|
||||||
.Minute => date.minute = try std.fmt.parseUnsigned(u8, prev_data, 10),
|
|
||||||
.Second => date.second = try std.fmt.parseUnsigned(u8, prev_data, 10),
|
|
||||||
.Start => {},
|
|
||||||
.End => return error.InvalidState,
|
|
||||||
}
|
|
||||||
return next_state;
|
|
||||||
}
|
|
||||||
|
|
||||||
fn parseEnglishMonth(data: []const u8) !u8 {
|
|
||||||
if (std.ascii.startsWithIgnoreCase(data, "Jan")) return 1;
|
|
||||||
if (std.ascii.startsWithIgnoreCase(data, "Feb")) return 2;
|
|
||||||
if (std.ascii.startsWithIgnoreCase(data, "Mar")) return 3;
|
|
||||||
if (std.ascii.startsWithIgnoreCase(data, "Apr")) return 4;
|
|
||||||
if (std.ascii.startsWithIgnoreCase(data, "May")) return 5;
|
|
||||||
if (std.ascii.startsWithIgnoreCase(data, "Jun")) return 6;
|
|
||||||
if (std.ascii.startsWithIgnoreCase(data, "Jul")) return 7;
|
|
||||||
if (std.ascii.startsWithIgnoreCase(data, "Aug")) return 8;
|
|
||||||
if (std.ascii.startsWithIgnoreCase(data, "Sep")) return 9;
|
|
||||||
if (std.ascii.startsWithIgnoreCase(data, "Oct")) return 10;
|
|
||||||
if (std.ascii.startsWithIgnoreCase(data, "Nov")) return 11;
|
|
||||||
if (std.ascii.startsWithIgnoreCase(data, "Dec")) return 12;
|
|
||||||
return error.InvalidMonth;
|
|
||||||
}
|
|
||||||
pub fn parseIso8601ToTimestamp(data: []const u8) !i64 {
|
pub fn parseIso8601ToTimestamp(data: []const u8) !i64 {
|
||||||
return try dateTimeToTimestamp(try parseIso8601ToDateTime(data));
|
return try dateTimeToTimestamp(try parseIso8601ToDateTime(data));
|
||||||
}
|
}
|
||||||
|
@ -229,144 +158,15 @@ fn endIsoState(current_state: IsoParsingState, date: *DateTime, prev_data: []con
|
||||||
}
|
}
|
||||||
return next_state;
|
return next_state;
|
||||||
}
|
}
|
||||||
pub fn dateTimeToTimestamp(datetime: DateTime) !i64 {
|
|
||||||
const epoch = DateTime{
|
pub fn dateTimeToTimestamp(datetime: DateTime) !zeit.Seconds {
|
||||||
.year = 1970,
|
return (try datetime.instant()).unixTimestamp();
|
||||||
.month = 1,
|
|
||||||
.day = 1,
|
|
||||||
.hour = 0,
|
|
||||||
.minute = 0,
|
|
||||||
.second = 0,
|
|
||||||
};
|
|
||||||
return secondsBetween(epoch, datetime);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const DateTimeToTimestampError = error{
|
const DateTimeToTimestampError = error{
|
||||||
DateTimeOutOfRange,
|
DateTimeOutOfRange,
|
||||||
};
|
};
|
||||||
|
|
||||||
fn secondsBetween(start: DateTime, end: DateTime) DateTimeToTimestampError!i64 {
|
|
||||||
try validateDatetime(start);
|
|
||||||
try validateDatetime(end);
|
|
||||||
if (end.year < start.year) return -1 * try secondsBetween(end, start);
|
|
||||||
if (start.month != 1 or
|
|
||||||
start.day != 1 or
|
|
||||||
start.hour != 0 or
|
|
||||||
start.minute != 0 or
|
|
||||||
start.second != 0)
|
|
||||||
{
|
|
||||||
const seconds_into_start_year = secondsFromBeginningOfYear(
|
|
||||||
start.year,
|
|
||||||
start.month,
|
|
||||||
start.day,
|
|
||||||
start.hour,
|
|
||||||
start.minute,
|
|
||||||
start.second,
|
|
||||||
);
|
|
||||||
const new_start = DateTime{
|
|
||||||
.year = start.year,
|
|
||||||
.month = 1,
|
|
||||||
.day = 1,
|
|
||||||
.hour = 0,
|
|
||||||
.minute = 0,
|
|
||||||
.second = 0,
|
|
||||||
};
|
|
||||||
return (try secondsBetween(new_start, end)) - seconds_into_start_year;
|
|
||||||
}
|
|
||||||
const leap_years_between = leapYearsBetween(start.year, end.year);
|
|
||||||
const add_days: u1 = 0;
|
|
||||||
const years_diff = end.year - start.year;
|
|
||||||
// log.debug("Years from epoch: {d}, Leap years: {d}", .{ years_diff, leap_years_between });
|
|
||||||
const days_diff: i32 = (years_diff * DAYS_PER_YEAR) + leap_years_between + add_days;
|
|
||||||
// log.debug("Days with leap year, without month: {d}", .{days_diff});
|
|
||||||
|
|
||||||
const seconds_into_year = secondsFromBeginningOfYear(
|
|
||||||
end.year,
|
|
||||||
end.month,
|
|
||||||
end.day,
|
|
||||||
end.hour,
|
|
||||||
end.minute,
|
|
||||||
end.second,
|
|
||||||
);
|
|
||||||
return (days_diff * SECONDS_PER_DAY) + @as(i64, seconds_into_year);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn validateDatetime(dt: DateTime) !void {
|
|
||||||
if (dt.month > 12 or
|
|
||||||
dt.day > 31 or
|
|
||||||
dt.hour >= 24 or
|
|
||||||
dt.minute >= 60 or
|
|
||||||
dt.second >= 60) return error.DateTimeOutOfRange;
|
|
||||||
}
|
|
||||||
|
|
||||||
fn secondsFromBeginningOfYear(year: u16, month: u8, day: u8, hour: u8, minute: u8, second: u8) u32 {
|
|
||||||
const current_year_is_leap_year = isLeapYear(year);
|
|
||||||
const leap_year_days_per_month: [12]u5 = .{ 31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 };
|
|
||||||
const normal_days_per_month: [12]u5 = .{ 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 };
|
|
||||||
const days_per_month = if (current_year_is_leap_year) leap_year_days_per_month else normal_days_per_month;
|
|
||||||
var current_month: usize = 1;
|
|
||||||
const end_month = month;
|
|
||||||
var days_diff: u32 = 0;
|
|
||||||
while (current_month != end_month) {
|
|
||||||
days_diff += days_per_month[current_month - 1]; // months are 1-based vs array is 0-based
|
|
||||||
current_month += 1;
|
|
||||||
}
|
|
||||||
// log.debug("Days with month, without day: {d}. Day of month {d}, will add {d} days", .{
|
|
||||||
// days_diff,
|
|
||||||
// day,
|
|
||||||
// day - 1,
|
|
||||||
// });
|
|
||||||
// We need -1 because we're not actually including the ending day (that's up to hour/minute)
|
|
||||||
// In other words, days in the month are 1-based, while hours/minutes are zero based
|
|
||||||
days_diff += day - 1;
|
|
||||||
// log.debug("Total days diff: {d}", .{days_diff});
|
|
||||||
var seconds_diff: u32 = days_diff * SECONDS_PER_DAY;
|
|
||||||
|
|
||||||
// From here out, we want to get everything into seconds
|
|
||||||
seconds_diff += @as(u32, hour) * 60 * 60;
|
|
||||||
seconds_diff += @as(u32, minute) * 60;
|
|
||||||
seconds_diff += @as(u32, second);
|
|
||||||
|
|
||||||
return seconds_diff;
|
|
||||||
}
|
|
||||||
fn isLeapYear(year: u16) bool {
|
|
||||||
if (year % 4 != 0) return false;
|
|
||||||
if (year % 400 == 0) return true;
|
|
||||||
if (year % 100 == 0) return false;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
fn leapYearsBetween(start_year_inclusive: u16, end_year_exclusive: u16) u16 {
|
|
||||||
const start = @min(start_year_inclusive, end_year_exclusive);
|
|
||||||
const end = @max(start_year_inclusive, end_year_exclusive);
|
|
||||||
var current = start;
|
|
||||||
// log.debug("Leap years starting from {d}, ending at {d}", .{ start, end });
|
|
||||||
while (current % 4 != 0 and current < end) {
|
|
||||||
current += 1;
|
|
||||||
}
|
|
||||||
if (current == end) return 0; // No leap years here. E.g. 1971-1973
|
|
||||||
// We're on a potential leap year, and now we can step by 4
|
|
||||||
var rc: u16 = 0;
|
|
||||||
while (current < end) {
|
|
||||||
if (current % 4 == 0) {
|
|
||||||
if (current % 100 != 0) {
|
|
||||||
// log.debug("Year {d} is leap year", .{current});
|
|
||||||
rc += 1;
|
|
||||||
current += 4;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
// We're on a century, which is normally not a leap year, unless
|
|
||||||
// it's divisible by 400
|
|
||||||
if (current % 400 == 0) {
|
|
||||||
// log.debug("Year {d} is leap year", .{current});
|
|
||||||
rc += 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
current += 4;
|
|
||||||
}
|
|
||||||
return rc;
|
|
||||||
}
|
|
||||||
|
|
||||||
fn printDateTime(dt: DateTime) void {
|
fn printDateTime(dt: DateTime) void {
|
||||||
log.debug("{:0>4}-{:0>2}-{:0>2}T{:0>2}:{:0>2}:{:0<2}Z", .{
|
log.debug("{:0>4}-{:0>2}-{:0>2}T{:0>2}:{:0>2}:{:0<2}Z", .{
|
||||||
dt.year,
|
dt.year,
|
||||||
|
|
Loading…
Add table
Reference in a new issue