1const std = @import("std");
2const builtin = @import("builtin");
3const string = []const u8;
4const extras = @import("extras");
5const nio = @import("nio");
6const time = @This();
7
8const sys = switch (builtin.target.os.tag) {
9 .linux => @import("sys-linux"),
10 .macos => @import("sys-darwin"),
11 .freebsd => @import("sys-freebsd"),
12 .netbsd => @import("sys-netbsd"),
13 .openbsd => @import("sys-openbsd"),
14 else => unreachable, // TODO:
15};
16
17pub const Zone = @import("./zone.zig").Zone;
18
19pub const DateTime = struct {
20 ms: u16,
21 seconds: u8,
22 minutes: u8,
23 hours: u8,
24 days: u8,
25 months: u8,
26 years: u16,
27 z_offset: i8,
28
29 pub fn initUnixMs(unix: u64) DateTime {
30 return epoch_unix.addMs(unix);
31 }
32
33 pub fn initUnix(unix: u64) DateTime {
34 return epoch_unix.addSecs(unix);
35 }
36
37 /// Caller asserts that this is > epoch
38 pub fn init(year: u16, month: u16, day: u16, hr: u16, min: u16, sec: u16, ms: u16) DateTime {
39 return epoch_unix
40 .addYears(year - epoch_unix.years)
41 .addMonths(month)
42 .addDays(day)
43 .addHours(hr)
44 .addMins(min)
45 .addSecs(sec)
46 .addMs(ms);
47 }
48
49 pub fn now() DateTime {
50 return initUnixMs(@intCast(milliTimestamp()));
51 }
52
53 pub fn nowAtOffset(offset: i8) DateTime {
54 var result: DateTime = .initUnixMs(@intCast(milliTimestamp() + (@as(i64, offset) * 15 * ms_per_min)));
55 result.z_offset = offset;
56 return result;
57 }
58
59 pub const epoch_unix = DateTime{
60 .ms = 0,
61 .seconds = 0,
62 .minutes = 0,
63 .hours = 0,
64 .days = 0,
65 .months = 0,
66 .years = 1970,
67 .z_offset = 0,
68 };
69
70 pub fn toISOString(self: DateTime) [25]u8 {
71 // "2021-10-21T23:20:30+00:00"
72 var result: [25]u8 = @splat(0);
73 var fbs: nio.FixedBufferStream([]u8) = .init(&result);
74 self.formatFmt("YYYY-MM-DD HH:mm:ss", &fbs) catch unreachable;
75 fbs = .init(result[19..]);
76 self.formatFmt("Z", &fbs) catch unreachable;
77 result[10] = 'T';
78 return result;
79 }
80
81 pub fn eql(self: DateTime, other: DateTime) bool {
82 return self.ms == other.ms and
83 self.seconds == other.seconds and
84 self.minutes == other.minutes and
85 self.hours == other.hours and
86 self.days == other.days and
87 self.months == other.months and
88 self.years == other.years and
89 self.z_offset == other.z_offset;
90 }
91
92 pub fn addMs(self: DateTime, count: u64) DateTime {
93 if (count == 0) return self;
94 var result = self;
95 result.ms += @intCast(count % 1000);
96 return result.addSecs(count / 1000);
97 }
98
99 pub fn addSecs(self: DateTime, count: u64) DateTime {
100 if (count == 0) return self;
101 var result = self;
102 result.seconds += @intCast(count % 60);
103 return result.addMins(count / 60);
104 }
105
106 pub fn addMins(self: DateTime, count: u64) DateTime {
107 if (count == 0) return self;
108 var result = self;
109 result.minutes += @intCast(count % 60);
110 return result.addHours(count / 60);
111 }
112
113 pub fn addHours(self: DateTime, count: u64) DateTime {
114 if (count == 0) return self;
115 var result = self;
116 result.hours += @intCast(count % 24);
117 return result.addDays(count / 24);
118 }
119
120 pub fn addDays(self: DateTime, count: u64) DateTime {
121 if (count == 0) return self;
122 var result = self;
123 var input = count;
124
125 while (true) {
126 const year_len = result.daysThisYear();
127 if (input >= year_len) {
128 result.years += 1;
129 input -= year_len;
130 continue;
131 }
132 break;
133 }
134 while (true) {
135 const month_len = result.daysThisMonth();
136 if (input >= month_len) {
137 result.months += 1;
138 input -= month_len;
139
140 if (result.months == 12) {
141 result.years += 1;
142 result.months = 0;
143 }
144 continue;
145 }
146 break;
147 }
148 {
149 const month_len = result.daysThisMonth();
150 if (result.days + input > month_len) {
151 const left = month_len -| result.days;
152 input -= left;
153 result.months += 1;
154 result.days = 0;
155 }
156 result.days += @intCast(input);
157
158 if (result.days == result.daysThisMonth()) {
159 result.months += 1;
160 result.days = 0;
161 }
162 if (result.months == 12) {
163 result.years += 1;
164 result.months = 0;
165 }
166 }
167
168 std.debug.assert(result.ms < 1000);
169 std.debug.assert(result.seconds < 60);
170 std.debug.assert(result.minutes < 60);
171 std.debug.assert(result.hours < 24);
172 std.debug.assert(result.days < result.daysThisMonth());
173 std.debug.assert(result.months < 12);
174 return result;
175 }
176
177 pub fn addWeeks(self: DateTime, count: u64) DateTime {
178 if (count == 0) return self;
179 return self.addDays(7).addWeeks(count - 1);
180 }
181
182 pub fn addMonths(self: DateTime, count: u64) DateTime {
183 if (count == 0) return self;
184 var result = self;
185 var input = count;
186 result.years += @intCast(input / 12);
187 input %= 12;
188 result.months += @intCast(input);
189 if (result.months >= 12) result.years += 1;
190 result.months %= 12;
191 result.days = @min(result.days, result.daysInMonth(result.months) - 1);
192 return result;
193 }
194
195 pub fn addYears(self: DateTime, count: u64) DateTime {
196 var result = self;
197 for (0..count) |_| {
198 result = result.addDays(result.daysThisYear());
199 }
200 return result;
201 }
202
203 pub fn addQuarters(self: DateTime, count: u64) DateTime {
204 if (count == 0) return self;
205 return self.addMonths(3).addQuarters(count - 1);
206 }
207
208 pub fn isLeapYear(self: DateTime) bool {
209 return time.isLeapYear(self.years);
210 }
211
212 pub fn daysThisYear(self: DateTime) u16 {
213 return time.daysInYear(self.years);
214 }
215
216 pub fn daysThisMonth(self: DateTime) u16 {
217 return self.daysInMonth(self.months);
218 }
219
220 fn daysInMonth(self: DateTime, month: u16) u16 {
221 return time.daysInMonth(self.years, month);
222 }
223
224 pub fn dayOfThisYear(self: DateTime) u16 {
225 var ret: u16 = 0;
226 for (0..self.months) |item| {
227 ret += self.daysInMonth(@intCast(item));
228 }
229 ret += self.days;
230 return ret;
231 }
232
233 pub fn toUnix(self: DateTime) u64 {
234 return self.toUnixMilli() / 1000;
235 }
236
237 pub fn toUnixMilli(self: DateTime) u64 {
238 var res: u64 = 0;
239 res += self.ms;
240 res += @as(u64, self.seconds) * ms_per_s;
241 res += @as(u64, self.minutes) * ms_per_min;
242 res += @as(u64, self.hours) * ms_per_hour;
243 res += self.daysSinceEpoch() * ms_per_day;
244 res = extras.safeAdd(res, @as(i32, -self.z_offset) * 15 * ms_per_min);
245 return res;
246 }
247
248 pub fn to_timespec(self: DateTime) sys.struct_timespec {
249 return .{
250 .sec = @intCast(self.toUnix()),
251 .nsec = @as(isize, self.ms) * ns_per_ms,
252 };
253 }
254
255 fn daysSinceEpoch(self: DateTime) u64 {
256 var res: u64 = 0;
257 res += self.days;
258 for (0..self.years - epoch_unix.years) |i| res += time.daysInYear(@intCast(i));
259 for (0..self.months) |i| res += self.daysInMonth(@intCast(i));
260 return res;
261 }
262
263 /// the timezone offset is stored as a signed amount of 15-min increments.
264 /// the structure of the return value is .{ sign, hrs, mins }.
265 /// 'sign' will be either '+' or '-'
266 pub fn tz_parts(self: DateTime) [3]u8 {
267 const o = self.z_offset;
268 const a = @abs(o);
269 return .{
270 if (o < 0) '-' else '+',
271 a / 4,
272 (a % 4) * 15,
273 };
274 }
275
276 /// fmt is based on https://momentjs.com/docs/#/displaying/format/
277 pub fn formatFmt(self: DateTime, comptime fmt: string, writer: anytype) !void {
278 if (fmt.len == 0) @compileError("DateTime: format string can't be empty");
279
280 @setEvalBranchQuota(100000);
281
282 comptime var s = 0;
283 comptime var e = 0;
284 comptime var next: ?FormatSeq = null;
285 inline for (fmt, 0..) |c, i| {
286 e = i + 1;
287
288 if (comptime std.meta.stringToEnum(FormatSeq, fmt[s..e])) |tag| {
289 next = tag;
290 if (i < fmt.len - 1) continue;
291 }
292
293 if (next) |tag| {
294 switch (tag) {
295 .MM => try writer.print("{:0>2}", .{self.months + 1}),
296 .M => try writer.print("{}", .{self.months + 1}),
297 .Mo => try printOrdinal(writer, self.months + 1),
298 .MMM => try printLongName(writer, self.months, &[_]string{ "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec" }),
299 .MMMM => try printLongName(writer, self.months, &[_]string{ "January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December" }),
300
301 .Q => try writer.print("{}", .{self.months / 3 + 1}),
302 .Qo => try printOrdinal(writer, self.months / 3 + 1),
303
304 .D => try writer.print("{}", .{self.days + 1}),
305 .Do => try printOrdinal(writer, self.days + 1),
306 .DD => try writer.print("{:0>2}", .{self.days + 1}),
307
308 .DDD => try writer.print("{}", .{self.dayOfThisYear() + 1}),
309 .DDDo => try printOrdinal(writer, self.dayOfThisYear() + 1),
310 .DDDD => try writer.print("{:0>3}", .{self.dayOfThisYear() + 1}),
311
312 .d => try writer.print("{}", .{@intFromEnum(self.weekday())}),
313 .do => try printOrdinal(writer, @intFromEnum(self.weekday())),
314 .dd => try writer.writeAll(@tagName(self.weekday())[0..2]),
315 .ddd => try writer.writeAll(@tagName(self.weekday())),
316 .dddd => try printLongName(writer, @intFromEnum(self.weekday()), &[_]string{ "Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday" }),
317 .e => try writer.print("{}", .{@intFromEnum(self.weekday())}),
318 .E => try writer.print("{}", .{@intFromEnum(self.weekday()) + 1}),
319
320 .w => try writer.print("{}", .{self.dayOfThisYear() / 7 + 1}),
321 .wo => try printOrdinal(writer, self.dayOfThisYear() / 7 + 1),
322 .ww => try writer.print("{:0>2}", .{self.dayOfThisYear() / 7 + 1}),
323
324 .Y => try writer.print("{}", .{self.years + 10000}),
325 .YY => try writer.print("{:0>2}", .{self.years % 100}),
326 .YYY => try writer.print("{}", .{self.years}),
327 .YYYY => try writer.print("{:0>4}", .{self.years}),
328
329 .N => try writer.writeAll(@tagName(self.era())),
330 .NN => try writer.writeAll("Anno Domini"),
331
332 .A => try printLongName(writer, self.hours / 12, &[_]string{ "AM", "PM" }),
333 .a => try printLongName(writer, self.hours / 12, &[_]string{ "am", "pm" }),
334
335 .H => try writer.print("{}", .{self.hours}),
336 .HH => try writer.print("{:0>2}", .{self.hours}),
337 .h => try writer.print("{}", .{wrap(self.hours, 12)}),
338 .hh => try writer.print("{:0>2}", .{wrap(self.hours, 12)}),
339 .k => try writer.print("{}", .{wrap(self.hours, 24)}),
340 .kk => try writer.print("{:0>2}", .{wrap(self.hours, 24)}),
341
342 .m => try writer.print("{}", .{self.minutes}),
343 .mm => try writer.print("{:0>2}", .{self.minutes}),
344
345 .s => try writer.print("{}", .{self.seconds}),
346 .ss => try writer.print("{:0>2}", .{self.seconds}),
347
348 .S => try writer.print("{}", .{self.ms / 100}),
349 .SS => try writer.print("{:0>2}", .{self.ms / 10}),
350 .SSS => try writer.print("{:0>3}", .{self.ms}),
351
352 .Z => try writer.print("{c}{:0>2}:{:0>2}", toTuple(self.tz_parts())),
353 .ZZ => try writer.print("{c}{:0>2}{:0>2}", toTuple(self.tz_parts())),
354
355 .x => try writer.print("{}", .{self.toUnixMilli()}),
356 .X => try writer.print("{}", .{self.toUnix()}),
357 }
358 next = null;
359 s = i;
360 }
361
362 switch (c) {
363 ',',
364 ' ',
365 ':',
366 '-',
367 '.',
368 'T',
369 'W',
370 '/',
371 => {
372 try writer.writeAll(&.{c});
373 s = i + 1;
374 continue;
375 },
376 else => {},
377 }
378 }
379 }
380
381 pub fn formatAlloc(self: DateTime, alloc: std.mem.Allocator, comptime fmt: string) !string {
382 var list = std.Io.Writer.Allocating.init(alloc);
383 defer list.deinit();
384 try self.formatFmt(fmt, &list.writer);
385 return list.toOwnedSlice();
386 }
387
388 const FormatSeq = enum {
389 M, // 1 2 ... 11 12
390 Mo, // 1st 2nd ... 11th 12th
391 MM, // 01 02 ... 11 12
392 MMM, // Jan Feb ... Nov Dec
393 MMMM, // January February ... November December
394 Q, // 1 2 3 4
395 Qo, // 1st 2nd 3rd 4th
396 D, // 1 2 ... 30 31
397 Do, // 1st 2nd ... 30th 31st
398 DD, // 01 02 ... 30 31
399 DDD, // 1 2 ... 364 365
400 DDDo, // 1st 2nd ... 364th 365th
401 DDDD, // 001 002 ... 364 365
402 d, // 0 1 ... 5 6
403 do, // 0th 1st ... 5th 6th
404 dd, // Su Mo ... Fr Sa
405 ddd, // Sun Mon ... Fri Sat
406 dddd, // Sunday Monday ... Friday Saturday
407 e, // 0 1 ... 5 6 (locale)
408 E, // 1 2 ... 6 7 (ISO)
409 w, // 1 2 ... 52 53
410 wo, // 1st 2nd ... 52nd 53rd
411 ww, // 01 02 ... 52 53
412 Y, // 11970 11971 ... 19999 20000 20001 (Holocene calendar)
413 YY, // 70 71 ... 29 30
414 YYY, // 1 2 ... 1970 1971 ... 2029 2030
415 YYYY, // 0001 0002 ... 1970 1971 ... 2029 2030
416 N, // BC AD
417 NN, // Before Christ ... Anno Domini
418 A, // AM PM
419 a, // am pm
420 H, // 0 1 ... 22 23
421 HH, // 00 01 ... 22 23
422 h, // 1 2 ... 11 12
423 hh, // 01 02 ... 11 12
424 k, // 1 2 ... 23 24
425 kk, // 01 02 ... 23 24
426 m, // 0 1 ... 58 59
427 mm, // 00 01 ... 58 59
428 s, // 0 1 ... 58 59
429 ss, // 00 01 ... 58 59
430 S, // 0 1 ... 8 9 (second fraction)
431 SS, // 00 01 ... 98 99
432 SSS, // 000 001 ... 998 999
433 Z, // -07:00 -06:00 ... +06:00 +07:00
434 ZZ, // -0700 -0600 ... +0600 +0700
435 x, // unix milli
436 X, // unix
437 };
438
439 pub fn since(self: DateTime, other: DateTime) Duration {
440 return Duration{
441 .ms = @as(i64, @intCast(self.toUnixMilli())) - @as(i64, @intCast(other.toUnixMilli())),
442 };
443 }
444
445 pub fn era(self: DateTime) Era {
446 if (self.years >= 0) return .AD;
447 @compileError("TODO");
448 }
449
450 pub fn weekday(self: DateTime) WeekDay {
451 var i = self.daysSinceEpoch() % 7;
452 var result = WeekDay.Thu; // weekday of epoch_unix
453 while (i > 0) : (i -= 1) {
454 result = result.next();
455 }
456 return result;
457 }
458
459 pub fn stringifyJson(self: DateTime, writer: anytype, options: std.json.Stringify.Options, json: type) !void {
460 return json.stringify(writer, &self.toISOString(), options);
461 }
462};
463
464pub const PlainYearMonth = struct {
465 years: u16,
466 months: u8,
467};
468
469pub const PlainMonthDay = struct {
470 months: u8,
471 days: u8,
472};
473
474pub const PlainDate = struct {
475 years: u16,
476 months: u8,
477 days: u8,
478};
479
480pub const PlainTime = struct {
481 hours: u8,
482 minutes: u8,
483 seconds: u8,
484 ms: u16,
485};
486
487pub const PlainDateTime = struct {
488 years: u16,
489 months: u8,
490 days: u8,
491 hours: u8,
492 minutes: u8,
493 seconds: u8,
494 ms: u16,
495};
496
497pub const format = struct {
498 pub const LT = "h:mm A";
499 pub const LTS = "h:mm:ss A";
500 pub const L = "MM/DD/YYYY";
501 pub const l = "M/D/YYY";
502 pub const LL = "MMMM D, YYYY";
503 pub const ll = "MMM D, YYY";
504 pub const LLL = LL ++ " " ++ LT;
505 pub const lll = ll ++ " " ++ LT;
506 pub const LLLL = "dddd, " ++ LLL;
507 pub const llll = "ddd, " ++ lll;
508};
509
510pub const WeekDay = enum {
511 Sun,
512 Mon,
513 Tue,
514 Wed,
515 Thu,
516 Fri,
517 Sat,
518
519 pub fn next(self: WeekDay) WeekDay {
520 return switch (self) {
521 .Sun => .Mon,
522 .Mon => .Tue,
523 .Tue => .Wed,
524 .Wed => .Thu,
525 .Thu => .Fri,
526 .Fri => .Sat,
527 .Sat => .Sun,
528 };
529 }
530
531 pub fn jsonStringify(self: @This(), json_stream: anytype) !void {
532 try json_stream.write(@tagName(self));
533 }
534};
535
536pub const Era = enum {
537 // BC,
538 AD,
539
540 pub fn jsonStringify(self: @This(), json_stream: anytype) !void {
541 try json_stream.write(@tagName(self));
542 }
543};
544
545// https://hueffner.de/falk/blog/a-leap-year-check-in-three-instructions.html
546pub fn isLeapYear(year: u16) bool {
547 if (year % 4 != 0) return false;
548 if (year % 25 != 0) return true;
549 if (year % 16 == 0) return true;
550 return false;
551}
552
553pub fn daysInYear(year: u16) u16 {
554 return if (isLeapYear(year)) 366 else 365;
555}
556
557fn daysInMonth(year: u16, month: u16) u16 {
558 const norm = [12]u16{ 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 };
559 const leap = [12]u16{ 31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 };
560 const month_days = if (!isLeapYear(year)) norm else leap;
561 return month_days[month];
562}
563
564fn printOrdinal(writer: anytype, num: u16) !void {
565 try writer.print("{}", .{num});
566 try writer.writeAll(switch (num) {
567 1 => "st",
568 2 => "nd",
569 3 => "rd",
570 else => "th",
571 });
572}
573
574fn printLongName(writer: anytype, index: u16, names: []const string) !void {
575 try writer.writeAll(names[index]);
576}
577
578fn wrap(val: u16, at: u16) u16 {
579 const tmp = val % at;
580 return if (tmp == 0) at else tmp;
581}
582
583fn toTuple(array: anytype) @Tuple(&@as([array.len]type, @splat(std.meta.Child(@TypeOf(array))))) {
584 var result: @Tuple(&@as([array.len]type, @splat(std.meta.Child(@TypeOf(array))))) = undefined;
585 inline for (array, 0..) |v, i| result[i] = v;
586 return result;
587}
588
589pub const Duration = struct {
590 ms: i64,
591};
592
593// Divisions of a nanosecond.
594pub const ns_per_us = 1000;
595pub const ns_per_ms = 1000 * ns_per_us;
596pub const ns_per_s = 1000 * ns_per_ms;
597pub const ns_per_min = 60 * ns_per_s;
598pub const ns_per_hour = 60 * ns_per_min;
599pub const ns_per_day = 24 * ns_per_hour;
600pub const ns_per_week = 7 * ns_per_day;
601
602// Divisions of a microsecond.
603pub const us_per_ms = 1000;
604pub const us_per_s = 1000 * us_per_ms;
605pub const us_per_min = 60 * us_per_s;
606pub const us_per_hour = 60 * us_per_min;
607pub const us_per_day = 24 * us_per_hour;
608pub const us_per_week = 7 * us_per_day;
609
610// Divisions of a millisecond.
611pub const ms_per_s = 1000;
612pub const ms_per_min = 60 * ms_per_s;
613pub const ms_per_hour = 60 * ms_per_min;
614pub const ms_per_day = 24 * ms_per_hour;
615pub const ms_per_week = 7 * ms_per_day;
616
617// Divisions of a second.
618pub const s_per_min = 60;
619pub const s_per_hour = s_per_min * 60;
620pub const s_per_day = s_per_hour * 24;
621pub const s_per_week = s_per_day * 7;
622
623pub fn nanoTimestamp() i128 {
624 const ts = sys.clock_gettime(sys.CLOCK.REALTIME) catch return 0;
625 var result: i128 = 0;
626 result += ts.sec;
627 result *= ns_per_s;
628 result += ts.nsec;
629 return result;
630}
631
632pub fn microTimestamp() i64 {
633 return @as(i64, @intCast(@divFloor(nanoTimestamp(), ns_per_us)));
634}
635
636pub fn milliTimestamp() i64 {
637 return @as(i64, @intCast(@divFloor(nanoTimestamp(), ns_per_ms)));
638}
639
640pub fn timestamp() i64 {
641 return @as(i64, @intCast(@divFloor(nanoTimestamp(), ns_per_s)));
642}
643
644pub const Instant = struct {
645 timestamp_ns: u64,
646
647 pub fn now() !Instant {
648 const ts = try sys.clock_gettime(sys.CLOCK.REALTIME);
649 return .{ .timestamp_ns = @as(u64, @intCast(ts.nsec)) + @as(u64, @intCast(ts.sec)) * ns_per_s };
650 }
651
652 pub fn since(self: Instant, earlier: Instant) u64 {
653 return self.timestamp_ns - earlier.timestamp_ns;
654 }
655
656 pub fn order(self: Instant, other: Instant) std.math.Order {
657 return std.math.order(self.timestamp_ns, other.timestamp_ns);
658 }
659};
660
661pub const Timer = struct {
662 started: Instant,
663
664 pub fn start() !Timer {
665 return .{ .started = try .now() };
666 }
667
668 fn sample() Instant {
669 return Instant.now() catch unreachable;
670 }
671
672 pub fn read(self: *const Timer) u64 {
673 return sample().since(self.started);
674 }
675
676 pub fn reset(self: *Timer) void {
677 self.started = sample();
678 }
679
680 pub fn lap(self: *Timer) u64 {
681 const current = sample();
682 defer self.started = current;
683 return current.since(self.started);
684 }
685};