From b9a523cefd1bdb8d019dffa1f9d71ee3a8bea417 Mon Sep 17 00:00:00 2001 From: Delyan Angelov Date: Sat, 5 Aug 2023 23:41:23 +0300 Subject: [PATCH] time: store time with nanosecond resolution in time.Time, deprecate Time.microsecond, add utility methods and tests (#19062) --- examples/clock/clock.v | 4 +- vlib/time/README.md | 3 +- vlib/time/duration_test.v | 6 ++ vlib/time/format.v | 16 +++-- vlib/time/operator.v | 13 ++-- vlib/time/operator_test.v | 84 ++++++++++++------------- vlib/time/parse.c.v | 22 ++++--- vlib/time/parse_test.v | 18 +++--- vlib/time/time.c.v | 13 +--- vlib/time/time.v | 109 ++++++++++++++++++++++++--------- vlib/time/time_addition_test.v | 4 +- vlib/time/time_darwin.c.v | 41 +++++-------- vlib/time/time_nix.c.v | 16 ++--- vlib/time/time_solaris.c.v | 4 +- vlib/time/time_test.v | 77 +++++++++++++---------- vlib/time/time_windows.c.v | 16 ++--- vlib/time/unix.v | 18 +++++- 17 files changed, 265 insertions(+), 199 deletions(-) diff --git a/examples/clock/clock.v b/examples/clock/clock.v index 8d503907a4..bb2b198caf 100644 --- a/examples/clock/clock.v +++ b/examples/clock/clock.v @@ -74,12 +74,12 @@ fn on_frame(mut app App) { // draw minute hand mut j := f32(n.minute) if n.second == 59 { // make minute hand move smoothly - j += f32(math.sin(f32(n.microsecond) / 1e6 * math.pi / 2.0)) + j += f32(math.sin(f32(n.nanosecond) / 1e9 * math.pi / 2.0)) } draw_convex_poly_rotate(mut app.gg, app.dpi_scale, app.minute_hand, hand_color, j * 6) // draw second hand with smooth transition - k := f32(n.second) + f32(math.sin(f32(n.microsecond) / 1e6 * math.pi / 2.0)) + k := f32(n.second) + f32(math.sin(f32(n.nanosecond) / 1e9 * math.pi / 2.0)) draw_convex_poly_rotate(mut app.gg, app.dpi_scale, app.second_hand, second_hand_color, 0 + k * 6) diff --git a/vlib/time/README.md b/vlib/time/README.md index 39f4375d4a..b653cd8f62 100644 --- a/vlib/time/README.md +++ b/vlib/time/README.md @@ -28,7 +28,7 @@ const time_to_test = time.Time{ hour: 21 minute: 23 second: 42 - microsecond: 123456 + nanosecond: 123456789 unix: 332198622 } @@ -38,6 +38,7 @@ assert '1980-07-11 21:23' == time_to_test.format() assert '1980-07-11 21:23:42' == time_to_test.format_ss() assert '1980-07-11 21:23:42.123' == time_to_test.format_ss_milli() assert '1980-07-11 21:23:42.123456' == time_to_test.format_ss_micro() +assert '1980-07-11 21:23:42.123456789' == time_to_test.format_ss_nano() ``` You can also parse strings to produce time.Time values, diff --git a/vlib/time/duration_test.v b/vlib/time/duration_test.v index b4fb2d8397..9732c43ed7 100644 --- a/vlib/time/duration_test.v +++ b/vlib/time/duration_test.v @@ -31,3 +31,9 @@ fn test_duration_str() { assert time.Duration(1 * time.hour + 5 * time.second).str() == '1:00:05' assert time.Duration(168 * time.hour + 5 * time.minute + 7 * time.second).str() == '168:05:07' } + +fn test_duration_debug() { + assert time.Duration(1 * time.nanosecond).debug() == 'Duration: 1ns' + assert time.Duration(169 * time.hour + 5 * time.minute + 7 * time.second).debug() == 'Duration: 7days, 1h, 5m, 7s' + assert (-time.Duration(169 * time.hour + 5 * time.minute + 7 * time.second)).debug() == 'Duration: - 7days, 1h, 5m, 7s' +} diff --git a/vlib/time/format.v b/vlib/time/format.v index a1914a72ee..247b8e49b1 100644 --- a/vlib/time/format.v +++ b/vlib/time/format.v @@ -17,7 +17,7 @@ pub fn (t Time) format_ss() string { // format_ss_milli returns a date string in "YYYY-MM-DD HH:mm:ss.123" format (24h). pub fn (t Time) format_ss_milli() string { - return '${t.year:04d}-${t.month:02d}-${t.day:02d} ${t.hour:02d}:${t.minute:02d}:${t.second:02d}.${(t.microsecond / 1000):03d}' + return '${t.year:04d}-${t.month:02d}-${t.day:02d} ${t.hour:02d}:${t.minute:02d}:${t.second:02d}.${(t.nanosecond / 1_000_000):03d}' } // format_rfc3339 returns a date string in "YYYY-MM-DDTHH:mm:ss.123Z" format (24 hours, see https://www.rfc-editor.org/rfc/rfc3339.html) @@ -25,12 +25,17 @@ pub fn (t Time) format_ss_milli() string { // It is intended to improve consistency and interoperability, when representing and using date and time in Internet protocols. pub fn (t Time) format_rfc3339() string { u := t.local_to_utc() - return '${u.year:04d}-${u.month:02d}-${u.day:02d}T${u.hour:02d}:${u.minute:02d}:${u.second:02d}.${(u.microsecond / 1000):03d}Z' + return '${u.year:04d}-${u.month:02d}-${u.day:02d}T${u.hour:02d}:${u.minute:02d}:${u.second:02d}.${(u.nanosecond / 1_000_000):03d}Z' } // format_ss_micro returns a date string in "YYYY-MM-DD HH:mm:ss.123456" format (24h). pub fn (t Time) format_ss_micro() string { - return '${t.year:04d}-${t.month:02d}-${t.day:02d} ${t.hour:02d}:${t.minute:02d}:${t.second:02d}.${t.microsecond:06d}' + return '${t.year:04d}-${t.month:02d}-${t.day:02d} ${t.hour:02d}:${t.minute:02d}:${t.second:02d}.${(t.nanosecond / 1_000):06d}' +} + +// format_ss_nano returns a date string in "YYYY-MM-DD HH:mm:ss.123456789" format (24h). +pub fn (t Time) format_ss_nano() string { + return '${t.year:04d}-${t.month:02d}-${t.day:02d} ${t.hour:02d}:${t.minute:02d}:${t.second:02d}.${t.nanosecond:09d}' } // hhmm returns a date string in "HH:mm" format (24h). @@ -381,8 +386,9 @@ pub fn (t Time) get_fmt_time_str(fmt_time FormatTime) string { .hhmm24 { '${t.hour:02d}:${t.minute:02d}' } .hhmmss12 { '${hour_}:${t.minute:02d}:${t.second:02d} ${tp}' } .hhmmss24 { '${t.hour:02d}:${t.minute:02d}:${t.second:02d}' } - .hhmmss24_milli { '${t.hour:02d}:${t.minute:02d}:${t.second:02d}.${(t.microsecond / 1000):03d}' } - .hhmmss24_micro { '${t.hour:02d}:${t.minute:02d}:${t.second:02d}.${t.microsecond:06d}' } + .hhmmss24_milli { '${t.hour:02d}:${t.minute:02d}:${t.second:02d}.${(t.nanosecond / 1_000_000):03d}' } + .hhmmss24_micro { '${t.hour:02d}:${t.minute:02d}:${t.second:02d}.${(t.nanosecond / 1_000):06d}' } + .hhmmss24_nano { '${t.hour:02d}:${t.minute:02d}:${t.second:02d}.${t.nanosecond:06d}' } else { 'unknown enumeration ${fmt_time}' } } } diff --git a/vlib/time/operator.v b/vlib/time/operator.v index f62854983d..bee2ddf34d 100644 --- a/vlib/time/operator.v +++ b/vlib/time/operator.v @@ -3,19 +3,22 @@ module time // operator `==` returns true if provided time is equal to time [inline] pub fn (t1 Time) == (t2 Time) bool { - return t1.unix == t2.unix && t1.microsecond == t2.microsecond + return t1.unix == t2.unix && t1.nanosecond == t2.nanosecond } // operator `<` returns true if provided time is less than time [inline] pub fn (t1 Time) < (t2 Time) bool { - return t1.unix < t2.unix || (t1.unix == t2.unix && t1.microsecond < t2.microsecond) + return t1.unix < t2.unix || (t1.unix == t2.unix && t1.nanosecond < t2.nanosecond) } // Time subtract using operator overloading. [inline] pub fn (lhs Time) - (rhs Time) Duration { - lhs_micro := lhs.unix * 1_000_000 + lhs.microsecond - rhs_micro := rhs.unix * 1_000_000 + rhs.microsecond - return (lhs_micro - rhs_micro) * microsecond + // lhs.unix * 1_000_000_000 + i64(lhs.nanosecond) will overflow i64, for years > 3000 . + // Doing the diff first, and *then* multiplying by `second`, is less likely to overflow, + // since lhs and rhs will be likely close to each other. + unixs := i64(lhs.unix - rhs.unix) * second + nanos := lhs.nanosecond - rhs.nanosecond + return unixs + nanos } diff --git a/vlib/time/operator_test.v b/vlib/time/operator_test.v index 5f3e1b7a3e..f209cfda9a 100644 --- a/vlib/time/operator_test.v +++ b/vlib/time/operator_test.v @@ -39,7 +39,7 @@ fn test_time1_should_be_same_as_time2() { hour: 22 minute: 11 second: 3 - microsecond: 100 + nanosecond: 100 }) t2 := new_time(Time{ year: 2000 @@ -48,7 +48,7 @@ fn test_time1_should_be_same_as_time2() { hour: 22 minute: 11 second: 3 - microsecond: 100 + nanosecond: 100 }) assert t1 == t2 } @@ -61,9 +61,9 @@ fn test_time1_should_not_be_same_as_time2() { hour: 22 minute: 11 second: 3 - microsecond: 100 + nanosecond: 100 }) - // Difference is one microsecond + // Difference is one nanosecond t2 := new_time(Time{ year: 2000 month: 5 @@ -71,7 +71,7 @@ fn test_time1_should_not_be_same_as_time2() { hour: 22 minute: 11 second: 3 - microsecond: 101 + nanosecond: 101 }) t3 := new_time(Time{ year: 2000 @@ -80,7 +80,7 @@ fn test_time1_should_not_be_same_as_time2() { hour: 22 minute: 11 second: 3 - microsecond: 0 + nanosecond: 0 }) // Difference is one second t4 := new_time(Time{ @@ -90,7 +90,7 @@ fn test_time1_should_not_be_same_as_time2() { hour: 22 minute: 11 second: 4 - microsecond: 0 + nanosecond: 0 }) assert t1 != t2 assert t3 != t4 @@ -104,9 +104,9 @@ fn test_time1_should_be_greater_than_time2() { hour: 22 minute: 11 second: 3 - microsecond: 102 + nanosecond: 102 }) - // Difference is one microsecond + // Difference is one nanosecond t2 := new_time(Time{ year: 2000 month: 5 @@ -114,7 +114,7 @@ fn test_time1_should_be_greater_than_time2() { hour: 22 minute: 11 second: 3 - microsecond: 101 + nanosecond: 101 }) t3 := new_time(Time{ year: 2000 @@ -123,7 +123,7 @@ fn test_time1_should_be_greater_than_time2() { hour: 22 minute: 11 second: 5 - microsecond: 0 + nanosecond: 0 }) // Difference is one second t4 := new_time(Time{ @@ -133,7 +133,7 @@ fn test_time1_should_be_greater_than_time2() { hour: 22 minute: 11 second: 4 - microsecond: 0 + nanosecond: 0 }) assert t1 > t2 assert t3 > t4 @@ -147,9 +147,9 @@ fn test_time2_should_be_less_than_time1() { hour: 22 minute: 11 second: 3 - microsecond: 102 + nanosecond: 102 }) - // Difference is one microsecond + // Difference is one nanosecond t2 := new_time(Time{ year: 2000 month: 5 @@ -157,7 +157,7 @@ fn test_time2_should_be_less_than_time1() { hour: 22 minute: 11 second: 3 - microsecond: 101 + nanosecond: 101 }) t3 := new_time(Time{ year: 2000 @@ -166,7 +166,7 @@ fn test_time2_should_be_less_than_time1() { hour: 22 minute: 11 second: 3 - microsecond: 0 + nanosecond: 0 }) // Difference is one second t4 := new_time(Time{ @@ -176,7 +176,7 @@ fn test_time2_should_be_less_than_time1() { hour: 22 minute: 11 second: 2 - microsecond: 0 + nanosecond: 0 }) assert t2 < t1 assert t4 < t3 @@ -190,9 +190,9 @@ fn test_time1_should_be_greater_or_equal_to_time2_when_gt() { hour: 22 minute: 11 second: 3 - microsecond: 102 + nanosecond: 102 }) - // Difference is one microsecond + // Difference is one nanosecond t2 := new_time(Time{ year: 2000 month: 5 @@ -200,7 +200,7 @@ fn test_time1_should_be_greater_or_equal_to_time2_when_gt() { hour: 22 minute: 11 second: 3 - microsecond: 101 + nanosecond: 101 }) t3 := new_time(Time{ year: 2000 @@ -209,7 +209,7 @@ fn test_time1_should_be_greater_or_equal_to_time2_when_gt() { hour: 22 minute: 11 second: 5 - microsecond: 0 + nanosecond: 0 }) // Difference is one second t4 := new_time(Time{ @@ -219,7 +219,7 @@ fn test_time1_should_be_greater_or_equal_to_time2_when_gt() { hour: 22 minute: 11 second: 4 - microsecond: 0 + nanosecond: 0 }) assert t1 >= t2 assert t3 >= t4 @@ -233,9 +233,9 @@ fn test_time1_should_be_greater_or_equal_to_time2_when_eq() { hour: 22 minute: 11 second: 3 - microsecond: 100 + nanosecond: 100 }) - // Difference is one microsecond + // Difference is one nanosecond t2 := new_time(Time{ year: 2000 month: 5 @@ -243,7 +243,7 @@ fn test_time1_should_be_greater_or_equal_to_time2_when_eq() { hour: 22 minute: 11 second: 3 - microsecond: 100 + nanosecond: 100 }) t3 := new_time(Time{ year: 2000 @@ -252,7 +252,7 @@ fn test_time1_should_be_greater_or_equal_to_time2_when_eq() { hour: 22 minute: 11 second: 3 - microsecond: 0 + nanosecond: 0 }) // Difference is one second t4 := new_time(Time{ @@ -262,7 +262,7 @@ fn test_time1_should_be_greater_or_equal_to_time2_when_eq() { hour: 22 minute: 11 second: 3 - microsecond: 0 + nanosecond: 0 }) assert t1 >= t2 assert t3 >= t4 @@ -276,9 +276,9 @@ fn test_time1_should_be_less_or_equal_to_time2_when_lt() { hour: 22 minute: 11 second: 3 - microsecond: 100 + nanosecond: 100 }) - // Difference is one microsecond + // Difference is one nanosecond t2 := new_time(Time{ year: 2000 month: 5 @@ -286,7 +286,7 @@ fn test_time1_should_be_less_or_equal_to_time2_when_lt() { hour: 22 minute: 11 second: 3 - microsecond: 101 + nanosecond: 101 }) t3 := new_time(Time{ year: 2000 @@ -295,7 +295,7 @@ fn test_time1_should_be_less_or_equal_to_time2_when_lt() { hour: 22 minute: 11 second: 3 - microsecond: 0 + nanosecond: 0 }) // Difference is one second t4 := new_time(Time{ @@ -305,7 +305,7 @@ fn test_time1_should_be_less_or_equal_to_time2_when_lt() { hour: 22 minute: 11 second: 4 - microsecond: 0 + nanosecond: 0 }) assert t1 <= t2 assert t3 <= t4 @@ -319,9 +319,9 @@ fn test_time1_should_be_less_or_equal_to_time2_when_eq() { hour: 22 minute: 11 second: 3 - microsecond: 100 + nanosecond: 100 }) - // Difference is one microsecond + // Difference is one nanosecond t2 := new_time(Time{ year: 2000 month: 5 @@ -329,7 +329,7 @@ fn test_time1_should_be_less_or_equal_to_time2_when_eq() { hour: 22 minute: 11 second: 3 - microsecond: 100 + nanosecond: 100 }) t3 := new_time(Time{ year: 2000 @@ -338,7 +338,7 @@ fn test_time1_should_be_less_or_equal_to_time2_when_eq() { hour: 22 minute: 11 second: 3 - microsecond: 0 + nanosecond: 0 }) // Difference is one second t4 := new_time(Time{ @@ -348,7 +348,7 @@ fn test_time1_should_be_less_or_equal_to_time2_when_eq() { hour: 22 minute: 11 second: 3 - microsecond: 0 + nanosecond: 0 }) assert t1 <= t2 assert t3 <= t4 @@ -362,7 +362,7 @@ fn test_time2_copied_from_time1_should_be_equal() { hour: 22 minute: 11 second: 3 - microsecond: 100 + nanosecond: 100 }) t2 := new_time(t1) assert t2 == t1 @@ -370,8 +370,8 @@ fn test_time2_copied_from_time1_should_be_equal() { fn test_subtract() { d_seconds := 3 - d_microseconds := 13 - duration := d_seconds * second + d_microseconds * microsecond + d_nanoseconds := 13 + duration := d_seconds * second + d_nanoseconds * nanosecond t1 := new_time(Time{ year: 2000 month: 5 @@ -379,9 +379,9 @@ fn test_subtract() { hour: 22 minute: 11 second: 3 - microsecond: 100 + nanosecond: 100 }) - t2 := unix2(i64(t1.unix) + d_seconds, t1.microsecond + d_microseconds) + t2 := unix_nanosecond(i64(t1.unix) + d_seconds, t1.nanosecond + d_nanoseconds) d1 := t2 - t1 d2 := t1 - t2 assert d1 > 0 diff --git a/vlib/time/parse.c.v b/vlib/time/parse.c.v index d4b7fe97df..775d3fef23 100644 --- a/vlib/time/parse.c.v +++ b/vlib/time/parse.c.v @@ -35,13 +35,13 @@ pub fn parse_rfc3339(s string) !Time { } // Check if sn is time only if !parts[0].contains('-') && parts[0].contains(':') { - mut hour_, mut minute_, mut second_, mut microsecond_, mut unix_offset, mut is_local_time := 0, 0, 0, 0, i64(0), true - hour_, minute_, second_, microsecond_, unix_offset, is_local_time = parse_iso8601_time(parts[0])! + mut hour_, mut minute_, mut second_, mut microsecond_, mut nanosecond_, mut unix_offset, mut is_local_time := 0, 0, 0, 0, 0, i64(0), true + hour_, minute_, second_, microsecond_, nanosecond_, unix_offset, is_local_time = parse_iso8601_time(parts[0])! t = new_time(Time{ hour: hour_ minute: minute_ second: second_ - microsecond: microsecond_ + nanosecond: nanosecond_ }) if is_local_time { return t // Time is already local time @@ -52,7 +52,7 @@ pub fn parse_rfc3339(s string) !Time { } else if unix_offset > 0 { unix_time += unix_offset } - t = unix2(i64(unix_time), t.microsecond) + t = unix_nanosecond(i64(unix_time), t.nanosecond) return t } @@ -171,9 +171,9 @@ pub fn parse_iso8601(s string) !Time { return error_invalid_time(12, 'malformed date') } year, month, day := parse_iso8601_date(parts[0])! - mut hour_, mut minute_, mut second_, mut microsecond_, mut unix_offset, mut is_local_time := 0, 0, 0, 0, i64(0), true + mut hour_, mut minute_, mut second_, mut microsecond_, mut nanosecond_, mut unix_offset, mut is_local_time := 0, 0, 0, 0, 0, i64(0), true if parts.len == 2 { - hour_, minute_, second_, microsecond_, unix_offset, is_local_time = parse_iso8601_time(parts[1])! + hour_, minute_, second_, microsecond_, nanosecond_, unix_offset, is_local_time = parse_iso8601_time(parts[1])! } mut t := new_time( year: year @@ -182,7 +182,7 @@ pub fn parse_iso8601(s string) !Time { hour: hour_ minute: minute_ second: second_ - microsecond: microsecond_ + nanosecond: nanosecond_ ) if is_local_time { return t // Time already local time @@ -193,7 +193,7 @@ pub fn parse_iso8601(s string) !Time { } else if unix_offset > 0 { unix_time += unix_offset } - t = unix2(i64(unix_time), t.microsecond) + t = unix_nanosecond(i64(unix_time), t.nanosecond) return t } @@ -237,7 +237,7 @@ fn parse_iso8601_date(s string) !(int, int, int) { return year, month, day } -fn parse_iso8601_time(s string) !(int, int, int, int, i64, bool) { +fn parse_iso8601_time(s string) !(int, int, int, int, int, i64, bool) { hour_ := 0 minute_ := 0 second_ := 0 @@ -281,6 +281,7 @@ fn parse_iso8601_time(s string) !(int, int, int, int, i64, bool) { if count < 4 { return error_invalid_time(10, 'malformed date') } + nanosecond_ = microsecond_ * 1000 } is_local_time := plus_min_z == `a` && count == 4 is_utc := plus_min_z == `Z` && count == 5 @@ -300,5 +301,6 @@ fn parse_iso8601_time(s string) !(int, int, int, int, i64, bool) { if plus_min_z == `+` { unix_offset *= -1 } - return hour_, minute_, second_, microsecond_, unix_offset, is_local_time + // eprintln('parse_iso8601_time s: $s | hour_: $hour_ | minute_: $minute_ | second_: $second_ | microsecond_: $microsecond_ | nanosecond_: $nanosecond_ | unix_offset: $unix_offset | is_local_time: $is_local_time') + return hour_, minute_, second_, microsecond_, nanosecond_, unix_offset, is_local_time } diff --git a/vlib/time/parse_test.v b/vlib/time/parse_test.v index 071adc5d5b..b58766e077 100644 --- a/vlib/time/parse_test.v +++ b/vlib/time/parse_test.v @@ -65,11 +65,11 @@ fn test_parse_iso8601() { ] times := [ [2020, 6, 5, 15, 38, 6, 0], - [2020, 6, 5, 15, 38, 6, 15959], - [2020, 6, 5, 15, 38, 6, 15959], - [2020, 6, 5, 13, 38, 6, 15959], - [2020, 6, 5, 17, 38, 6, 15959], - [2020, 11, 5, 15, 38, 6, 15959], + [2020, 6, 5, 15, 38, 6, 15959000], + [2020, 6, 5, 15, 38, 6, 15959000], + [2020, 6, 5, 13, 38, 6, 15959000], + [2020, 6, 5, 17, 38, 6, 15959000], + [2020, 11, 5, 15, 38, 6, 15959000], ] for i, format in formats { t := time.parse_iso8601(format) or { @@ -89,8 +89,8 @@ fn test_parse_iso8601() { assert t.minute == minute second := times[i][5] assert t.second == second - microsecond := times[i][6] - assert t.microsecond == microsecond + nanosecond := times[i][6] + assert t.nanosecond == nanosecond } } @@ -107,7 +107,7 @@ fn test_parse_iso8601_local() { assert t.hour == 15 assert t.minute == 38 assert t.second == 6 - assert t.microsecond == 15959 + assert t.nanosecond == 15959_000 } fn test_parse_iso8601_invalid() { @@ -145,7 +145,7 @@ fn test_parse_iso8601_date_only() { assert t.hour == 0 assert t.minute == 0 assert t.second == 0 - assert t.microsecond == 0 + assert t.nanosecond == 0 } fn check_invalid_date(s string) { diff --git a/vlib/time/time.c.v b/vlib/time/time.c.v index ce1a72d53c..34c52d5f48 100644 --- a/vlib/time/time.c.v +++ b/vlib/time/time.c.v @@ -53,13 +53,6 @@ pub fn utc() Time { return solaris_utc() } return linux_utc() - /* - // defaults to most common feature, the microsecond precision is not available - // in this API call - t := C.time(0) - _ = C.time(&t) - return unix2(i64(t), 0) - */ } // new_time returns a time struct with the calculated Unix time. @@ -90,7 +83,7 @@ pub fn ticks() i64 { } $else { ts := C.timeval{} C.gettimeofday(&ts, 0) - return i64(ts.tv_sec * u64(1000) + (ts.tv_usec / u64(1000))) + return i64(ts.tv_sec * u64(1000) + (ts.tv_usec / u64(1_000))) } // t := i64(C.mach_absolute_time()) // # Nanoseconds elapsedNano = AbsoluteToNanoseconds( *(AbsoluteTime *) &t ); @@ -105,7 +98,7 @@ pub fn (t Time) str() string { } // convert_ctime converts a C time to V time. -fn convert_ctime(t C.tm, microsecond int) Time { +fn convert_ctime(t C.tm, nanosecond int) Time { return Time{ year: t.tm_year + 1900 month: t.tm_mon + 1 @@ -113,7 +106,7 @@ fn convert_ctime(t C.tm, microsecond int) Time { hour: t.tm_hour minute: t.tm_min second: t.tm_sec - microsecond: microsecond + nanosecond: nanosecond unix: make_unix_time(t) // for the actual code base when we // call convert_ctime, it is always diff --git a/vlib/time/time.v b/vlib/time/time.v index 4407c08cc9..7a0bd68f0d 100644 --- a/vlib/time/time.v +++ b/vlib/time/time.v @@ -40,15 +40,17 @@ pub const ( // Time contains various time units for a point in time. pub struct Time { pub: - year int - month int - day int - hour int - minute int - second int - microsecond int - unix i64 - is_local bool // used to make time.now().local().local() == time.now().local() + year int + month int + day int + hour int + minute int + second int + nanosecond int + unix i64 + is_local bool // used to make time.now().local().local() == time.now().local() + // + microsecond int [deprecated: 'use t.nanosecond / 1000 instead'; deprecated_after: '2023-08-05'] } // FormatDelimiter contains different time formats. @@ -59,6 +61,7 @@ pub enum FormatTime { hhmmss24 hhmmss24_milli hhmmss24_micro + hhmmss24_nano no_time } @@ -99,7 +102,7 @@ pub fn (t Time) smonth() string { return time.months_string[i * 3..(i + 1) * 3] } -// unix_time returns the UNIX time. +// unix_time returns the UNIX time with second resolution. [inline] pub fn (t Time) unix_time() i64 { return t.unix @@ -108,18 +111,39 @@ pub fn (t Time) unix_time() i64 { // unix_time_milli returns the UNIX time with millisecond resolution. [inline] pub fn (t Time) unix_time_milli() i64 { - return t.unix * 1000 + (t.microsecond / 1000) + return t.unix * 1_000 + (i64(t.nanosecond) / 1_000_000) +} + +// unix_time_micro returns the UNIX time with microsecond resolution. +[inline] +pub fn (t Time) unix_time_micro() i64 { + return t.unix * 1_000_000 + (i64(t.nanosecond) / 1_000) +} + +// unix_time_nano returns the UNIX time with nanosecond resolution. +[inline] +pub fn (t Time) unix_time_nano() i64 { + // TODO: use i128 here, when V supports it, since the following expression overflows for years like 3001: + return t.unix * 1_000_000_000 + i64(t.nanosecond) } // add returns a new time with the given duration added. pub fn (t Time) add(d Duration) Time { - microseconds := i64(t.unix) * 1_000_000 + t.microsecond + d.microseconds() - unix := microseconds / 1_000_000 - micro := microseconds % 1_000_000 - if t.is_local { - return unix2(unix, int(micro)).as_local() + // This expression overflows i64 for big years (and we do not have i128 yet): + // nanos := t.unix * 1_000_000_000 + i64(t.nanosecond) <- + // ... so instead, handle the addition manually in parts ¯\_(ツ)_/¯ + mut unixs := t.unix + mut nanos := i64(t.nanosecond) + d.nanoseconds() + unixs += nanos / time.second + nanos = nanos % time.second + if nanos < 0 { + unixs-- + nanos += time.second } - return unix2(unix, int(micro)) + if t.is_local { + return unix_nanosecond(unixs, int(nanos)).as_local() + } + return unix_nanosecond(unixs, int(nanos)) } // add_seconds returns a new time struct with an added number of seconds. @@ -311,9 +335,9 @@ pub fn days_in_month(month int, year int) !int { return res } -// debug returns detailed breakdown of time (`Time{ year: YYYY month: MM day: dd hour: HH: minute: mm second: ss microsecond: micros unix: unix }`) +// debug returns detailed breakdown of time (`Time{ year: YYYY month: MM day: dd hour: HH: minute: mm second: ss nanosecond: nanos unix: unix }`) pub fn (t Time) debug() string { - return 'Time{ year: ${t.year:04} month: ${t.month:02} day: ${t.day:02} hour: ${t.hour:02} minute: ${t.minute:02} second: ${t.second:02} microsecond: ${t.microsecond:06} unix: ${t.unix:07} }' + return 'Time{ year: ${t.year:04} month: ${t.month:02} day: ${t.day:02} hour: ${t.hour:02} minute: ${t.minute:02} second: ${t.second:02} nanosecond: ${t.nanosecond:09} unix: ${t.unix:07} }' } // A lot of these are taken from the Go library. @@ -326,6 +350,7 @@ pub const ( second = Duration(1000 * millisecond) minute = Duration(60 * second) hour = Duration(60 * minute) + // day = Duration(24 * hour) infinite = Duration(i64(9223372036854775807)) ) @@ -348,23 +373,22 @@ pub fn (d Duration) milliseconds() i64 { // consider all of them in sub-one intervals // seconds returns the duration as a floating point number of seconds. pub fn (d Duration) seconds() f64 { - sec := d / time.second - nsec := d % time.second - return f64(sec) + f64(nsec) / time.second + return f64(d) / f64(time.second) } // minutes returns the duration as a floating point number of minutes. pub fn (d Duration) minutes() f64 { - min := d / time.minute - nsec := d % time.minute - return f64(min) + f64(nsec) / time.minute + return f64(d) / f64(time.minute) } // hours returns the duration as a floating point number of hours. pub fn (d Duration) hours() f64 { - hr := d / time.hour - nsec := d % time.hour - return f64(hr) + f64(nsec) / time.hour + return f64(d) / f64(time.hour) +} + +// days returns the duration as a floating point number of days. +pub fn (d Duration) days() f64 { + return f64(d) / f64(time.hour * 24) } // str pretty prints the duration @@ -412,6 +436,35 @@ pub fn (d Duration) str() string { return '${ns}ns' } +// debug returns a detailed breakdown of the Duration, as: 'Duration: - 50days, 4h, 3m, 7s, 541ms, 78us, 9ns' +pub fn (d Duration) debug() string { + mut res := []string{} + mut x := i64(d) + mut sign := '' + if x < 0 { + sign = '- ' + x = -x + } + for label, v in { + 'days': 24 * time.hour + 'h': time.hour + 'm': time.minute + 's': time.second + 'ms': time.millisecond + 'us': time.microsecond + } { + if x > v { + xx := x / v + x = x % v + res << xx.str() + label + } + } + if x > 0 { + res << '${x}ns' + } + return 'Duration: ${sign}${res.join(', ')}' +} + // offset returns time zone UTC offset in seconds. pub fn offset() int { t := utc() diff --git a/vlib/time/time_addition_test.v b/vlib/time/time_addition_test.v index 0976262f48..5f5c7f11e9 100644 --- a/vlib/time/time_addition_test.v +++ b/vlib/time/time_addition_test.v @@ -3,8 +3,6 @@ import time fn test_add_to_day_in_the_previous_century() { a := time.parse_iso8601('1900-01-01')! aa := a.add_days(180) - dump(a.debug()) - dump(aa.debug()) assert aa.ymmdd() == '1900-06-30' } @@ -23,6 +21,8 @@ fn test_add_to_day_in_the_recent_past() { fn test_add_to_day_in_the_future_1() { a := time.parse_iso8601('3000-11-01')! aa := a.add_days(180) + dump(a.debug()) + dump(aa.debug()) assert aa.ymmdd() == '3001-04-30' } diff --git a/vlib/time/time_darwin.c.v b/vlib/time/time_darwin.c.v index 3de9657c0e..c4b9bf50ae 100644 --- a/vlib/time/time_darwin.c.v +++ b/vlib/time/time_darwin.c.v @@ -2,11 +2,10 @@ module time #include -const ( - // start_time is needed on Darwin and Windows because of potential overflows - start_time = C.mach_absolute_time() - time_base = init_time_base() -) +// start_time is needed on Darwin and Windows because of potential overflows +const start_time = C.mach_absolute_time() + +const time_base = init_time_base() [typedef] struct C.mach_timebase_info_data_t { @@ -25,11 +24,6 @@ struct InternalTimeBase { denom u32 = 1 } -pub struct C.timeval { - tv_sec u64 - tv_usec u64 -} - fn init_time_base() C.mach_timebase_info_data_t { tb := C.mach_timebase_info_data_t{} C.mach_timebase_info(&tb) @@ -62,29 +56,22 @@ fn vpc_now_darwin() u64 { return (tm - time.start_time) * time.time_base.numer / time.time_base.denom } -// darwin_now returns a better precision current time for Darwin based operating system -// this should be implemented with native system calls eventually -// but for now a bit tweaky. It uses the deprecated gettimeofday clock to get -// the microseconds seconds part and converts to local time +// darwin_now returns a better precision current time for macos fn darwin_now() Time { - // get the high precision time as UTC clock - tv := C.timeval{} - C.gettimeofday(&tv, 0) + // get the high precision time as UTC realtime clock, and use the nanoseconds part + mut ts := C.timespec{} + C.clock_gettime(C.CLOCK_REALTIME, &ts) loc_tm := C.tm{} - asec := voidptr(&tv.tv_sec) - C.localtime_r(asec, &loc_tm) - return convert_ctime(loc_tm, int(tv.tv_usec)) + C.localtime_r(voidptr(&ts.tv_sec), &loc_tm) + return convert_ctime(loc_tm, int(ts.tv_nsec)) } -// darwin_utc returns a better precision current time for Darwin based operating system -// this should be implemented with native system calls eventually -// but for now a bit tweaky. It uses the deprecated gettimeofday clock to get -// the microseconds seconds part and normal local time to get correct local time +// darwin_utc returns a better precision current time for macos fn darwin_utc() Time { // get the high precision time as UTC clock - tv := C.timeval{} - C.gettimeofday(&tv, 0) - return unix2(i64(tv.tv_sec), int(tv.tv_usec)) + mut ts := C.timespec{} + C.clock_gettime(C.CLOCK_REALTIME, &ts) + return unix_nanosecond(i64(ts.tv_sec), int(ts.tv_nsec)) } // dummy to compile with all compilers diff --git a/vlib/time/time_nix.c.v b/vlib/time/time_nix.c.v index 0b2d24b257..0065468bfd 100644 --- a/vlib/time/time_nix.c.v +++ b/vlib/time/time_nix.c.v @@ -36,7 +36,7 @@ pub fn (t Time) local() Time { } loc_tm := C.tm{} C.localtime_r(voidptr(&t.unix), &loc_tm) - return convert_ctime(loc_tm, t.microsecond) + return convert_ctime(loc_tm, t.nanosecond) } // in most systems, these are __quad_t, which is an i64 @@ -58,7 +58,7 @@ pub fn sys_mono_now() u64 { } $else { ts := C.timespec{} C.clock_gettime(C.CLOCK_MONOTONIC, &ts) - return u64(ts.tv_sec) * 1000000000 + u64(ts.tv_nsec) + return u64(ts.tv_sec) * 1_000_000_000 + u64(ts.tv_nsec) } } @@ -68,7 +68,7 @@ pub fn sys_mono_now() u64 { fn vpc_now() u64 { ts := C.timespec{} C.clock_gettime(C.CLOCK_MONOTONIC, &ts) - return u64(ts.tv_sec) * 1000000000 + u64(ts.tv_nsec) + return u64(ts.tv_sec) * 1_000_000_000 + u64(ts.tv_nsec) } // The linux_* functions are placed here, since they're used on Android as well @@ -83,7 +83,7 @@ fn linux_now() Time { C.clock_gettime(C.CLOCK_REALTIME, &ts) loc_tm := C.tm{} C.localtime_r(voidptr(&ts.tv_sec), &loc_tm) - return convert_ctime(loc_tm, int(ts.tv_nsec / 1000)) + return convert_ctime(loc_tm, int(ts.tv_nsec)) } fn linux_utc() Time { @@ -91,7 +91,7 @@ fn linux_utc() Time { // and use the nanoseconds part mut ts := C.timespec{} C.clock_gettime(C.CLOCK_REALTIME, &ts) - return unix2(i64(ts.tv_sec), int(ts.tv_nsec / 1000)) + return unix_nanosecond(i64(ts.tv_sec), int(ts.tv_nsec)) } // dummy to compile with all compilers @@ -104,12 +104,6 @@ fn win_utc() Time { return Time{} } -// dummy to compile with all compilers -pub struct C.timeval { - tv_sec u64 - tv_usec u64 -} - // return absolute timespec for now()+d pub fn (d Duration) timespec() C.timespec { mut ts := C.timespec{} diff --git a/vlib/time/time_solaris.c.v b/vlib/time/time_solaris.c.v index fd037f46dc..c977359515 100644 --- a/vlib/time/time_solaris.c.v +++ b/vlib/time/time_solaris.c.v @@ -10,7 +10,7 @@ fn solaris_now() Time { C.clock_gettime(C.CLOCK_REALTIME, &ts) loc_tm := C.tm{} C.localtime_r(voidptr(&ts.tv_sec), &loc_tm) - return convert_ctime(loc_tm, int(ts.tv_nsec / 1000)) + return convert_ctime(loc_tm, int(ts.tv_nsec)) } fn solaris_utc() Time { @@ -18,7 +18,7 @@ fn solaris_utc() Time { // and use the nanoseconds part mut ts := C.timespec{} C.clock_gettime(C.CLOCK_REALTIME, &ts) - return unix2(i64(ts.tv_sec), int(ts.tv_nsec / 1000)) + return unix_nanosecond(i64(ts.tv_sec), int(ts.tv_nsec)) } // dummy to compile with all compilers diff --git a/vlib/time/time_test.v b/vlib/time/time_test.v index f1caa2d9b4..23c13351c5 100644 --- a/vlib/time/time_test.v +++ b/vlib/time/time_test.v @@ -1,18 +1,16 @@ import time import math -const ( - time_to_test = time.Time{ - year: 1980 - month: 7 - day: 11 - hour: 21 - minute: 23 - second: 42 - microsecond: 123456 - unix: 332198622 - } -) +const time_to_test = time.Time{ + year: 1980 + month: 7 + day: 11 + hour: 21 + minute: 23 + second: 42 + nanosecond: 123456789 + unix: 332198622 +} fn test_is_leap_year() { // 1996 % 4 = 0 and 1996 % 100 > 0 @@ -83,6 +81,14 @@ fn test_unix() { assert t6.second == 29 } +fn test_format_rfc3339() { + // assert '1980-07-11T19:23:42.123Z' + res := time_to_test.format_rfc3339() + assert res.ends_with('23:42.123Z') + assert res.starts_with('1980-07-1') + assert res.contains('T') +} + fn test_format_ss() { assert '11.07.1980 21:23:42' == time_to_test.get_fmt_str(.dot, .hhmmss24, .ddmmyyyy) } @@ -93,20 +99,18 @@ fn test_format_ss_milli() { assert '1980-07-11 21:23:42.123' == time_to_test.format_ss_milli() } -fn test_format_rfc3339() { - // assert '1980-07-11T19:23:42.123Z' - res := time_to_test.format_rfc3339() - assert res.ends_with('23:42.123Z') - assert res.starts_with('1980-07-1') - assert res.contains('T') -} - fn test_format_ss_micro() { assert '11.07.1980 21:23:42.123456' == time_to_test.get_fmt_str(.dot, .hhmmss24_micro, .ddmmyyyy) assert '1980-07-11 21:23:42.123456' == time_to_test.format_ss_micro() } +fn test_format_ss_nano() { + assert '11.07.1980 21:23:42.123456789' == time_to_test.get_fmt_str(.dot, .hhmmss24_nano, + .ddmmyyyy) + assert '1980-07-11 21:23:42.123456789' == time_to_test.format_ss_nano() +} + fn test_smonth() { month_names := ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'] @@ -180,21 +184,29 @@ fn test_weekday_str() { fn test_add() { d_seconds := 3 - d_microseconds := 13 - duration := time.Duration(d_seconds * time.second + d_microseconds * time.microsecond) + d_nanoseconds := 13 + duration := time.Duration(d_seconds * time.second + d_nanoseconds * time.nanosecond) + // dump(duration.debug()) t1 := time_to_test + // dump(t1.debug()) t2 := time_to_test.add(duration) + // dump(t2.debug()) assert t2.second == t1.second + d_seconds - assert t2.microsecond == t1.microsecond + d_microseconds + assert t2.nanosecond == t1.nanosecond + d_nanoseconds assert t2.unix == t1.unix + d_seconds assert t2.is_local == t1.is_local + // t3 := time_to_test.add(-duration) + // dump(t3.debug()) assert t3.second == t1.second - d_seconds - assert t3.microsecond == t1.microsecond - d_microseconds + assert t3.nanosecond == t1.nanosecond - d_nanoseconds assert t3.unix == t1.unix - d_seconds assert t3.is_local == t1.is_local + // t4 := time_to_test.as_local() + // dump(t4.debug()) t5 := t4.add(duration) + // dump(t5.debug()) assert t5.is_local == t4.is_local } @@ -220,13 +232,14 @@ fn test_now() { assert now.minute < 60 assert now.second >= 0 assert now.second <= 60 // <= 60 cause of leap seconds - assert now.microsecond >= 0 - assert now.microsecond < 1000000 + assert now.nanosecond >= 0 + assert now.nanosecond < time.second } fn test_utc() { now := time.utc() // The year the test was built + // dump(now.debug()) assert now.year >= 2020 assert now.month > 0 assert now.month <= 12 @@ -234,20 +247,20 @@ fn test_utc() { assert now.minute < 60 assert now.second >= 0 assert now.second <= 60 // <= 60 cause of leap seconds - assert now.microsecond >= 0 - assert now.microsecond < 1000000 + assert now.nanosecond >= 0 + assert now.nanosecond < time.second } fn test_unix_time() { t1 := time.utc() time.sleep(50 * time.millisecond) t2 := time.utc() - eprintln('t1: ${t1}') - eprintln('t2: ${t2}') + eprintln(' t1: ${t1}') + eprintln(' t2: ${t2}') ut1 := t1.unix_time() ut2 := t2.unix_time() - eprintln('ut1: ${ut1}') - eprintln('ut2: ${ut2}') + eprintln(' ut1: ${ut1}') + eprintln(' ut2: ${ut2}') assert ut2 - ut1 < 2 // utm1 := t1.unix_time_milli() diff --git a/vlib/time/time_windows.c.v b/vlib/time/time_windows.c.v index 0d76b4e8d1..7f7c69ea9c 100644 --- a/vlib/time/time_windows.c.v +++ b/vlib/time/time_windows.c.v @@ -39,6 +39,8 @@ fn C.SystemTimeToTzSpecificLocalTime(lpTimeZoneInformation &C.TIME_ZONE_INFORMAT fn C.localtime_s(t &C.time_t, tm &C.tm) +fn C.timespec_get(t &C.timespec, base int) int + const ( // start_time is needed on Darwin and Windows because of potential overflows start_time = init_win_time_start() @@ -107,7 +109,7 @@ pub fn (t Time) local() Time { hour: u16(t.hour) minute: u16(t.minute) second: u16(t.second) - millisecond: u16(t.microsecond / 1000) + millisecond: u16(t.nanosecond / 1_000_000) } st_local := SystemTime{} C.SystemTimeToTzSpecificLocalTime(unsafe { nil }, &st_utc, &st_local) @@ -118,7 +120,7 @@ pub fn (t Time) local() Time { hour: st_local.hour minute: st_local.minute second: st_local.second // These are the same - microsecond: int(st_local.millisecond) * 1000 + nanosecond: int(st_local.millisecond) * 1_000_000 unix: st_local.unix_time() } return t_local @@ -141,7 +143,7 @@ fn win_now() Time { hour: st_local.hour minute: st_local.minute second: st_local.second - microsecond: int(st_local.millisecond) * 1000 + nanosecond: int(st_local.millisecond) * 1_000_000 unix: st_local.unix_time() is_local: true } @@ -163,7 +165,7 @@ fn win_utc() Time { hour: st_utc.hour minute: st_utc.minute second: st_utc.second - microsecond: int(st_utc.millisecond) * 1000 + nanosecond: int(st_utc.millisecond) * 1_000_000 unix: st_utc.unix_time() is_local: false } @@ -213,12 +215,6 @@ fn solaris_utc() Time { return Time{} } -// dummy to compile with all compilers -pub struct C.timeval { - tv_sec u64 - tv_usec u64 -} - // sleep makes the calling thread sleep for a given duration (in nanoseconds). pub fn sleep(duration Duration) { C.Sleep(int(duration / millisecond)) diff --git a/vlib/time/unix.v b/vlib/time/unix.v index 7add150e43..b0e7403051 100644 --- a/vlib/time/unix.v +++ b/vlib/time/unix.v @@ -3,7 +3,7 @@ // that can be found in the LICENSE file. module time -// unix returns a time struct from Unix time. +// unix returns a time struct from an Unix timestamp (number of seconds since 1970-01-01) pub fn unix(abs i64) Time { // Split into day and time mut day_offset := abs / seconds_per_day @@ -24,8 +24,20 @@ pub fn unix(abs i64) Time { } } -// unix2 returns a time struct from Unix time and microsecond value +// unix2 returns a Time struct, given an Unix timestamp in seconds, and a microsecond value +[deprecated: 'use unix_microsecond(unix_ts, us) instead'] +[deprecated_after: '2023-09-05'] pub fn unix2(abs i64, microsecond int) Time { + return unix_nanosecond(abs, microsecond * 1000) +} + +// unix_microsecond returns a Time struct, given an Unix timestamp in seconds, and a microsecond value +pub fn unix_microsecond(abs i64, microsecond int) Time { + return unix_nanosecond(abs, microsecond * 1000) +} + +// unix_nanosecond returns a Time struct, given an Unix timestamp in seconds, and a nanosecond value +pub fn unix_nanosecond(abs i64, nanosecond int) Time { // Split into day and time mut day_offset := abs / seconds_per_day if abs % seconds_per_day < 0 { @@ -41,7 +53,7 @@ pub fn unix2(abs i64, microsecond int) Time { hour: hr minute: min second: sec - microsecond: microsecond + nanosecond: nanosecond unix: abs } }