Compare commits
10 commits
536228c9ee
...
10fe374ea7
Author | SHA1 | Date | |
---|---|---|---|
10fe374ea7 | |||
8f4f120a3b | |||
95c8113136 | |||
![]() |
2d2b99c954 | ||
9547839328 | |||
1b56c10dc2 | |||
54251ea116 | |||
8ab36acb45 | |||
2578b0f4fa | |||
61757c9336 |
6 changed files with 574 additions and 402 deletions
25
build.zig
25
build.zig
|
@ -1,17 +1,26 @@
|
||||||
const std = @import("std");
|
const std = @import("std");
|
||||||
|
|
||||||
pub fn build(b: *std.build.Builder) void {
|
pub fn build(b: *std.Build) void {
|
||||||
// Standard release options allow the person running `zig build` to select
|
// Standard release options allow the person running `zig build` to select
|
||||||
// between Debug, ReleaseSafe, ReleaseFast, and ReleaseSmall.
|
// between Debug, ReleaseSafe, ReleaseFast, and ReleaseSmall.
|
||||||
const mode = b.standardReleaseOptions();
|
const target = b.standardTargetOptions(.{});
|
||||||
|
const optimize = b.standardOptimizeOption(.{});
|
||||||
|
|
||||||
const lib = b.addStaticLibrary("chrono", "src/main.zig");
|
const lib = b.addStaticLibrary(.{
|
||||||
lib.setBuildMode(mode);
|
.name = "chrono",
|
||||||
lib.install();
|
.root_source_file = .{ .path = "src/main.zig" },
|
||||||
|
.target = target,
|
||||||
|
.optimize = optimize,
|
||||||
|
});
|
||||||
|
b.installArtifact(lib);
|
||||||
|
|
||||||
const main_tests = b.addTest("src/main.zig");
|
const main_tests = b.addTest(.{
|
||||||
main_tests.setBuildMode(mode);
|
.root_source_file = .{ .path = "src/tests.zig" },
|
||||||
|
.target = target,
|
||||||
|
.optimize = optimize,
|
||||||
|
});
|
||||||
|
const run_main_tests = b.addRunArtifact(main_tests);
|
||||||
|
|
||||||
const test_step = b.step("test", "Run library tests");
|
const test_step = b.step("test", "Run library tests");
|
||||||
test_step.dependOn(&main_tests.step);
|
test_step.dependOn(&run_main_tests.step);
|
||||||
}
|
}
|
||||||
|
|
456
src/main.zig
456
src/main.zig
|
@ -1,245 +1,14 @@
|
||||||
const std = @import("std");
|
const std = @import("std");
|
||||||
const debug = std.debug;
|
pub const Year = @import("year.zig").Year;
|
||||||
const testing = std.testing;
|
pub const Month = @import("month.zig").Month;
|
||||||
|
pub const tz = @import("timezone.zig");
|
||||||
|
pub const Offset = tz.Offset;
|
||||||
|
pub const TimeZone = tz.TimeZone;
|
||||||
|
|
||||||
pub const SECONDS_PER_MINUTE = 60;
|
pub const SECONDS_PER_MINUTE = 60;
|
||||||
pub const SECONDS_PER_HOUR = 60 * 60;
|
pub const SECONDS_PER_HOUR = 60 * 60;
|
||||||
pub const SECONDS_PER_DAY = SECONDS_PER_HOUR * 24;
|
pub const SECONDS_PER_DAY = SECONDS_PER_HOUR * 24;
|
||||||
|
|
||||||
pub const YearTag = enum(u1) {
|
|
||||||
normal,
|
|
||||||
leap,
|
|
||||||
|
|
||||||
fn new(year: i32) YearTag {
|
|
||||||
return if (@rem(year, 4) == 0 and (@rem(year, 100) != 0 or @rem(year, 400) == 0)) .leap else .normal;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
pub const Year = union(YearTag) {
|
|
||||||
normal: i32,
|
|
||||||
leap: i32,
|
|
||||||
|
|
||||||
const Self = @This();
|
|
||||||
|
|
||||||
pub fn new(year: i32) Self {
|
|
||||||
return switch (YearTag.new(year)) {
|
|
||||||
.normal => Self{ .normal = year },
|
|
||||||
.leap => Self{ .leap = year },
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn days(self: Self) u16 {
|
|
||||||
return switch (self) {
|
|
||||||
.normal => 365,
|
|
||||||
.leap => 366,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn seconds(self: Self) i64 {
|
|
||||||
return @as(i64, self.days()) * SECONDS_PER_DAY;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get(self: Self) i32 {
|
|
||||||
return switch (self) {
|
|
||||||
.normal => |year| year,
|
|
||||||
.leap => |year| year,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn next(self: Self) Self {
|
|
||||||
return Self.new(self.get() + 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn previous(self: Self) Self {
|
|
||||||
return Self.new(self.get() - 1);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
test "new year" {
|
|
||||||
try testing.expectEqual(Year.new(2023), Year{ .normal = 2023 });
|
|
||||||
try testing.expectEqual(Year.new(2024), Year{ .leap = 2024 });
|
|
||||||
debug.print("Passed\n", .{});
|
|
||||||
}
|
|
||||||
|
|
||||||
test "get year" {
|
|
||||||
try testing.expectEqual(Year.new(2023).get(), 2023);
|
|
||||||
try testing.expectEqual(Year.new(2024).get(), 2024);
|
|
||||||
debug.print("Passed\n", .{});
|
|
||||||
}
|
|
||||||
|
|
||||||
test "next year" {
|
|
||||||
try testing.expectEqual(Year.new(2023).next(), Year{ .leap = 2024 });
|
|
||||||
debug.print("Passed\n", .{});
|
|
||||||
}
|
|
||||||
|
|
||||||
test "last year" {
|
|
||||||
try testing.expectEqual(Year.new(2024).previous(), Year{ .normal = 2023 });
|
|
||||||
debug.print("Passed\n", .{});
|
|
||||||
}
|
|
||||||
|
|
||||||
pub const Month = enum(u4) {
|
|
||||||
january = 1,
|
|
||||||
february = 2,
|
|
||||||
march = 3,
|
|
||||||
april = 4,
|
|
||||||
may = 5,
|
|
||||||
june = 6,
|
|
||||||
july = 7,
|
|
||||||
august = 8,
|
|
||||||
september = 9,
|
|
||||||
october = 10,
|
|
||||||
november = 11,
|
|
||||||
december = 12,
|
|
||||||
|
|
||||||
const Self = @This();
|
|
||||||
|
|
||||||
pub fn days(self: Self, year: Year) u5 {
|
|
||||||
return switch (@enumToInt(self)) {
|
|
||||||
1, 3, 5, 7, 8, 10, 12 => 31,
|
|
||||||
2 => switch (year) {
|
|
||||||
.normal => 28,
|
|
||||||
.leap => 29,
|
|
||||||
},
|
|
||||||
else => 30,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn seconds(self: Self, year: Year) u32 {
|
|
||||||
return @as(u32, self.days(year)) * SECONDS_PER_DAY;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn next(self: Self) ?Self {
|
|
||||||
const num = @enumToInt(self);
|
|
||||||
return if (num < 12) @intToEnum(Self, num + 1) else null;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn previous(self: Self) ?Self {
|
|
||||||
const num = @enumToInt(self);
|
|
||||||
return if (num > 1) @intToEnum(Self, num - 1) else null;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
test "get days in month" {
|
|
||||||
const year = Year.new(2023);
|
|
||||||
const month = Month.february;
|
|
||||||
try testing.expectEqual(month.days(year), 28);
|
|
||||||
debug.print("Passed\n", .{});
|
|
||||||
}
|
|
||||||
|
|
||||||
test "next month" {
|
|
||||||
try testing.expectEqual(Month.june.next(), .july);
|
|
||||||
try testing.expectEqual(Month.december.next(), null);
|
|
||||||
debug.print("Passed\n", .{});
|
|
||||||
}
|
|
||||||
|
|
||||||
test "last month" {
|
|
||||||
try testing.expectEqual(Month.june.previous(), .may);
|
|
||||||
try testing.expectEqual(Month.january.previous(), null);
|
|
||||||
debug.print("Passed\n", .{});
|
|
||||||
}
|
|
||||||
|
|
||||||
pub const TimeZoneTag = enum(u1) {
|
|
||||||
utc,
|
|
||||||
offset,
|
|
||||||
};
|
|
||||||
|
|
||||||
pub const Sign = enum(u1) {
|
|
||||||
positive,
|
|
||||||
negative,
|
|
||||||
};
|
|
||||||
|
|
||||||
const HoursMinutes = struct {
|
|
||||||
hours: u4,
|
|
||||||
minutes: ?u6,
|
|
||||||
};
|
|
||||||
|
|
||||||
pub const Offset = union(Sign) {
|
|
||||||
positive: HoursMinutes,
|
|
||||||
negative: HoursMinutes,
|
|
||||||
|
|
||||||
const Self = @This();
|
|
||||||
|
|
||||||
fn new(hours: i8, minutes: ?u6) ?Self {
|
|
||||||
if (hours > 12 or hours < -12) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
if (minutes) |m| {
|
|
||||||
if (m > 59) return null;
|
|
||||||
if (hours == 0 and m == 0) return null;
|
|
||||||
} else if (hours == 0) return null;
|
|
||||||
if (hours < 0) {
|
|
||||||
const h = @intCast(u4, @as(i8, hours) * -1);
|
|
||||||
return Self{ .negative = .{ .hours = h, .minutes = minutes } };
|
|
||||||
} else {
|
|
||||||
return Self{ .positive = .{ .hours = @intCast(u4, hours), .minutes = minutes } };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn asSeconds(self: Self) i64 {
|
|
||||||
return switch (self) {
|
|
||||||
.positive => |ofs| blk: {
|
|
||||||
var seconds = @as(i64, ofs.hours) * 3600;
|
|
||||||
if (ofs.minutes) |m| seconds += (@as(i64, m) * 60);
|
|
||||||
break :blk seconds;
|
|
||||||
},
|
|
||||||
.negative => |ofs| blk: {
|
|
||||||
var seconds = @as(i64, ofs.hours) * 3600;
|
|
||||||
if (ofs.minutes) |m| seconds += (@as(i64, m) * 60);
|
|
||||||
break :blk seconds * -1;
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
test "new offsets" {
|
|
||||||
try testing.expectEqual(Offset.new(-5, null), Offset{ .negative = .{ .hours = 5, .minutes = null } });
|
|
||||||
try testing.expectEqual(Offset.new(3, null), Offset{ .positive = .{ .hours = 3, .minutes = null } });
|
|
||||||
debug.print("Passed\n", .{});
|
|
||||||
}
|
|
||||||
|
|
||||||
test "as seconds" {
|
|
||||||
try testing.expectEqual(Offset.new(-4, 30).?.asSeconds(), -16200);
|
|
||||||
try testing.expectEqual(Offset.new(3, null).?.asSeconds(), 10800);
|
|
||||||
debug.print("Passed\n", .{});
|
|
||||||
}
|
|
||||||
|
|
||||||
pub const TimeZone = union(TimeZoneTag) {
|
|
||||||
utc: void,
|
|
||||||
offset: Offset,
|
|
||||||
|
|
||||||
const Self = @This();
|
|
||||||
|
|
||||||
pub fn new(hours: ?i8, minutes: ?u6) ?Self {
|
|
||||||
return if (hours) |h| blk: {
|
|
||||||
if (h == 0) {
|
|
||||||
break :blk .utc;
|
|
||||||
} else if (Offset.new(h, minutes)) |ofs| {
|
|
||||||
break :blk Self{ .offset = ofs };
|
|
||||||
} else {
|
|
||||||
break :blk null;
|
|
||||||
}
|
|
||||||
} else if (minutes) |m| Self{ .offset = Offset.new(0, m).? } else .utc;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
test "new timezone" {
|
|
||||||
const tz = TimeZone.new(-5, null).?;
|
|
||||||
try testing.expectEqual(@as(TimeZoneTag, tz), .offset);
|
|
||||||
switch (tz) {
|
|
||||||
.offset => |ofs| try testing.expectEqual(ofs, Offset{ .negative = .{ .hours = 5, .minutes = null } }),
|
|
||||||
else => unreachable,
|
|
||||||
}
|
|
||||||
debug.print("Passed\n", .{});
|
|
||||||
}
|
|
||||||
|
|
||||||
test "new timezone utc" {
|
|
||||||
const tz0 = TimeZone.new(null, null).?;
|
|
||||||
const tz1 = TimeZone.new(0, null).?;
|
|
||||||
try testing.expectEqual(@as(TimeZoneTag, tz0), .utc);
|
|
||||||
try testing.expectEqual(@as(TimeZoneTag, tz1), .utc);
|
|
||||||
debug.print("Passed\n", .{});
|
|
||||||
}
|
|
||||||
|
|
||||||
pub const WeekDay = enum(u3) {
|
pub const WeekDay = enum(u3) {
|
||||||
thursday = 0,
|
thursday = 0,
|
||||||
friday,
|
friday,
|
||||||
|
@ -278,7 +47,7 @@ pub const DateTime = struct {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn toTimestamp(self: Self) i64 {
|
fn toTimestampNaive(self: Self) i64 {
|
||||||
var seconds: i64 = 0;
|
var seconds: i64 = 0;
|
||||||
if (self.year.get() < 1970) {
|
if (self.year.get() < 1970) {
|
||||||
var year = Year.new(1970);
|
var year = Year.new(1970);
|
||||||
|
@ -310,6 +79,11 @@ pub const DateTime = struct {
|
||||||
if (self.second) |s| {
|
if (self.second) |s| {
|
||||||
seconds += s;
|
seconds += s;
|
||||||
}
|
}
|
||||||
|
return seconds;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn toTimestamp(self: Self) i64 {
|
||||||
|
var seconds = self.toTimestampNaive();
|
||||||
if (self.getOffset()) |ofs| seconds -= ofs.asSeconds();
|
if (self.getOffset()) |ofs| seconds -= ofs.asSeconds();
|
||||||
return seconds;
|
return seconds;
|
||||||
}
|
}
|
||||||
|
@ -345,7 +119,7 @@ pub const DateTime = struct {
|
||||||
seconds -= 60;
|
seconds -= 60;
|
||||||
minutes -= 1;
|
minutes -= 1;
|
||||||
}
|
}
|
||||||
const second = @intCast(u6, seconds + 60);
|
const second = @as(u6, @intCast(seconds + 60));
|
||||||
return Self{
|
return Self{
|
||||||
.year = year,
|
.year = year,
|
||||||
.month = month.?,
|
.month = month.?,
|
||||||
|
@ -376,15 +150,15 @@ pub const DateTime = struct {
|
||||||
return Self{
|
return Self{
|
||||||
.year = year,
|
.year = year,
|
||||||
.month = month,
|
.month = month,
|
||||||
.day = @intCast(u8, day),
|
.day = @as(u8, @intCast(day)),
|
||||||
.hour = @intCast(u5, hour),
|
.hour = @as(u5, @intCast(hour)),
|
||||||
.minute = @intCast(u6, minute),
|
.minute = @as(u6, @intCast(minute)),
|
||||||
.second = @intCast(u6, seconds),
|
.second = @as(u6, @intCast(seconds)),
|
||||||
.tz = .utc,
|
.tz = .utc,
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
return Self{
|
return Self{
|
||||||
.year = Year.new(0),
|
.year = Year.new(1970),
|
||||||
.month = .january,
|
.month = .january,
|
||||||
.day = 1,
|
.day = 1,
|
||||||
.hour = 0,
|
.hour = 0,
|
||||||
|
@ -400,9 +174,9 @@ pub const DateTime = struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn weekday(self: Self) WeekDay {
|
pub fn weekday(self: Self) WeekDay {
|
||||||
const ts = self.toTimestamp();
|
const ts = self.toTimestampNaive();
|
||||||
const days = @divTrunc(ts, SECONDS_PER_DAY);
|
const days = @divTrunc(ts, SECONDS_PER_DAY);
|
||||||
return @intToEnum(WeekDay, @rem(days, 7));
|
return @as(WeekDay, @enumFromInt(@rem(days, 7)));
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn compare(self: Self, other: Self) Comparison {
|
pub fn compare(self: Self, other: Self) Comparison {
|
||||||
|
@ -410,153 +184,47 @@ pub const DateTime = struct {
|
||||||
const b = other.toTimestamp();
|
const b = other.toTimestamp();
|
||||||
return if (a > b) .gt else if (a < b) .lt else .eq;
|
return if (a > b) .gt else if (a < b) .lt else .eq;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn format(
|
||||||
|
self: Self,
|
||||||
|
comptime fmt: []const u8,
|
||||||
|
options: std.fmt.FormatOptions,
|
||||||
|
writer: anytype,
|
||||||
|
) !void {
|
||||||
|
_ = fmt;
|
||||||
|
_ = options;
|
||||||
|
|
||||||
|
try writer.print("{s}-{d:0>2}-{d:0>2}", .{
|
||||||
|
self.year, @intFromEnum(self.month), self.day,
|
||||||
|
});
|
||||||
|
if (self.hour) |h| {
|
||||||
|
try writer.print("T{d:0>2}", .{h});
|
||||||
|
if (self.minute) |m| {
|
||||||
|
try writer.print(":{d:0>2}", .{m});
|
||||||
|
if (self.second) |s| {
|
||||||
|
try writer.print(":{d:0>2}", .{s});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
try writer.print("{s}", .{self.tz});
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn format_basic(
|
||||||
|
self: Self,
|
||||||
|
writer: anytype,
|
||||||
|
) !void {
|
||||||
|
try writer.print("{s}{d:0>2}{d:0>2}", .{
|
||||||
|
self.year, @intFromEnum(self.month), self.day,
|
||||||
|
});
|
||||||
|
if (self.hour) |h| {
|
||||||
|
try writer.print("T{d:0>2}", .{h});
|
||||||
|
if (self.minute) |m| {
|
||||||
|
try writer.print("{d:0>2}", .{m});
|
||||||
|
if (self.second) |s| {
|
||||||
|
try writer.print("{d:0>2}", .{s});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
try self.tz.format_basic(writer);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
test "get year" {
|
|
||||||
const dt = DateTime{
|
|
||||||
.year = Year.new(2023),
|
|
||||||
.month = .june,
|
|
||||||
.day = 12,
|
|
||||||
.hour = 1,
|
|
||||||
.minute = 5,
|
|
||||||
.second = 14,
|
|
||||||
.tz = TimeZone.new(-5, null).?,
|
|
||||||
};
|
|
||||||
try testing.expectEqual(dt.getYear(), 2023);
|
|
||||||
debug.print("Passed\n", .{});
|
|
||||||
}
|
|
||||||
|
|
||||||
test "get offset" {
|
|
||||||
const dt = DateTime{
|
|
||||||
.year = Year.new(2023),
|
|
||||||
.month = .june,
|
|
||||||
.day = 12,
|
|
||||||
.hour = 1,
|
|
||||||
.minute = 5,
|
|
||||||
.second = 14,
|
|
||||||
.tz = TimeZone.new(-5, null).?,
|
|
||||||
};
|
|
||||||
try testing.expectEqual(dt.getOffset().?, Offset{ .negative = .{ .hours = 5, .minutes = null } });
|
|
||||||
debug.print("Passed\n", .{});
|
|
||||||
}
|
|
||||||
|
|
||||||
test "to timestamp utc" {
|
|
||||||
const dt = DateTime{
|
|
||||||
.year = Year.new(2023),
|
|
||||||
.month = .june,
|
|
||||||
.day = 13,
|
|
||||||
.hour = 5,
|
|
||||||
.minute = 21,
|
|
||||||
.second = 22,
|
|
||||||
.tz = .utc,
|
|
||||||
};
|
|
||||||
try testing.expectEqual(dt.toTimestamp(), 1686633682);
|
|
||||||
debug.print("Passed\n", .{});
|
|
||||||
}
|
|
||||||
|
|
||||||
test "to timestamp negative offset" {
|
|
||||||
const dt = DateTime{
|
|
||||||
.year = Year.new(2023),
|
|
||||||
.month = .june,
|
|
||||||
.day = 13,
|
|
||||||
.hour = 0,
|
|
||||||
.minute = 21,
|
|
||||||
.second = 22,
|
|
||||||
.tz = TimeZone.new(-5, null).?,
|
|
||||||
};
|
|
||||||
try testing.expectEqual(dt.toTimestamp(), 1686633682);
|
|
||||||
debug.print("Passed\n", .{});
|
|
||||||
}
|
|
||||||
|
|
||||||
test "conversions" {
|
|
||||||
const ts = std.time.timestamp();
|
|
||||||
const dt = DateTime.fromTimestamp(ts);
|
|
||||||
try testing.expectEqual(dt.toTimestamp(), ts);
|
|
||||||
debug.print("Passed\n", .{});
|
|
||||||
}
|
|
||||||
|
|
||||||
test "now" {
|
|
||||||
const ts = std.time.timestamp();
|
|
||||||
const dt = DateTime.now();
|
|
||||||
try testing.expect(ts <= dt.toTimestamp());
|
|
||||||
// Make sure they're identical at least to the tens place, since it's possible
|
|
||||||
// for them to vary by a second or more depending on system resources
|
|
||||||
const a = @divTrunc(ts, 10);
|
|
||||||
const b = @divTrunc(dt.toTimestamp(), 10);
|
|
||||||
try testing.expectEqual(a, b);
|
|
||||||
debug.print("Passed\n", .{});
|
|
||||||
}
|
|
||||||
|
|
||||||
test "get weekday" {
|
|
||||||
const dt = DateTime{
|
|
||||||
.year = Year.new(2023),
|
|
||||||
.month = .june,
|
|
||||||
.day = 13,
|
|
||||||
.hour = 6,
|
|
||||||
.minute = 21,
|
|
||||||
.second = 22,
|
|
||||||
.tz = .utc,
|
|
||||||
};
|
|
||||||
try testing.expectEqual(dt.weekday(), .tuesday);
|
|
||||||
debug.print("Passed\n", .{});
|
|
||||||
}
|
|
||||||
|
|
||||||
test "get weekday 2" {
|
|
||||||
const dt = DateTime{
|
|
||||||
.year = Year.new(2023),
|
|
||||||
.month = .june,
|
|
||||||
.day = 10,
|
|
||||||
.hour = 6,
|
|
||||||
.minute = 21,
|
|
||||||
.second = 22,
|
|
||||||
.tz = .utc,
|
|
||||||
};
|
|
||||||
try testing.expectEqual(dt.weekday(), .saturday);
|
|
||||||
debug.print("Passed\n", .{});
|
|
||||||
}
|
|
||||||
|
|
||||||
test "ordering lt" {
|
|
||||||
const a = DateTime{
|
|
||||||
.year = Year.new(2023),
|
|
||||||
.month = .june,
|
|
||||||
.day = 10,
|
|
||||||
.hour = 6,
|
|
||||||
.minute = 21,
|
|
||||||
.second = 22,
|
|
||||||
.tz = .utc,
|
|
||||||
};
|
|
||||||
const b = DateTime{
|
|
||||||
.year = Year.new(2023),
|
|
||||||
.month = .june,
|
|
||||||
.day = 10,
|
|
||||||
.hour = 6,
|
|
||||||
.minute = 21,
|
|
||||||
.second = 23,
|
|
||||||
.tz = .utc,
|
|
||||||
};
|
|
||||||
try testing.expectEqual(a.compare(b), .lt);
|
|
||||||
debug.print("Passed\n", .{});
|
|
||||||
}
|
|
||||||
|
|
||||||
test "ordering gt" {
|
|
||||||
const a = DateTime{
|
|
||||||
.year = Year.new(2023),
|
|
||||||
.month = .june,
|
|
||||||
.day = 10,
|
|
||||||
.hour = 6,
|
|
||||||
.minute = 21,
|
|
||||||
.second = 22,
|
|
||||||
.tz = .utc,
|
|
||||||
};
|
|
||||||
const b = DateTime{
|
|
||||||
.year = Year.new(2023),
|
|
||||||
.month = .june,
|
|
||||||
.day = 10,
|
|
||||||
.hour = 6,
|
|
||||||
.minute = 21,
|
|
||||||
.second = 22,
|
|
||||||
.tz = TimeZone.new(1, null).?,
|
|
||||||
};
|
|
||||||
try testing.expectEqual(a.compare(b), .gt);
|
|
||||||
debug.print("Passed\n", .{});
|
|
||||||
}
|
|
||||||
|
|
47
src/month.zig
Normal file
47
src/month.zig
Normal file
|
@ -0,0 +1,47 @@
|
||||||
|
const std = @import("std");
|
||||||
|
const debug = std.debug;
|
||||||
|
const testing = std.testing;
|
||||||
|
const Year = @import("year.zig").Year;
|
||||||
|
const SECONDS_PER_DAY = @import("main.zig").SECONDS_PER_DAY;
|
||||||
|
|
||||||
|
pub const Month = enum(u4) {
|
||||||
|
january = 1,
|
||||||
|
february = 2,
|
||||||
|
march = 3,
|
||||||
|
april = 4,
|
||||||
|
may = 5,
|
||||||
|
june = 6,
|
||||||
|
july = 7,
|
||||||
|
august = 8,
|
||||||
|
september = 9,
|
||||||
|
october = 10,
|
||||||
|
november = 11,
|
||||||
|
december = 12,
|
||||||
|
|
||||||
|
const Self = @This();
|
||||||
|
|
||||||
|
pub fn days(self: Self, year: Year) u5 {
|
||||||
|
return switch (@intFromEnum(self)) {
|
||||||
|
1, 3, 5, 7, 8, 10, 12 => 31,
|
||||||
|
2 => switch (year) {
|
||||||
|
.normal => 28,
|
||||||
|
.leap => 29,
|
||||||
|
},
|
||||||
|
else => 30,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn seconds(self: Self, year: Year) u32 {
|
||||||
|
return @as(u32, self.days(year)) * SECONDS_PER_DAY;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn next(self: Self) ?Self {
|
||||||
|
const num = @intFromEnum(self);
|
||||||
|
return if (num < 12) @enumFromInt(num + 1) else null;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn previous(self: Self) ?Self {
|
||||||
|
const num = @intFromEnum(self);
|
||||||
|
return if (num > 1) @enumFromInt(num - 1) else null;
|
||||||
|
}
|
||||||
|
};
|
252
src/tests.zig
Normal file
252
src/tests.zig
Normal file
|
@ -0,0 +1,252 @@
|
||||||
|
const std = @import("std");
|
||||||
|
const debug = std.debug;
|
||||||
|
const mem = std.mem;
|
||||||
|
const testing = std.testing;
|
||||||
|
const DateTime = @import("main.zig").DateTime;
|
||||||
|
const Year = @import("year.zig").Year;
|
||||||
|
const Month = @import("month.zig").Month;
|
||||||
|
const tz = @import("timezone.zig");
|
||||||
|
const Offset = tz.Offset;
|
||||||
|
const TimeZone = tz.TimeZone;
|
||||||
|
|
||||||
|
test "new year" {
|
||||||
|
try testing.expectEqual(Year.new(2023), Year{ .normal = 2023 });
|
||||||
|
try testing.expectEqual(Year.new(2024), Year{ .leap = 2024 });
|
||||||
|
}
|
||||||
|
|
||||||
|
test "get year" {
|
||||||
|
try testing.expectEqual(Year.new(2023).get(), 2023);
|
||||||
|
try testing.expectEqual(Year.new(2024).get(), 2024);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "next year" {
|
||||||
|
try testing.expectEqual(Year.new(2023).next(), Year{ .leap = 2024 });
|
||||||
|
}
|
||||||
|
|
||||||
|
test "last year" {
|
||||||
|
try testing.expectEqual(Year.new(2024).previous(), Year{ .normal = 2023 });
|
||||||
|
}
|
||||||
|
|
||||||
|
test "get days in month" {
|
||||||
|
const year = Year.new(2023);
|
||||||
|
const month = Month.february;
|
||||||
|
try testing.expectEqual(month.days(year), 28);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "next month" {
|
||||||
|
try testing.expectEqual(Month.june.next(), .july);
|
||||||
|
try testing.expectEqual(Month.december.next(), null);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "last month" {
|
||||||
|
try testing.expectEqual(Month.june.previous(), .may);
|
||||||
|
try testing.expectEqual(Month.january.previous(), null);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "new offsets" {
|
||||||
|
try testing.expectEqual(Offset.new(-5, null), Offset{ .negative = .{ .hours = 5, .minutes = null } });
|
||||||
|
try testing.expectEqual(Offset.new(3, null), Offset{ .positive = .{ .hours = 3, .minutes = null } });
|
||||||
|
}
|
||||||
|
|
||||||
|
test "as seconds" {
|
||||||
|
try testing.expectEqual(Offset.new(-4, 30).?.asSeconds(), -16200);
|
||||||
|
try testing.expectEqual(Offset.new(3, null).?.asSeconds(), 10800);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "new timezone" {
|
||||||
|
const zone = TimeZone.new(-5, null).?;
|
||||||
|
try testing.expectEqual(@as(tz.TimeZoneTag, zone), .offset);
|
||||||
|
switch (zone) {
|
||||||
|
.offset => |ofs| try testing.expectEqual(ofs, Offset{ .negative = .{ .hours = 5, .minutes = null } }),
|
||||||
|
else => unreachable,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
test "new timezone utc" {
|
||||||
|
const tz0 = TimeZone.new(null, null).?;
|
||||||
|
const tz1 = TimeZone.new(0, null).?;
|
||||||
|
try testing.expectEqual(@as(tz.TimeZoneTag, tz0), .utc);
|
||||||
|
try testing.expectEqual(@as(tz.TimeZoneTag, tz1), .utc);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "get year from DateTime" {
|
||||||
|
const dt = DateTime{
|
||||||
|
.year = Year.new(2023),
|
||||||
|
.month = .june,
|
||||||
|
.day = 12,
|
||||||
|
.hour = 1,
|
||||||
|
.minute = 5,
|
||||||
|
.second = 14,
|
||||||
|
.tz = TimeZone.new(-5, null).?,
|
||||||
|
};
|
||||||
|
try testing.expectEqual(dt.getYear(), 2023);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "get offset" {
|
||||||
|
const dt = DateTime{
|
||||||
|
.year = Year.new(2023),
|
||||||
|
.month = .june,
|
||||||
|
.day = 12,
|
||||||
|
.hour = 1,
|
||||||
|
.minute = 5,
|
||||||
|
.second = 14,
|
||||||
|
.tz = TimeZone.new(-5, null).?,
|
||||||
|
};
|
||||||
|
try testing.expectEqual(dt.getOffset().?, Offset{ .negative = .{ .hours = 5, .minutes = null } });
|
||||||
|
}
|
||||||
|
|
||||||
|
test "to timestamp utc" {
|
||||||
|
const dt = DateTime{
|
||||||
|
.year = Year.new(2023),
|
||||||
|
.month = .june,
|
||||||
|
.day = 13,
|
||||||
|
.hour = 5,
|
||||||
|
.minute = 21,
|
||||||
|
.second = 22,
|
||||||
|
.tz = .utc,
|
||||||
|
};
|
||||||
|
try testing.expectEqual(dt.toTimestamp(), 1686633682);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "to timestamp negative offset" {
|
||||||
|
const dt = DateTime{
|
||||||
|
.year = Year.new(2023),
|
||||||
|
.month = .june,
|
||||||
|
.day = 13,
|
||||||
|
.hour = 0,
|
||||||
|
.minute = 21,
|
||||||
|
.second = 22,
|
||||||
|
.tz = TimeZone.new(-5, null).?,
|
||||||
|
};
|
||||||
|
try testing.expectEqual(dt.toTimestamp(), 1686633682);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "conversions" {
|
||||||
|
const ts = std.time.timestamp();
|
||||||
|
const dt = DateTime.fromTimestamp(ts);
|
||||||
|
try testing.expectEqual(dt.toTimestamp(), ts);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "now" {
|
||||||
|
const ts = std.time.timestamp();
|
||||||
|
const dt = DateTime.now();
|
||||||
|
try testing.expect(ts <= dt.toTimestamp());
|
||||||
|
// Make sure they're identical at least to the tens place, since it's possible
|
||||||
|
// for them to vary by a second or more depending on system resources
|
||||||
|
const a = @divTrunc(ts, 10);
|
||||||
|
const b = @divTrunc(dt.toTimestamp(), 10);
|
||||||
|
try testing.expectEqual(a, b);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "get weekday" {
|
||||||
|
const dt = DateTime{
|
||||||
|
.year = Year.new(2023),
|
||||||
|
.month = .june,
|
||||||
|
.day = 13,
|
||||||
|
.hour = 6,
|
||||||
|
.minute = 21,
|
||||||
|
.second = 22,
|
||||||
|
.tz = .utc,
|
||||||
|
};
|
||||||
|
try testing.expectEqual(dt.weekday(), .tuesday);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "get weekday 2" {
|
||||||
|
const dt = DateTime{
|
||||||
|
.year = Year.new(2023),
|
||||||
|
.month = .june,
|
||||||
|
.day = 10,
|
||||||
|
.hour = 6,
|
||||||
|
.minute = 21,
|
||||||
|
.second = 22,
|
||||||
|
.tz = .utc,
|
||||||
|
};
|
||||||
|
try testing.expectEqual(dt.weekday(), .saturday);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "ordering lt" {
|
||||||
|
const a = DateTime{
|
||||||
|
.year = Year.new(2023),
|
||||||
|
.month = .june,
|
||||||
|
.day = 10,
|
||||||
|
.hour = 6,
|
||||||
|
.minute = 21,
|
||||||
|
.second = 22,
|
||||||
|
.tz = .utc,
|
||||||
|
};
|
||||||
|
const b = DateTime{
|
||||||
|
.year = Year.new(2023),
|
||||||
|
.month = .june,
|
||||||
|
.day = 10,
|
||||||
|
.hour = 6,
|
||||||
|
.minute = 21,
|
||||||
|
.second = 23,
|
||||||
|
.tz = .utc,
|
||||||
|
};
|
||||||
|
try testing.expectEqual(a.compare(b), .lt);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "ordering gt" {
|
||||||
|
const a = DateTime{
|
||||||
|
.year = Year.new(2023),
|
||||||
|
.month = .june,
|
||||||
|
.day = 10,
|
||||||
|
.hour = 6,
|
||||||
|
.minute = 21,
|
||||||
|
.second = 22,
|
||||||
|
.tz = .utc,
|
||||||
|
};
|
||||||
|
const b = DateTime{
|
||||||
|
.year = Year.new(2023),
|
||||||
|
.month = .june,
|
||||||
|
.day = 10,
|
||||||
|
.hour = 6,
|
||||||
|
.minute = 21,
|
||||||
|
.second = 22,
|
||||||
|
.tz = TimeZone.new(1, null).?,
|
||||||
|
};
|
||||||
|
try testing.expectEqual(a.compare(b), .gt);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "custom fmt" {
|
||||||
|
const dt = DateTime{
|
||||||
|
.year = Year.new(2023),
|
||||||
|
.month = .june,
|
||||||
|
.day = 10,
|
||||||
|
.hour = 6,
|
||||||
|
.minute = 21,
|
||||||
|
.second = 22,
|
||||||
|
.tz = .utc,
|
||||||
|
};
|
||||||
|
const dt_string = try std.fmt.allocPrint(
|
||||||
|
testing.allocator,
|
||||||
|
"{s}",
|
||||||
|
.{dt},
|
||||||
|
);
|
||||||
|
defer testing.allocator.free(dt_string);
|
||||||
|
try testing.expect(mem.eql(u8, dt_string, "2023-06-10T06:21:22Z"));
|
||||||
|
}
|
||||||
|
|
||||||
|
test "fmt basic" {
|
||||||
|
const dt = DateTime{
|
||||||
|
.year = Year.new(2023),
|
||||||
|
.month = .june,
|
||||||
|
.day = 10,
|
||||||
|
.hour = 6,
|
||||||
|
.minute = 21,
|
||||||
|
.second = 22,
|
||||||
|
.tz = TimeZone.new(-4, null).?,
|
||||||
|
};
|
||||||
|
var dt_array = std.ArrayList(u8).init(testing.allocator);
|
||||||
|
defer dt_array.deinit();
|
||||||
|
var writer = dt_array.writer();
|
||||||
|
try dt.format_basic(writer);
|
||||||
|
try testing.expect(mem.eql(u8, dt_array.items, "20230610T062122-04"));
|
||||||
|
const dt_string = try std.fmt.allocPrint(
|
||||||
|
testing.allocator,
|
||||||
|
"{s}",
|
||||||
|
.{dt},
|
||||||
|
);
|
||||||
|
defer testing.allocator.free(dt_string);
|
||||||
|
try testing.expect(mem.eql(u8, dt_string, "2023-06-10T06:21:22-04"));
|
||||||
|
}
|
126
src/timezone.zig
Normal file
126
src/timezone.zig
Normal file
|
@ -0,0 +1,126 @@
|
||||||
|
const std = @import("std");
|
||||||
|
const debug = std.debug;
|
||||||
|
const testing = std.testing;
|
||||||
|
|
||||||
|
pub const TimeZoneTag = enum(u1) {
|
||||||
|
utc,
|
||||||
|
offset,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const Sign = enum(u1) {
|
||||||
|
positive,
|
||||||
|
negative,
|
||||||
|
};
|
||||||
|
|
||||||
|
const HoursMinutes = struct {
|
||||||
|
hours: u4,
|
||||||
|
minutes: ?u6,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const Offset = union(Sign) {
|
||||||
|
positive: HoursMinutes,
|
||||||
|
negative: HoursMinutes,
|
||||||
|
|
||||||
|
const Self = @This();
|
||||||
|
|
||||||
|
pub fn new(hours: i8, minutes: ?u6) ?Self {
|
||||||
|
if (hours > 12 or hours < -12) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (minutes) |m| {
|
||||||
|
if (m > 59) return null;
|
||||||
|
if (hours == 0 and m == 0) return null;
|
||||||
|
} else if (hours == 0) return null;
|
||||||
|
if (hours < 0) {
|
||||||
|
const h: u4 = @intCast(@as(i8, hours) * -1);
|
||||||
|
return Self{ .negative = .{ .hours = h, .minutes = minutes } };
|
||||||
|
} else {
|
||||||
|
return Self{ .positive = .{ .hours = @intCast(hours), .minutes = minutes } };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn asSeconds(self: Self) i64 {
|
||||||
|
return switch (self) {
|
||||||
|
.positive => |ofs| blk: {
|
||||||
|
var seconds = @as(i64, ofs.hours) * 3600;
|
||||||
|
if (ofs.minutes) |m| seconds += (@as(i64, m) * 60);
|
||||||
|
break :blk seconds;
|
||||||
|
},
|
||||||
|
.negative => |ofs| blk: {
|
||||||
|
var seconds = @as(i64, ofs.hours) * 3600;
|
||||||
|
if (ofs.minutes) |m| seconds += (@as(i64, m) * 60);
|
||||||
|
break :blk seconds * -1;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const TimeZone = union(TimeZoneTag) {
|
||||||
|
utc: void,
|
||||||
|
offset: Offset,
|
||||||
|
|
||||||
|
const Self = @This();
|
||||||
|
|
||||||
|
pub fn new(hours: ?i8, minutes: ?u6) ?Self {
|
||||||
|
return if (hours) |h| blk: {
|
||||||
|
if (h == 0) {
|
||||||
|
break :blk .utc;
|
||||||
|
} else if (Offset.new(h, minutes)) |ofs| {
|
||||||
|
break :blk Self{ .offset = ofs };
|
||||||
|
} else {
|
||||||
|
break :blk null;
|
||||||
|
}
|
||||||
|
} else if (minutes) |m| Self{ .offset = Offset.new(0, m).? } else .utc;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn format(
|
||||||
|
self: Self,
|
||||||
|
comptime fmt: []const u8,
|
||||||
|
options: std.fmt.FormatOptions,
|
||||||
|
writer: anytype,
|
||||||
|
) !void {
|
||||||
|
_ = fmt;
|
||||||
|
_ = options;
|
||||||
|
|
||||||
|
switch (self) {
|
||||||
|
.utc => try writer.writeAll("Z"),
|
||||||
|
.offset => |ofs| switch (ofs) {
|
||||||
|
.positive => |p| {
|
||||||
|
try writer.print("+{d:0>2}", .{p.hours});
|
||||||
|
if (p.minutes) |m| {
|
||||||
|
try writer.print(":{d:0>2}", .{m});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
.negative => |n| {
|
||||||
|
try writer.print("-{d:0>2}", .{n.hours});
|
||||||
|
if (n.minutes) |m| {
|
||||||
|
try writer.print(":{d:0>2}", .{m});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn format_basic(
|
||||||
|
self: Self,
|
||||||
|
writer: anytype,
|
||||||
|
) !void {
|
||||||
|
switch (self) {
|
||||||
|
.utc => try writer.writeAll("Z"),
|
||||||
|
.offset => |ofs| switch (ofs) {
|
||||||
|
.positive => |p| {
|
||||||
|
try writer.print("+{d:0>2}", .{p.hours});
|
||||||
|
if (p.minutes) |m| {
|
||||||
|
try writer.print("{d:0>2}", .{m});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
.negative => |n| {
|
||||||
|
try writer.print("-{d:0>2}", .{n.hours});
|
||||||
|
if (n.minutes) |m| {
|
||||||
|
try writer.print("{d:0>2}", .{m});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
70
src/year.zig
Normal file
70
src/year.zig
Normal file
|
@ -0,0 +1,70 @@
|
||||||
|
const std = @import("std");
|
||||||
|
const debug = std.debug;
|
||||||
|
const testing = std.testing;
|
||||||
|
const SECONDS_PER_DAY = @import("main.zig").SECONDS_PER_DAY;
|
||||||
|
|
||||||
|
pub const YearTag = enum(u1) {
|
||||||
|
normal,
|
||||||
|
leap,
|
||||||
|
|
||||||
|
fn new(year: i32) YearTag {
|
||||||
|
return if (@rem(year, 4) == 0 and (@rem(year, 100) != 0 or @rem(year, 400) == 0)) .leap else .normal;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const Year = union(YearTag) {
|
||||||
|
normal: i32,
|
||||||
|
leap: i32,
|
||||||
|
|
||||||
|
const Self = @This();
|
||||||
|
|
||||||
|
pub fn new(year: i32) Self {
|
||||||
|
return switch (YearTag.new(year)) {
|
||||||
|
.normal => Self{ .normal = year },
|
||||||
|
.leap => Self{ .leap = year },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn days(self: Self) u16 {
|
||||||
|
return switch (self) {
|
||||||
|
.normal => 365,
|
||||||
|
.leap => 366,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn seconds(self: Self) i64 {
|
||||||
|
return @as(i64, self.days()) * SECONDS_PER_DAY;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get(self: Self) i32 {
|
||||||
|
return switch (self) {
|
||||||
|
.normal => |year| year,
|
||||||
|
.leap => |year| year,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn next(self: Self) Self {
|
||||||
|
return Self.new(self.get() + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn previous(self: Self) Self {
|
||||||
|
return Self.new(self.get() - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn format(
|
||||||
|
self: Self,
|
||||||
|
comptime fmt: []const u8,
|
||||||
|
options: std.fmt.FormatOptions,
|
||||||
|
writer: anytype,
|
||||||
|
) !void {
|
||||||
|
_ = fmt;
|
||||||
|
_ = options;
|
||||||
|
|
||||||
|
const year = self.get();
|
||||||
|
if (year > 0) {
|
||||||
|
try writer.print("{d:0>4}", .{@as(u32, @intCast(year))});
|
||||||
|
} else {
|
||||||
|
try writer.print("{d:0>4}", .{year});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
Loading…
Add table
Reference in a new issue