time: add microsecond precision to Time struct

pull/5265/head
Tomas Hellström 2020-06-07 15:19:09 +02:00 committed by GitHub
parent eec5cf1eb1
commit 9c8769503f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 832 additions and 17 deletions

View File

@ -20,6 +20,7 @@ fn C.sprintf(a ...voidptr) int
fn C.strlen(s byteptr) int
fn C.sscanf(byteptr, byteptr,...byteptr) int
fn C.isdigit(s byteptr) bool
// stdio.h

View File

@ -103,8 +103,6 @@ pub fn new(uri string) &Client {
return ws
}
fn C.sscanf() int
fn (ws &Client) parse_uri() &Uri {
u := urllib.parse(ws.uri) or {
panic(err)
@ -572,7 +570,7 @@ fn (mut ws Client) send_control_frame(code OPCode, frame_typ string, payload []b
mut bytes_written := -1
if ws.socket.sockfd <= 0 {
l.e('No socket opened.')
unsafe {
unsafe {
payload.free()
}
l.c('send_control_frame: error sending ${frame_typ} control frame.')

View File

@ -0,0 +1,40 @@
module time
// eq returns true if provided time is equal to time
pub fn (t1 Time) eq(t2 Time) bool {
if t1.unix == t2.unix && t1.microsecond == t2.microsecond {
return true
}
return false
}
// ne returns true if provided time is not equal to time
pub fn (t1 Time) ne(t2 Time) bool {
return !t1.eq(t2)
}
// lt returns true if provided time is less than time
pub fn (t1 Time) lt(t2 Time) bool {
if t1.unix < t2.unix || (t1.unix == t2.unix && t1.microsecond < t2.microsecond) {
return true
}
return false
}
// le returns true if provided time is less or equal to time
pub fn (t1 Time) le(t2 Time) bool {
return t1.lt(t2) || t1.eq(t2)
}
// gt returns true if provided time is greater than time
pub fn (t1 Time) gt(t2 Time) bool {
if t1.unix > t2.unix || (t1.unix == t2.unix && t1.microsecond > t2.microsecond) {
return true
}
return false
}
// ge returns true if provided time is greater or equal to time
pub fn (t1 Time) ge(t2 Time) bool {
return t1.gt(t2) || t1.eq(t2)
}

View File

@ -0,0 +1,409 @@
module time
fn assert_greater_time(ms int, t1 Time) {
time.sleep_ms(ms)
t2 := now()
assert t2.gt(t1)
}
// Tests the now in all platform and the gt operator function with at least ms resolution
fn test_now_always_results_in_greater_time() {
t1 := now()
$if macos {
assert_greater_time(1, t1)
return
}
$if windows {
// Lower resolution of time for windows
assert_greater_time(15, t1)
return
}
$if linux {
assert_greater_time(1, t1)
return
}
$if solaris {
assert_greater_time(1, t1)
return
}
// other platforms may have more accurate resolution,
// but we do not know that ... so wait at least 1s:
assert_greater_time(1001, t1)
}
fn test_time1_should_be_same_as_time2() {
t1 := new_time( Time {
year: 2000
month: 5
day: 10
hour: 22
minute: 11
second: 3
microsecond: 100
})
t2 := new_time( Time {
year: 2000
month: 5
day: 10
hour: 22
minute: 11
second: 3
microsecond: 100
})
assert t1.eq(t2)
}
fn test_time1_should_not_be_same_as_time2() {
t1 := new_time( Time {
year: 2000
month: 5
day: 10
hour: 22
minute: 11
second: 3
microsecond: 100
})
// Difference is one microsecond
t2 := new_time( Time {
year: 2000
month: 5
day: 10
hour: 22
minute: 11
second: 3
microsecond: 101
})
t3 := new_time( Time {
year: 2000
month: 5
day: 10
hour: 22
minute: 11
second: 3
microsecond: 0
})
// Difference is one second
t4 := new_time( Time {
year: 2000
month: 5
day: 10
hour: 22
minute: 11
second: 4
microsecond: 0
})
assert t1.ne(t2)
assert t3.ne(t4)
}
fn test_time1_should_be_greater_than_time2() {
t1 := new_time( Time {
year: 2000
month: 5
day: 10
hour: 22
minute: 11
second: 3
microsecond: 102
})
// Difference is one microsecond
t2 := new_time( Time {
year: 2000
month: 5
day: 10
hour: 22
minute: 11
second: 3
microsecond: 101
})
t3 := new_time( Time {
year: 2000
month: 5
day: 10
hour: 22
minute: 11
second: 5
microsecond: 0
})
// Difference is one second
t4 := new_time( Time {
year: 2000
month: 5
day: 10
hour: 22
minute: 11
second: 4
microsecond: 0
})
assert t1.gt(t2)
assert t3.gt(t4)
}
fn test_time2_should_be_less_than_time1() {
t1 := new_time( Time {
year: 2000
month: 5
day: 10
hour: 22
minute: 11
second: 3
microsecond: 102
})
// Difference is one microsecond
t2 := new_time( Time {
year: 2000
month: 5
day: 10
hour: 22
minute: 11
second: 3
microsecond: 101
})
t3 := new_time( Time {
year: 2000
month: 5
day: 10
hour: 22
minute: 11
second: 3
microsecond: 0
})
// Difference is one second
t4 := new_time( Time {
year: 2000
month: 5
day: 10
hour: 22
minute: 11
second: 2
microsecond: 0
})
assert t2.lt(t1)
assert t4.lt(t3)
}
fn test_time1_should_be_greater_or_equal_to_time2_when_gt() {
t1 := new_time( Time {
year: 2000
month: 5
day: 10
hour: 22
minute: 11
second: 3
microsecond: 102
})
// Difference is one microsecond
t2 := new_time( Time {
year: 2000
month: 5
day: 10
hour: 22
minute: 11
second: 3
microsecond: 101
})
t3 := new_time( Time {
year: 2000
month: 5
day: 10
hour: 22
minute: 11
second: 5
microsecond: 0
})
// Difference is one second
t4 := new_time( Time {
year: 2000
month: 5
day: 10
hour: 22
minute: 11
second: 4
microsecond: 0
})
assert t1.ge(t2)
assert t3.ge(t4)
}
fn test_time1_should_be_greater_or_equal_to_time2_when_eq() {
t1 := new_time( Time {
year: 2000
month: 5
day: 10
hour: 22
minute: 11
second: 3
microsecond: 100
})
// Difference is one microsecond
t2 := new_time( Time {
year: 2000
month: 5
day: 10
hour: 22
minute: 11
second: 3
microsecond: 100
})
t3 := new_time( Time {
year: 2000
month: 5
day: 10
hour: 22
minute: 11
second: 3
microsecond: 0
})
// Difference is one second
t4 := new_time( Time {
year: 2000
month: 5
day: 10
hour: 22
minute: 11
second: 3
microsecond: 0
})
assert t1.ge(t2)
assert t3.ge(t4)
}
fn test_time1_should_be_less_or_equal_to_time2_when_lt() {
t1 := new_time( Time {
year: 2000
month: 5
day: 10
hour: 22
minute: 11
second: 3
microsecond: 100
})
// Difference is one microsecond
t2 := new_time( Time {
year: 2000
month: 5
day: 10
hour: 22
minute: 11
second: 3
microsecond: 101
})
t3 := new_time( Time {
year: 2000
month: 5
day: 10
hour: 22
minute: 11
second: 3
microsecond: 0
})
// Difference is one second
t4 := new_time( Time {
year: 2000
month: 5
day: 10
hour: 22
minute: 11
second: 4
microsecond: 0
})
assert t1.le(t2)
assert t3.le(t4)
}
fn test_time1_should_be_less_or_equal_to_time2_when_eq() {
t1 := new_time( Time {
year: 2000
month: 5
day: 10
hour: 22
minute: 11
second: 3
microsecond: 100
})
// Difference is one microsecond
t2 := new_time( Time {
year: 2000
month: 5
day: 10
hour: 22
minute: 11
second: 3
microsecond: 100
})
t3 := new_time( Time {
year: 2000
month: 5
day: 10
hour: 22
minute: 11
second: 3
microsecond: 0
})
// Difference is one second
t4 := new_time( Time {
year: 2000
month: 5
day: 10
hour: 22
minute: 11
second: 3
microsecond: 0
})
assert t1.le(t2)
assert t3.le(t4)
}
fn test_time2_copied_from_time1_should_be_equal() {
t1 := new_time( Time {
year: 2000
month: 5
day: 10
hour: 22
minute: 11
second: 3
microsecond: 100
})
t2 := new_time(t1)
assert t2.eq(t1)
}

View File

@ -47,3 +47,65 @@ pub fn parse_rfc2822(s string) ?Time {
return parse(tos(tmstr, count))
}
// parse_rfc8601 parses rfc8601 time format yyyy-MM-ddTHH:mm:ss.dddddd+dd:dd as local time
// the fraction part is difference in milli seconds and the last part is offset
// from UTC time and can be both +/- HH:mm
// Remarks: Not all rfc8601 is supported only the 'yyyy-MM-ddTHH:mm:ss.dddddd+dd:dd'
pub fn parse_rfc8601(s string) ?Time {
mut year := 0
mut month := 0
mut day := 0
mut hour := 0
mut minute := 0
mut second := 0
mut mic_second := 0
mut time_char := `a`
mut plus_min := `a`
mut offset_hour := 0
mut offset_min := 0
count := C.sscanf(s.str, "%4d-%2d-%2d%c%2d:%2d:%2d.%6d%c%2d:%2d", &year, &month, &day,
&time_char, &hour, &minute,
&second, &mic_second, &plus_min,
&offset_hour, &offset_min)
if count != 11 {
return error('Invalid 8601 format')
}
if time_char != `T` && time_char != ` ` {
return error('Invalid 8601 format, expected space or `T` as time separator')
}
if plus_min != `+` && plus_min != `-` {
return error('Invalid 8601 format, expected `+` or `-` as time separator' )
}
t := new_time(Time{
year: year
month: month
day: day
hour: hour
minute: minute
second: second
microsecond: mic_second
})
mut unix_offset := int(0)
if offset_hour > 0 {
unix_offset += 3600*offset_hour
}
if offset_min > 0 {
unix_offset += 60*offset_min
}
if unix_offset != 0 {
if plus_min == `+` {
return unix2(int(t.unix) + unix_offset, t.microsecond)
} else {
return unix2(int(t.unix) - unix_offset, t.microsecond)
}
}
return t
}

View File

@ -12,7 +12,7 @@ fn test_parse() {
fn test_parse_invalid() {
s := 'Invalid time string'
t := time.parse(s) or {
_ := time.parse(s) or {
assert true
return
}
@ -38,9 +38,37 @@ fn test_parse_rfc2822() {
fn test_parse_rfc2822_invalid() {
s3 := 'Thu 12 Foo 2019 06:07:45 +0800'
t3 := time.parse_rfc2822(s3) or {
_ := time.parse_rfc2822(s3) or {
assert true
return
}
assert false
}
fn test_rfc8601_parse_utc() {
ok_format := '2020-06-02T15:38:06.015959+00:00'
t := time.parse_rfc8601(ok_format) or {panic(err)}
assert t.year == 2020
assert t.month == 6
assert t.day == 2
assert t.hour == 15
assert t.minute == 38
assert t.second == 6
assert t.microsecond == 15959
}
fn test_rfc8601_parse_cest() {
ok_format := '2020-06-02T15:38:06.015959+02:00'
t := time.parse_rfc8601(ok_format) or {panic(err)}
assert t.year == 2020
assert t.month == 6
assert t.day == 2
assert t.hour == 17
assert t.minute == 38
assert t.second == 6
assert t.microsecond == 15959
}

View File

@ -0,0 +1,16 @@
// tests that use and test private functions
module time
// test the old behavor 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 := time.convert_ctime(tm, 0)
new_time := time.now()
diff := new_time.unix - old_time.unix
// could in very rare cases be that the second changed between calls
assert (diff >=0 && diff <=1) == true
}

View File

@ -39,13 +39,14 @@ const (
pub struct Time {
pub:
year int
month int
day int
hour int
minute int
second int
unix u64
year int
month int
day int
hour int
minute int
second int
microsecond int
unix u64
}
pub enum FormatTime {
@ -86,9 +87,23 @@ fn C.time(t &C.time_t) C.time_t
// now returns current local time.
pub fn now() Time {
$if macos {
return darwin_now()
}
$if windows {
return win_now()
}
$if solaris {
return solaris_now()
}
$if linux {
return linux_now()
}
// defaults to most common feature, the microsecond precision is not available
// in this API call
t := C.time(0)
now := C.localtime(&t)
return convert_ctime(now)
return convert_ctime(now, 0)
}
// smonth returns month name.
@ -107,6 +122,7 @@ pub fn new_time(t Time) Time {
minute: t.minute
second: t.second
unix: u64(t.unix_time())
microsecond: t.microsecond
}
// TODO Use the syntax below when it works with reserved keywords like `unix`
// return {
@ -264,7 +280,7 @@ pub fn (t Time) str() string {
return t.format_ss()
}
fn convert_ctime(t C.tm) Time {
fn convert_ctime(t C.tm, microsecond int) Time {
return Time{
year: t.tm_year + 1900
month: t.tm_mon + 1
@ -272,6 +288,7 @@ fn convert_ctime(t C.tm) Time {
hour: t.tm_hour
minute: t.tm_min
second: t.tm_sec
microsecond: microsecond
unix: u64(make_unix_time(t))
}
}

View File

@ -23,6 +23,11 @@ struct InternalTimeBase {
denom u32 = 1
}
pub struct C.timeval {
tv_sec u64
tv_usec u64
}
fn init_time_base() InternalTimeBase {
tb := C.mach_timebase_info_data_t{}
C.mach_timebase_info(&tb)
@ -47,3 +52,29 @@ fn vpc_now_darwin() u64 {
}
return (tm - start_time) * time_base.numer / 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 normal local time to get correct local time
// if the time has shifted on a second level between calls it uses
// zero as microsecond. Not perfect but better that unix time only us a second
fn darwin_now() Time {
// get the high precision time as UTC clock
// and use the nanoseconds part
tv := C.timeval{}
C.gettimeofday(&tv, 0)
t := C.time(0)
tm := C.localtime(&t)
// if the second part (very rare) is different
// microseconds is set to zero since it passed the second
// also avoid divide by zero if nsec is zero
if int(t) != tv.tv_sec {
return convert_ctime(tm, 0)
}
return convert_ctime(tm, int(tv.tv_usec))
}

View File

@ -1,5 +1,41 @@
module time
fn sys_mono_now_darwin() u64 {
fn sys_mono_now_darwin() u64 {
return 0
}
// dummy to compile with all compilers
pub fn darwin_now() Time {
return Time{}
}
// dummy to compile with all compilers
pub fn solaris_now() Time {
return Time{}
}
// linux_now returns the local time with high precision for most os:es
// this should be implemented with native system calls eventually
// but for now a bit tweaky. It uses the realtime clock to get
// the nano seconds part and normal local time to get correct local time
// if the time has shifted on a second level between calls it uses
// zero as microsecond. Not perfect but better that unix time only
fn linux_now() Time {
// 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)
t := C.time(0)
tm := C.localtime(&t)
// if the second part (very rare) is different
// microseconds is set to zero since it passed the second
// also avoid divide by zero if nsec is zero
if int(t) != ts.tv_sec || ts.tv_nsec == 0 {
return convert_ctime(tm, 0)
}
return convert_ctime(tm, int(ts.tv_nsec/1000))
}

View File

@ -3,6 +3,8 @@
// that can be found in the LICENSE file.
module time
#include <time.h>
struct C.tm {
tm_sec int
tm_min int
@ -47,3 +49,15 @@ fn vpc_now() u64 {
C.clock_gettime(C.CLOCK_MONOTONIC, &ts)
return u64(ts.tv_sec) * 1_000_000_000 + u64(ts.tv_nsec)
}
// dummy to compile with all compilers
pub fn win_now() Time {
return Time{}
}
// dummy to compile with all compilers
pub struct C.timeval {
tv_sec u64
tv_usec u64
}

View File

@ -0,0 +1,26 @@
module time
// dummy to compile with all compilers
pub fn linux_now() Time {
return Time{}
}
pub fn darwin_now() Time {
return Time{}
}
fn solaris_now() Time {
// 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)
t := C.time(0)
tm := C.localtime(&t)
// if the second part (very rare) is different
// microseconds is set to zero since it passed the second
// also avoid divide by zero if nsec is zero
if int(t) != ts.tv_sec || ts.tv_nsec == 0 {
return convert_ctime(tm, 0)
}
return convert_ctime(tm, int(ts.tv_nsec/1000))
}

View File

@ -149,3 +149,20 @@ fn test_add_days() {
fn test_str() {
assert '1980-07-11 21:23:42' == time_to_test.str()
}
// not optimal test but will find obvious bugs
fn test_now() {
now := time.now()
// The year the test was built
assert now.year >= 2020
assert now.month > 0
assert now.month <= 12
assert now.minute >= 0
assert now.minute < 60
assert now.second >=0
assert now.second < 60
assert now.microsecond >= 0
assert now.microsecond < 1000000
}

View File

@ -3,6 +3,9 @@
// that can be found in the LICENSE file.
module time
#include <time.h>
#include <sysinfoapi.h>
struct C.tm {
tm_year int
tm_mon int
@ -12,13 +15,37 @@ struct C.tm {
tm_sec int
}
struct C._FILETIME
struct SystemTime {
year u16
month u16
day_of_week u16
day u16
hour u16
minute u16
second u16
millisecond u16
}
fn C.GetSystemTimeAsFileTime(lpSystemTimeAsFileTime C._FILETIME)
fn C.FileTimeToSystemTime()
fn C.SystemTimeToTzSpecificLocalTime()
const (
// start_time is needed on Darwin and Windows because of potential overflows
start_time = init_win_time_start()
freq_time = init_win_time_freq()
start_time = init_win_time_start()
freq_time = init_win_time_freq()
start_local_time = local_as_unix_time()
)
// in most systems, these are __quad_t, which is an i64
struct C.timespec {
tv_sec i64
tv_nsec i64
}
fn C._mkgmtime(&C.tm) time_t
fn C.QueryPerformanceCounter(&u64) C.BOOL
@ -55,3 +82,73 @@ fn vpc_now() u64 {
C.QueryPerformanceCounter(&tm)
return tm
}
// local_as_unix_time returns the current local time as unix time
fn local_as_unix_time() int {
t := C.time(0)
tm := C.localtime(&t)
return make_unix_time(tm)
}
// win_now calculates current time using winapi to get higher resolution on windows
// GetSystemTimeAsFileTime is used. It can resolve time down to millisecond
// other more precice methods can be implemented in the future
fn win_now() Time {
ft_utc := C._FILETIME{}
C.GetSystemTimeAsFileTime(&ft_utc)
st_utc := SystemTime{}
C.FileTimeToSystemTime(&ft_utc, &st_utc)
st_local := SystemTime{}
C.SystemTimeToTzSpecificLocalTime(voidptr(0), &st_utc, &st_local)
t := Time {
year: st_local.year
month: st_local.month
day: st_local.day
hour: st_local.hour
minute: st_local.minute
second: st_local.second
microsecond: st_local.millisecond*1000
unix: u64(st_local.unix_time())
}
return t
}
// unix_time returns Unix time.
pub fn (st SystemTime) unix_time() int {
tt := C.tm{
tm_sec: st.second
tm_min: st.minute
tm_hour: st.hour
tm_mday: st.day
tm_mon: st.month - 1
tm_year: st.year - 1900
}
return make_unix_time(tt)
}
// dummy to compile with all compilers
pub fn darwin_now() Time {
return Time{}
}
// dummy to compile with all compilers
pub fn linux_now() Time {
return Time{}
}
// dummy to compile with all compilers
pub fn solaris_now() Time {
return Time{}
}
// dummy to compile with all compilers
pub struct C.timeval {
tv_sec u64
tv_usec u64
}

View File

@ -24,6 +24,29 @@ pub fn unix(abs int) Time {
}
}
// unix2 returns a time struct from Unix time and microsecond value
pub fn unix2(abs int, microsecond int) Time {
// Split into day and time
mut day_offset := abs / seconds_per_day
if abs % seconds_per_day < 0 {
// Compensate for round towards zero on integers as we want floored instead
day_offset--
}
year,month,day := calculate_date_from_offset(day_offset)
hr,min,sec := calculate_time_from_offset(abs % seconds_per_day)
return Time{
year: year
month: month
day: day
hour: hr
minute: min
second: sec
microsecond: microsecond
unix: u64(abs)
}
}
[inline]
fn calculate_date_from_offset(day_offset_ int) (int,int,int) {
mut day_offset := day_offset_