From 2b4253caf9e6846b88310575768449191d8a9ba9 Mon Sep 17 00:00:00 2001 From: Leo Developer Date: Thu, 4 Sep 2025 23:22:52 +0200 Subject: [PATCH] time: always return utc() timezone for Time.unix/0 (fix #17784) (#25233) --- vlib/orm/orm_test.v | 4 +++- vlib/time/operator.v | 7 +++++-- vlib/time/private_test.c.v | 14 -------------- vlib/time/time.v | 30 ++++++++++++++++++++++++------ vlib/time/time_format_test.v | 2 +- vlib/time/time_test.c.v | 2 +- vlib/time/time_test.v | 17 ----------------- vlib/v/gen/c/json.v | 8 ++++---- 8 files changed, 38 insertions(+), 46 deletions(-) delete mode 100644 vlib/time/private_test.c.v diff --git a/vlib/orm/orm_test.v b/vlib/orm/orm_test.v index c6eebc747a..5b5be2180f 100644 --- a/vlib/orm/orm_test.v +++ b/vlib/orm/orm_test.v @@ -393,7 +393,9 @@ fn test_orm() { // Note: usually updated_time_mod.created != t, because t has // its microseconds set, while the value retrieved from the DB // has them zeroed, because the db field resolution is seconds. - assert modules.first().created.format_ss() == t.format_ss() + // Note: the database also stores the time in UTC, so the + // comparison must be done on the unix timestamp. + assert modules.first().created.unix() == t.unix() users = sql db { select from User where (name == 'Sam' && is_customer == true) || id == 1 diff --git a/vlib/time/operator.v b/vlib/time/operator.v index 8c5fab8e89..8242e3444e 100644 --- a/vlib/time/operator.v +++ b/vlib/time/operator.v @@ -3,13 +3,16 @@ 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.nanosecond == t2.nanosecond + return t1.is_local == t2.is_local && t1.local_unix() == t2.local_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.nanosecond < t2.nanosecond) + t1u := t1.unix() + t2u := t2.unix() + return t1u < t2u || (t1u == t2u && t1.nanosecond < t2.nanosecond) } // Time subtract using operator overloading. diff --git a/vlib/time/private_test.c.v b/vlib/time/private_test.c.v deleted file mode 100644 index 7e69c40374..0000000000 --- a/vlib/time/private_test.c.v +++ /dev/null @@ -1,14 +0,0 @@ -// tests that use and test private functions -module time - -// test the old behavior is same as new, the unix time should always be local time -fn test_new_is_same_as_old_for_all_platforms() { - t := C.time(0) - tm := C.localtime(&t) - old_time := convert_ctime(tm, 0) - new_time := now() - diff := new_time.unix - old_time.unix - // could in very rare cases be that the second changed between calls - dump(diff) - assert (diff >= -2 && diff <= 2) == true -} diff --git a/vlib/time/time.v b/vlib/time/time.v index 9e18c8ef52..c9b1059a4d 100644 --- a/vlib/time/time.v +++ b/vlib/time/time.v @@ -106,6 +106,12 @@ pub fn (t Time) smonth() string { // unix returns the UNIX time with second resolution. @[inline] pub fn (t Time) unix() i64 { + return time_with_unix(t.local_to_utc()).unix +} + +// local_unix returns the UNIX local time with second resolution. +@[inline] +pub fn (t Time) local_unix() i64 { return time_with_unix(t).unix } @@ -135,14 +141,26 @@ pub fn (t Time) add(duration_in_nanosecond Duration) Time { // ... so instead, handle the addition manually in parts ¯\_(ツ)_/¯ mut increased_time_nanosecond := i64(t.nanosecond) + duration_in_nanosecond.nanoseconds() // increased_time_second - mut increased_time_second := t.unix() + (increased_time_nanosecond / second) + mut increased_time_second := t.local_unix() + (increased_time_nanosecond / second) increased_time_nanosecond = increased_time_nanosecond % second if increased_time_nanosecond < 0 { increased_time_second-- increased_time_nanosecond += second } res := unix_nanosecond(increased_time_second, int(increased_time_nanosecond)) - return if t.is_local { res.as_local() } else { res } + + if t.is_local { + // we need to reset unix to 0, because we don't know the offset + // and we can't calculate it without it without causing infinite recursion + // so unfortunately we need to recalculate unix next time it is needed + return Time{ + ...res + is_local: true + unix: 0 + } + } + + return res } // add_seconds returns a new time struct with an added number of seconds. @@ -177,7 +195,7 @@ pub fn since(t Time) Duration { // ``` pub fn (t Time) relative() string { znow := now() - mut secs := znow.unix - t.unix() + mut secs := znow.unix() - t.unix() mut prefix := '' mut suffix := '' if secs < 0 { @@ -239,7 +257,7 @@ pub fn (t Time) relative() string { // ``` pub fn (t Time) relative_short() string { znow := now() - mut secs := znow.unix - t.unix() + mut secs := znow.unix() - t.unix() mut prefix := '' mut suffix := '' if secs < 0 { @@ -364,9 +382,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 nanosecond: nanos 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 is_local: false }`). 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} nanosecond: ${t.nanosecond:09} 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} is_local: ${t.is_local} }' } // offset returns time zone UTC offset in seconds. diff --git a/vlib/time/time_format_test.v b/vlib/time/time_format_test.v index 8ca07ac5d4..f21be4061a 100644 --- a/vlib/time/time_format_test.v +++ b/vlib/time/time_format_test.v @@ -12,7 +12,7 @@ const time_to_test = time.Time{ fn test_now_format() { t := time.now() u := t.unix() - assert t.format() == time.unix(int(u)).format() + assert t.format() == time.unix(int(u)).utc_to_local().format() } fn test_format() { diff --git a/vlib/time/time_test.c.v b/vlib/time/time_test.c.v index 2222e1e2d6..7bf896608d 100644 --- a/vlib/time/time_test.c.v +++ b/vlib/time/time_test.c.v @@ -15,7 +15,7 @@ fn test_tm_gmtoff() { dump(t2) dump(t1.nanosecond) dump(t2.nanosecond) - diff := int(t1.unix() - t2.unix()) + diff := int(t1.local_unix() - t2.unix()) dump(diff) dump(info.tm_gmtoff) assert diff in [info.tm_gmtoff - 1, info.tm_gmtoff, info.tm_gmtoff + 1] diff --git a/vlib/time/time_test.v b/vlib/time/time_test.v index 34554cab82..e33b0e5c2e 100644 --- a/vlib/time/time_test.v +++ b/vlib/time/time_test.v @@ -90,17 +90,10 @@ fn test_unix() { assert t6.hour == 6 assert t6.minute == 9 assert t6.second == 29 - assert local_time_to_test.unix() == 332198622 assert utc_time_to_test.unix() == 332198622 } fn test_format_rfc3339() { - // assert '1980-07-11T19:23:42.123Z' - res := local_time_to_test.format_rfc3339() - assert res.ends_with('23:42.123Z') - assert res.starts_with('1980-07-1') - assert res.contains('T') - // assert '1980-07-11T19:23:42.123Z' utc_res := utc_time_to_test.format_rfc3339() assert utc_res.ends_with('23:42.123Z') @@ -109,11 +102,6 @@ fn test_format_rfc3339() { } fn test_format_rfc3339_micro() { - res := local_time_to_test.format_rfc3339_micro() - assert res.ends_with('23:42.123456Z') - assert res.starts_with('1980-07-1') - assert res.contains('T') - utc_res := utc_time_to_test.format_rfc3339_micro() assert utc_res.ends_with('23:42.123456Z') assert utc_res.starts_with('1980-07-1') @@ -121,11 +109,6 @@ fn test_format_rfc3339_micro() { } fn test_format_rfc3339_nano() { - res := local_time_to_test.format_rfc3339_nano() - assert res.ends_with('23:42.123456789Z') - assert res.starts_with('1980-07-1') - assert res.contains('T') - utc_res := utc_time_to_test.format_rfc3339_nano() assert utc_res.ends_with('23:42.123456789Z') assert utc_res.starts_with('1980-07-1') diff --git a/vlib/v/gen/c/json.v b/vlib/v/gen/c/json.v index db30ad49bd..7b102074aa 100644 --- a/vlib/v/gen/c/json.v +++ b/vlib/v/gen/c/json.v @@ -417,7 +417,7 @@ fn (mut g Gen) gen_sumtype_enc_dec(utyp ast.Type, sym ast.TypeSymbol, mut enc st if variant_sym.kind == .enum { enc.writeln('\t\tcJSON_AddItemToObject(o, "${unmangled_variant_name}", ${js_enc_name('u64')}(*${var_data}${field_op}_${variant_typ}));') } else if variant_sym.name == 'time.Time' { - enc.writeln('\t\tcJSON_AddItemToObject(o, "${unmangled_variant_name}", ${js_enc_name('i64')}(${var_data}${field_op}_${variant_typ}->__v_unix));') + enc.writeln('\t\tcJSON_AddItemToObject(o, "${unmangled_variant_name}", ${js_enc_name('i64')}(time__Time_unix(*${var_data}${field_op}_${variant_typ})));') } else { enc.writeln('\t\tcJSON_AddItemToObject(o, "${unmangled_variant_name}", ${js_enc_name(variant_typ)}(*${var_data}${field_op}_${variant_typ}));') } @@ -442,7 +442,7 @@ fn (mut g Gen) gen_sumtype_enc_dec(utyp ast.Type, sym ast.TypeSymbol, mut enc st } } else if variant_sym.name == 'time.Time' { enc.writeln('\t\tcJSON_AddItemToObject(o, "_type", cJSON_CreateString("${unmangled_variant_name}"));') - enc.writeln('\t\tcJSON_AddItemToObject(o, "value", ${js_enc_name('i64')}(${var_data}${field_op}_${variant_typ}->__v_unix));') + enc.writeln('\t\tcJSON_AddItemToObject(o, "value", ${js_enc_name('i64')}(time__Time_unix(*${var_data}${field_op}_${variant_typ})));') } else { enc.writeln('\t\tcJSON_free(o);') enc.writeln('\t\to = ${js_enc_name(variant_typ)}(*${var_data}${field_op}_${variant_typ});') @@ -954,9 +954,9 @@ fn (mut g Gen) gen_struct_enc_dec(utyp ast.Type, type_info ast.TypeInfo, styp st // time struct requires special treatment // it has to be encoded as a unix timestamp number if is_option { - enc.writeln('${indent}cJSON_AddItemToObject(o, "${name}", json__encode_u64((*(${g.base_type(field.typ)}*)(${prefix_enc}${op}${c_name(field.name)}.data)).__v_unix));') + enc.writeln('${indent}cJSON_AddItemToObject(o, "${name}", json__encode_u64(time__Time_unix(*(${g.base_type(field.typ)}*)(${prefix_enc}${op}${c_name(field.name)}.data))));') } else { - enc.writeln('${indent}cJSON_AddItemToObject(o, "${name}", json__encode_u64(${prefix_enc}${op}${c_name(field.name)}.__v_unix));') + enc.writeln('${indent}cJSON_AddItemToObject(o, "${name}", json__encode_u64(time__Time_unix(${prefix_enc}${op}${c_name(field.name)})));') } } else { if !field.typ.is_any_kind_of_pointer() {