Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion vlib/encoding/cbor/generic.v
Original file line number Diff line number Diff line change
Expand Up @@ -587,7 +587,7 @@ fn (mut u Unpacker) unpack_into[T](mut out T) ! {
// session is encoded as the correct instant rather than as wall-clock
// digits without an offset.
fn format_rfc3339_nano(t time.Time) string {
utc := if t.is_local { t.local_to_utc() } else { t }
utc := if t.is_local || t.location() != none { t.local_to_utc() } else { t }
return '${utc.year:04d}-${utc.month:02d}-${utc.day:02d}T${utc.hour:02d}:${utc.minute:02d}:${utc.second:02d}.${utc.nanosecond:09d}Z'
}

Expand Down
26 changes: 26 additions & 0 deletions vlib/encoding/cbor/tests/time_test.v
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ module main

import encoding.cbor
import encoding.hex
import os
import time

fn h(s string) []u8 {
Expand Down Expand Up @@ -37,3 +38,28 @@ fn test_time_decode_tag1_float() {
// Sub-second component is non-zero (~500ms).
assert got.nanosecond > 0
}

fn test_time_encode_tag0_zoned_subsecond_uses_utc_instant() {
old_zoneinfo := os.getenv_opt('ZONEINFO')
os.setenv('ZONEINFO', os.join_path(@VEXEROOT, 'vlib', 'time', 'tzdata', 'zoneinfo.zip'), true)
defer {
if old := old_zoneinfo {
os.setenv('ZONEINFO', old, true)
} else {
os.unsetenv('ZONEINFO')
}
}
loc := time.load_location('Asia/Shanghai')!
t := time.unix_nanosecond(1_704_067_200, 1).in(loc)!
bytes := cbor.encode[time.Time](t, cbor.EncodeOpts{})!
v := cbor.decode[cbor.Value](bytes, cbor.DecodeOpts{})!
assert v is cbor.Tag
if v is cbor.Tag {
assert v.number == 0
content := v.content()
assert content is cbor.Text
if content is cbor.Text {
assert content.value == '2024-01-01T00:00:00.000000001Z'
}
}
}
33 changes: 33 additions & 0 deletions vlib/time/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,39 @@ Omitted `month` and `day` values default to `1`, and out-of-range values panic.
Use `t.is_zero()` to check whether a `time.Time` is still its zero value before formatting or
serializing it.

IANA time zone data can be loaded by name and used to convert Unix timestamps
or UTC `Time` values to that location's calendar time. `load_location` searches
`ZONEINFO`, system zoneinfo paths, and V's installed `zoneinfo.zip`. Programs
that need embedded time zone data (no system zoneinfo, portable binaries) can
import `time.tzdata`.

```v
import time
import time.tzdata as _

shanghai := time.load_location('Asia/Shanghai')!
local := time.unix(1_704_067_200).in(shanghai)!
assert local.format_ss() == '2024-01-01 08:00:00'
assert local.unix() == 1_704_067_200
assert (local.zone()!).offset == 28_800
```

IANA-zoned values keep `unix` as the absolute UTC epoch instant. Calendar
fields (`year`, `hour`, ...) are wall time in that location. Prefer
`t.location()` / `t.zone()` over the older `is_local` flag: `is_local` only
marks system-local wall time with a fixed process offset and is left `false`
on IANA-zoned `Time` values.

The bundled `vlib/time/tzdata/zoneinfo.zip` is a store-only (uncompressed) zip
of IANA zoneinfo files for offline use via `import time.tzdata`. Refresh it
from a full system zoneinfo tree when updating tzdata. On macOS, prefer
`/usr/share/zoneinfo.default` over `/usr/share/zoneinfo`, because the latter may
contain truncated files with fixed POSIX tails. For example:

```sh
cd /usr/share/zoneinfo.default && zip -0 -r /path/to/v/vlib/time/tzdata/zoneinfo.zip .
```

Another very useful feature of the `time` module is the stop watch,
for when you want to measure short time periods, elapsed while you
executed other tasks. [See](https://play.vlang.io/?query=f6c008bc34):
Expand Down
32 changes: 19 additions & 13 deletions vlib/time/format.v
Original file line number Diff line number Diff line change
Expand Up @@ -184,7 +184,7 @@ pub fn (t Time) format_rfc3339() string {
}

t_ := time_with_unix(t)
if t_.is_local {
if t_.is_local || t_.location() != none {
utc_time := t_.local_to_utc()
int_to_byte_array_no_pad(utc_time.year, mut buf, 4)
int_to_byte_array_no_pad(utc_time.month, mut buf, 7)
Expand Down Expand Up @@ -217,7 +217,7 @@ pub fn (t Time) format_rfc3339_micro() string {
}

t_ := time_with_unix(t)
if t_.is_local {
if t_.is_local || t_.location() != none {
utc_time := t_.local_to_utc()
int_to_byte_array_no_pad(utc_time.year, mut buf, 4)
int_to_byte_array_no_pad(utc_time.month, mut buf, 7)
Expand Down Expand Up @@ -250,7 +250,7 @@ pub fn (t Time) format_rfc3339_nano() string {
}

t_ := time_with_unix(t)
if t_.is_local {
if t_.is_local || t_.location() != none {
utc_time := t_.local_to_utc()
Comment thread
guweigang marked this conversation as resolved.
int_to_byte_array_no_pad(utc_time.year, mut buf, 4)
int_to_byte_array_no_pad(utc_time.month, mut buf, 7)
Expand Down Expand Up @@ -720,9 +720,10 @@ pub fn (t Time) get_fmt_str(fmt_dlmtr FormatDelimiter, fmt_time FormatTime, fmt_
@[deprecated: 'use `http_header_string()` instead']
@[deprecated_after: '2026-09-30']
pub fn (t Time) utc_string() string {
day_str := t.weekday_str()
month_str := t.smonth()
utc_string := '${day_str}, ${t.day} ${month_str} ${t.year} ${t.hour:02d}:${t.minute:02d}:${t.second:02d} UTC'
t_ := if t.location() != none { t.local_to_utc() } else { t }
day_str := t_.weekday_str()
month_str := t_.smonth()
utc_string := '${day_str}, ${t_.day} ${month_str} ${t_.year} ${t_.hour:02d}:${t_.minute:02d}:${t_.second:02d} UTC'
return utc_string
}

Expand Down Expand Up @@ -750,22 +751,27 @@ pub fn (t Time) write_http_header(dst &u8, dst_len int) ! {
if dst_len < http_date_len {
return error('time.write_http_header: dst_len must be >= 29')
}
day_str := long_days[iclamp(0, t.day_of_week() - 1, 6)] // read in place: no substr
if t.location() != none {
unsafe { t.local_to_utc().write_http_header(dst, dst_len)! }
return
}
t_ := t
day_str := long_days[iclamp(0, t_.day_of_week() - 1, 6)] // read in place: no substr
// months_string is indexed in place (no smonth() substr allocation); out-of-range
// months keep smonth()'s historical '---' fallback.
mi := if t.month >= 1 && t.month <= 12 { (t.month - 1) * 3 } else { -1 }
mi := if t_.month >= 1 && t_.month <= 12 { (t_.month - 1) * 3 } else { -1 }
m0 := if mi >= 0 { months_string[mi] } else { `-` }
m1 := if mi >= 0 { months_string[mi + 1] } else { `-` }
m2 := if mi >= 0 { months_string[mi + 2] } else { `-` }

mut buf := [day_str[0], day_str[1], day_str[2], `,`, ` `, `0`, `0`, ` `, m0, m1, m2, ` `, `0`,
`0`, `0`, `0`, ` `, `0`, `0`, `:`, `0`, `0`, `:`, `0`, `0`, ` `, `G`, `M`, `T`]!
unsafe {
int_to_ptr_byte_array_no_pad(t.day, &buf[5], 2)
int_to_ptr_byte_array_no_pad(t.year, &buf[12], 4)
int_to_ptr_byte_array_no_pad(t.hour, &buf[17], 2)
int_to_ptr_byte_array_no_pad(t.minute, &buf[20], 2)
int_to_ptr_byte_array_no_pad(t.second, &buf[23], 2)
int_to_ptr_byte_array_no_pad(t_.day, &buf[5], 2)
int_to_ptr_byte_array_no_pad(t_.year, &buf[12], 4)
int_to_ptr_byte_array_no_pad(t_.hour, &buf[17], 2)
int_to_ptr_byte_array_no_pad(t_.minute, &buf[20], 2)
int_to_ptr_byte_array_no_pad(t_.second, &buf[23], 2)
// plain byte loop instead of vmemcpy: compiles on every backend (JS has
// no vmemcpy) and C compilers turn it into a memcpy at -prod anyway
for i in 0 .. 29 {
Expand Down
6 changes: 6 additions & 0 deletions vlib/time/operator.v
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,12 @@ module time
// operator `==` returns true if provided time is equal to time
@[inline]
pub fn (t1 Time) == (t2 Time) bool {
if _ := t1.loc {
return t1.nanosecond == t2.nanosecond && t1.unix() == t2.unix()
}
if _ := t2.loc {
return t1.nanosecond == t2.nanosecond && t1.unix() == t2.unix()
}
return t1.nanosecond == t2.nanosecond && t1.is_local == t2.is_local
&& t1.local_unix() == t2.local_unix()
}
Expand Down
70 changes: 66 additions & 4 deletions vlib/time/time.c.v
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
// that can be found in the LICENSE file.
module time

import strings

#include <time.h>

// C.timeval represents a C time value.
Expand Down Expand Up @@ -57,6 +59,9 @@ pub fn utc() Time {
}

fn time_with_unix(t Time) Time {
if _ := t.loc {
return t
}
if t.unix != 0 {
return t
}
Expand Down Expand Up @@ -137,16 +142,73 @@ fn convert_ctime(t C.tm, nanosecond int) Time {

// strftime returns the formatted time using `strftime(3)`.
pub fn (t Time) strftime(fmt string) string {
mut strftime_fmt := fmt
mut strftime_unix := t.unix
mut zone_name := ''
mut zone_offset := ''
mut unix_time := ''
if loc := t.location() {
zone := loc.zone_at(t.unix) or { Zone{} }
zone_name = zone.name
zone_offset = strftime_zone_offset(zone.offset)
unix_time = t.unix.str()
strftime_fmt = strftime_location_format(fmt)
strftime_unix = t.local_unix()
}
mut tm := &C.tm{}
$if windows {
tm = C.gmtime(voidptr(&t.unix))
tm = C.gmtime(voidptr(&strftime_unix))
} $else {
C.gmtime_r(voidptr(&t.unix), tm)
C.gmtime_r(voidptr(&strftime_unix), tm)
}
mut buf := [1024]char{}
fmt_c := unsafe { &char(fmt.str) }
fmt_c := unsafe { &char(strftime_fmt.str) }
C.strftime(&buf[0], usize(sizeof(buf)), fmt_c, tm)
return unsafe { cstring_to_vstring(&buf[0]) }
res := unsafe { cstring_to_vstring(&buf[0]) }
if zone_name != '' || zone_offset != '' || unix_time != '' {
return res.replace(strftime_zone_name_placeholder, zone_name).replace(strftime_zone_offset_placeholder,
zone_offset).replace(strftime_unix_placeholder, unix_time)
}
return res
}

const strftime_zone_name_placeholder = '@V_STRFTIME_ZONE_NAME@'
const strftime_zone_offset_placeholder = '@V_STRFTIME_ZONE_OFFSET@'
const strftime_unix_placeholder = '@V_STRFTIME_UNIX@'

fn strftime_location_format(fmt string) string {
mut out := strings.new_builder(fmt.len)
for i := 0; i < fmt.len; i++ {
if fmt[i] != `%` || i + 1 >= fmt.len {
out.write_u8(fmt[i])
continue
}
i++
match fmt[i] {
`Z` {
out.write_string(strftime_zone_name_placeholder)
}
`z` {
out.write_string(strftime_zone_offset_placeholder)
}
`s` {
out.write_string(strftime_unix_placeholder)
}
else {
out.write_u8(`%`)
out.write_u8(fmt[i])
}
}
}
return out.str()
}

fn strftime_zone_offset(offset int) string {
sign := if offset < 0 { '-' } else { '+' }
abs_offset := if offset < 0 { -offset } else { offset }
hours := abs_offset / seconds_per_hour
minutes := (abs_offset % seconds_per_hour) / seconds_per_minute
return '${sign}${hours:02}${minutes:02}'
}

// some *nix system functions (e.g. `C.poll()`, C.epoll_wait()) accept an `int`
Expand Down
3 changes: 3 additions & 0 deletions vlib/time/time.js.v
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,9 @@ pub fn sleep(dur Duration) {
}

fn time_with_unix(t Time) Time {
if _ := t.loc {
return t
}
if t.unix != 0 {
return t
}
Expand Down
41 changes: 39 additions & 2 deletions vlib/time/time.v
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ pub const days_before = [
@[markused]
pub struct Time {
unix i64
Comment thread
guweigang marked this conversation as resolved.
loc ?Location
Comment thread
guweigang marked this conversation as resolved.
Comment thread
guweigang marked this conversation as resolved.
pub:
year int
month int
Expand Down Expand Up @@ -143,12 +144,29 @@ pub fn (t Time) smonth() string {
// unix returns the UNIX time with second resolution.
@[inline]
pub fn (t Time) unix() i64 {
if _ := t.loc {
return t.unix
}
return time_with_unix(t.local_to_utc()).unix
}

// local_unix returns the UNIX local time with second resolution.
// For IANA locations this is the absolute unix instant plus the zone offset
// active at that instant (wall-clock seconds since epoch in that location).
@[inline]
pub fn (t Time) local_unix() i64 {
if loc := t.loc {
// zone_at only fails for empty/corrupt locations; use the first zone
// offset rather than silently treating the instant as UTC.
zone_offset := loc.offset_at(t.unix) or {
if loc.zones.len > 0 {
loc.zones[0].offset
} else {
0
}
}
return t.unix + i64(zone_offset)
}
return time_with_unix(t).unix
}

Expand Down Expand Up @@ -185,14 +203,27 @@ 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.local_unix() + (increased_time_nanosecond / second)
base_unix := if _ := t.loc { t.unix } else { t.local_unix() }
mut increased_time_second := base_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))

if loc := t.loc {
return loc.unix_nanosecond_to_local(res.unix, res.nanosecond) or {
// Keep the location even if projection fails so later operations
// still treat this value as an IANA-zoned instant.
Time{
...res
loc: loc
unix: res.unix
}
}
}

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
Expand Down Expand Up @@ -441,6 +472,9 @@ pub fn offset() int {
// local_to_utc converts the receiver `t` to the corresponding UTC time, if it contains local time.
// If the receiver already does contain UTC time, it returns it unchanged.
pub fn (t Time) local_to_utc() Time {
if _ := t.loc {
return unix_nanosecond(t.unix, t.nanosecond)
}
if !t.is_local {
return t
}
Expand All @@ -453,6 +487,9 @@ pub fn (t Time) local_to_utc() Time {
// utc_to_local converts the receiver `u` to the corresponding local time, if it contains UTC time.
// If the receiver already does contain local time, it returns it unchanged.
pub fn (u Time) utc_to_local() Time {
if _ := u.loc {
return u
}
if u.is_local {
return u
}
Expand Down Expand Up @@ -483,5 +520,5 @@ pub fn (t Time) as_utc() Time {
// is_utc returns true, when the receiver `t` is a UTC time, and false otherwise.
// See also #Time.utc_to_local .
pub fn (t Time) is_utc() bool {
return !t.is_local
return !t.is_local && t.location() == none
}
2 changes: 1 addition & 1 deletion vlib/time/time_windows.c.v
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ fn C.GetSystemTimeAsFileTime(lpSystemTimeAsFileTime &C._FILETIME)

fn C.FileTimeToSystemTime(lpFileTime &C._FILETIME, lpSystemTime &SystemTime)

fn C.SystemTimeToTzSpecificLocalTime(lpTimeZoneInformation &C.TIME_ZONE_INFORMATION, lpUniversalTime &SystemTime,
fn C.SystemTimeToTzSpecificLocalTime(lpTimeZoneInformation voidptr, lpUniversalTime &SystemTime,
lpLocalTime &SystemTime)

fn C.localtime_s(t &C.time_t, tm &C.tm)
Expand Down
Loading
Loading