Skip to content
Merged
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
16 changes: 8 additions & 8 deletions vlib/time/parse.c.v
Original file line number Diff line number Diff line change
Expand Up @@ -290,7 +290,7 @@ pub fn parse_rfc3339(s string) !Time {
].bytestr()}"')
} else {
if s[s.len - 1] in [u8(`Z`), `z`] {
return new(Time{
return new_checked(Time{
year: year
month: month
day: day
Expand All @@ -313,7 +313,7 @@ pub fn parse_rfc3339(s string) !Time {

// Check if it is UTC time
if unsafe { vmemcmp(s.str + s.len - 5, c'00:00', 5) == 0 } {
return new(Time{
return new_checked(Time{
year: year
month: month
day: day
Expand Down Expand Up @@ -346,7 +346,7 @@ pub fn parse_rfc3339(s string) !Time {
offset_in_minutes *= -1
}

wall_time := new(Time{
wall_time := new_checked(Time{
year: year
month: month
day: day
Expand All @@ -355,7 +355,7 @@ pub fn parse_rfc3339(s string) !Time {
second: second_
nanosecond: nanosecond_
is_local: false
})
})!

return fixed_offset_time(wall_time, offset_in_minutes * seconds_per_minute)
}
Expand Down Expand Up @@ -424,14 +424,14 @@ pub fn parse(s string) !Time {
if isecond > 59 || isecond < 0 {
return error_invalid_time(8, 'seconds must be between 0 and 60')
}
res := new(Time{
res := new_checked(Time{
year: iyear
month: imonth
day: iday
hour: ihour
minute: iminute
second: isecond
})
})!
return res
}

Expand Down Expand Up @@ -496,15 +496,15 @@ pub fn parse_iso8601(s string) !Time {
hour_, minute_, second_, _, nanosecond_, unix_offset, is_local_time =
parse_iso8601_time(parts[1])!
}
t := new(
t := new_checked(
year: year
month: month
day: day
hour: hour_
minute: minute_
second: second_
nanosecond: nanosecond_
)
)!
if is_local_time {
return t // Time already local time
}
Expand Down
49 changes: 49 additions & 0 deletions vlib/time/parse_test.v
Original file line number Diff line number Diff line change
Expand Up @@ -421,3 +421,52 @@ fn test_parse_format() {

assert false, '> failing for datetime: ${s}, the datetime string should not have passed the format "YYYY-M-D H:m:s"'
}

fn test_parse_rejects_feb_30() {
time.parse('2024-02-30 10:00:00') or {
assert err.msg().contains('day must be between 1 and 29')
return
}
assert false, 'February 30th should not parse'
}

fn test_parse_rejects_feb_29_in_non_leap_year() {
time.parse('2023-02-29 10:00:00') or {
assert err.msg().contains('day must be between 1 and 28')
return
}
assert false, 'February 29th 2023 should not parse'
}

fn test_parse_rejects_april_31() {
time.parse('2024-04-31 10:00:00') or {
assert err.msg().contains('day must be between 1 and 30')
return
}
assert false, 'April 31st should not parse'
}

fn test_parse_rfc3339_rejects_day_past_end_of_month() {
time.parse_rfc3339('2024-02-30T10:00:00Z') or { return }
assert false, 'February 30th should not parse'
}

fn test_parse_rfc3339_rejects_april_31_with_offset() {
time.parse_rfc3339('2024-04-31T10:00:00+01:00') or { return }
assert false, 'April 31st should not parse'
}

fn test_parse_iso8601_rejects_day_past_end_of_month() {
time.parse_iso8601('2024-02-30T10:00:00Z') or { return }
assert false, 'February 30th should not parse'
}

fn test_parse_rfc2822_rejects_day_past_end_of_month() {
time.parse_rfc2822('Thu, 30 Feb 2024 10:00:00 +0100') or { return }
assert false, 'February 30th should not parse'
}

fn test_parse_accepts_last_day_of_month() {
assert time.parse('2024-02-29 10:00:00')!.day == 29
assert time.parse_rfc3339('2024-04-30T10:00:00Z')!.day == 30
}
32 changes: 22 additions & 10 deletions vlib/time/time.v
Original file line number Diff line number Diff line change
Expand Up @@ -94,42 +94,54 @@ pub enum FormatDelimiter {
no_delimiter
}

fn normalize_new_time(t Time) Time {
fn check_new_time(t Time) ! {
month := if t.month == 0 { 1 } else { t.month }
day := if t.day == 0 { 1 } else { t.day }
if t.year < -9999 || t.year > 9999 {
panic('invalid time: year must be between -9999 and 9999')
return error('invalid time: year must be between -9999 and 9999')
}
if month < 1 || month > 12 {
panic('invalid time: month must be between 1 and 12')
return error('invalid time: month must be between 1 and 12')
}
max_day := month_days[month - 1] + if month == 2 && is_leap_year(t.year) {
1
} else {
0
}
if day < 1 || day > max_day {
panic('invalid time: day must be between 1 and ${max_day} for year ${t.year}, month ${month}')
return error('invalid time: day must be between 1 and ${max_day} for year ${t.year}, month ${month}')
}
if t.hour < 0 || t.hour > 23 {
panic('invalid time: hour must be between 0 and 23')
return error('invalid time: hour must be between 0 and 23')
}
if t.minute < 0 || t.minute > 59 {
panic('invalid time: minute must be between 0 and 59')
return error('invalid time: minute must be between 0 and 59')
}
if t.second < 0 || t.second > 59 {
panic('invalid time: second must be between 0 and 59')
return error('invalid time: second must be between 0 and 59')
}
if t.nanosecond < 0 || t.nanosecond >= 1_000_000_000 {
panic('invalid time: nanosecond must be between 0 and 999999999')
return error('invalid time: nanosecond must be between 0 and 999999999')
}
}

fn normalize_new_time(t Time) Time {
check_new_time(t) or { panic(err.msg()) }
return Time{
...t
month: month
day: day
month: if t.month == 0 { 1 } else { t.month }
day: if t.day == 0 { 1 } else { t.day }
}
}

// new_checked is like `new`, but returns an error instead of panicking when
// the fields do not form a valid date and time. The parse functions use it so
// that malformed input, such as February 30th, is reported to the caller.
fn new_checked(t Time) !Time {
check_new_time(t)!
return Time.new(t)
}

// Time.new returns a time struct with the calculated Unix time.
pub fn Time.new(t Time) Time {
return time_with_unix(normalize_new_time(t))
Expand Down