New implementations of times.parse & times.format (#8094)

This commit is contained in:
Oscar Nihlgård
2018-07-09 20:04:25 +02:00
committed by Andreas Rumpf
parent c6671776a1
commit 3b310e91cd
4 changed files with 975 additions and 619 deletions

View File

@@ -683,8 +683,8 @@ when isMainModule:
# works:
import times
var nullTime: DateTime
check &"{nullTime:yyyy-mm-dd}", "0000-00-00"
var dt = initDateTime(01, mJan, 2000, 00, 00, 00)
check &"{dt:yyyy-MM-dd}", "2000-01-01"
var tm = fromUnix(0)
discard &"{tm}"

File diff suppressed because it is too large Load Diff

View File

@@ -36,8 +36,8 @@ let utcPlus2 = Timezone(zoneInfoFromUtc: staticZoneInfoFromUtc, zoneInfoFromTz:
block timezoneTests:
let dt = initDateTime(01, mJan, 2017, 12, 00, 00, utcPlus2)
doAssert $dt == "2017-01-01T12:00:00+02:00"
doAssert $dt.utc == "2017-01-01T10:00:00+00:00"
doAssert $dt.utc == "2017-01-01T10:00:00Z"
doAssert $dt.utc.inZone(utcPlus2) == $dt
doAssert $initDateTime(01, mJan, 1911, 12, 00, 00, utc()) == "1911-01-01T12:00:00+00:00"
doAssert $initDateTime(01, mJan, 0023, 12, 00, 00, utc()) == "0023-01-01T12:00:00+00:00"
doAssert $initDateTime(01, mJan, 1911, 12, 00, 00, utc()) == "1911-01-01T12:00:00Z"
doAssert $initDateTime(01, mJan, 0023, 12, 00, 00, utc()) == "0023-01-01T12:00:00Z"

View File

@@ -8,6 +8,24 @@ discard """
import
times, os, strutils, unittest
proc staticTz(hours, minutes, seconds: int = 0): Timezone {.noSideEffect.} =
let offset = hours * 3600 + minutes * 60 + seconds
proc zoneInfoFromTz(adjTime: Time): ZonedTime {.locks: 0.} =
result.isDst = false
result.utcOffset = offset
result.adjTime = adjTime
proc zoneInfoFromUtc(time: Time): ZonedTime {.locks: 0.}=
result.isDst = false
result.utcOffset = offset
result.adjTime = fromUnix(time.toUnix - offset)
result.name = ""
result.zoneInfoFromTz = zoneInfoFromTz
result.zoneInfoFromUtc = zoneInfoFromUtc
# $ date --date='@2147483647'
# Tue 19 Jan 03:14:07 GMT 2038
@@ -19,25 +37,10 @@ proc checkFormat(t: DateTime, format, expected: string) =
echo "actual : ", actual
doAssert false
let t = fromUnix(2147483647).utc
t.checkFormat("ddd dd MMM hh:mm:ss yyyy", "Tue 19 Jan 03:14:07 2038")
t.checkFormat("ddd ddMMMhh:mm:ssyyyy", "Tue 19Jan03:14:072038")
t.checkFormat("d dd ddd dddd h hh H HH m mm M MM MMM MMMM s" &
" ss t tt y yy yyy yyyy yyyyy z zz zzz",
"19 19 Tue Tuesday 3 03 3 03 14 14 1 01 Jan January 7 07 A AM 8 38 038 2038 02038 +0 +00 +00:00")
t.checkFormat("yyyyMMddhhmmss", "20380119031407")
# issue 7620
let t7620_am = parse("4/15/2017 12:01:02 AM +0", "M/d/yyyy' 'h:mm:ss' 'tt' 'z", utc())
t7620_am.checkFormat("M/d/yyyy' 'h:mm:ss' 'tt' 'z", "4/15/2017 12:01:02 AM +0")
let t7620_pm = parse("4/15/2017 12:01:02 PM +0", "M/d/yyyy' 'h:mm:ss' 'tt' 'z", utc())
t7620_pm.checkFormat("M/d/yyyy' 'h:mm:ss' 'tt' 'z", "4/15/2017 12:01:02 PM +0")
let t2 = fromUnix(160070789).utc # Mon 27 Jan 16:06:29 GMT 1975
let t2 = fromUnix(160070789).utc() # Mon 27 Jan 16:06:29 GMT 1975
t2.checkFormat("d dd ddd dddd h hh H HH m mm M MM MMM MMMM s" &
" ss t tt y yy yyy yyyy yyyyy z zz zzz",
"27 27 Mon Monday 4 04 16 16 6 06 1 01 Jan January 29 29 P PM 5 75 975 1975 01975 +0 +00 +00:00")
"27 27 Mon Monday 4 04 16 16 6 06 1 01 Jan January 29 29 P PM 5 75 975 1975 01975 Z Z Z")
var t4 = fromUnix(876124714).utc # Mon 6 Oct 08:58:34 BST 1997
t4.checkFormat("M MM MMM MMMM", "10 10 Oct October")
@@ -83,16 +86,16 @@ let seqB: seq[Time] = @[]
doAssert seqA == seqB
for tz in [
(0, "+0", "+00", "+00:00"), # UTC
(-3600, "+1", "+01", "+01:00"), # CET
(-39600, "+11", "+11", "+11:00"), # two digits
(-1800, "+0", "+00", "+00:30"), # half an hour
(7200, "-2", "-02", "-02:00"), # positive
(38700, "-10", "-10", "-10:45")]: # positive with three quaters hour
let ti = DateTime(month: mJan, monthday: 1, utcOffset: tz[0])
doAssert ti.format("z") == tz[1]
doAssert ti.format("zz") == tz[2]
doAssert ti.format("zzz") == tz[3]
(staticTz(seconds = 0), "+0", "+00", "+00:00"), # UTC
(staticTz(seconds = -3600), "+1", "+01", "+01:00"), # CET
(staticTz(seconds = -39600), "+11", "+11", "+11:00"), # two digits
(staticTz(seconds = -1800), "+0", "+00", "+00:30"), # half an hour
(staticTz(seconds = 7200), "-2", "-02", "-02:00"), # positive
(staticTz(seconds = 38700), "-10", "-10", "-10:45")]: # positive with three quaters hour
let dt = initDateTime(1, mJan, 2000, 00, 00, 00, tz[0])
doAssert dt.format("z") == tz[1]
doAssert dt.format("zz") == tz[2]
doAssert dt.format("zzz") == tz[3]
block countLeapYears:
# 1920, 2004 and 2020 are leap years, and should be counted starting at the following year
@@ -112,11 +115,9 @@ template parseTest(s, f, sExpected: string, ydExpected: int) =
let
parsed = s.parse(f, utc())
parsedStr = $parsed
if parsedStr != sExpected:
echo "GOT ", parsedStr, " EXPECTED ", sExpected, " FORMAT ", f
check parsedStr == sExpected
if parsed.yearday != ydExpected:
echo s
echo parsed.repr
echo parsed.yearday, " exp: ", ydExpected
check(parsed.yearday == ydExpected)
template parseTestExcp(s, f: string) =
@@ -130,51 +131,43 @@ template parseTestTimeOnly(s, f, sExpected: string) =
# explicit timezone offsets in all tests.
template runTimezoneTests() =
parseTest("Tuesday at 09:04am on Dec 15, 2015 +0",
"dddd at hh:mmtt on MMM d, yyyy z", "2015-12-15T09:04:00+00:00", 348)
"dddd 'at' hh:mmtt 'on' MMM d, yyyy z", "2015-12-15T09:04:00Z", 348)
# ANSIC = "Mon Jan _2 15:04:05 2006"
parseTest("Thu Jan 12 15:04:05 2006 +0", "ddd MMM dd HH:mm:ss yyyy z",
"2006-01-12T15:04:05+00:00", 11)
"2006-01-12T15:04:05Z", 11)
# UnixDate = "Mon Jan _2 15:04:05 MST 2006"
parseTest("Thu Jan 12 15:04:05 2006 +0", "ddd MMM dd HH:mm:ss yyyy z",
"2006-01-12T15:04:05+00:00", 11)
"2006-01-12T15:04:05Z", 11)
# RubyDate = "Mon Jan 02 15:04:05 -0700 2006"
parseTest("Mon Feb 29 15:04:05 -07:00 2016 +0", "ddd MMM dd HH:mm:ss zzz yyyy z",
"2016-02-29T15:04:05+00:00", 59) # leap day
"2016-02-29T15:04:05Z", 59) # leap day
# RFC822 = "02 Jan 06 15:04 MST"
parseTest("12 Jan 16 15:04 +0", "dd MMM yy HH:mm z",
"2016-01-12T15:04:00+00:00", 11)
"2016-01-12T15:04:00Z", 11)
# RFC822Z = "02 Jan 06 15:04 -0700" # RFC822 with numeric zone
parseTest("01 Mar 16 15:04 -07:00", "dd MMM yy HH:mm zzz",
"2016-03-01T22:04:00+00:00", 60) # day after february in leap year
"2016-03-01T22:04:00Z", 60) # day after february in leap year
# RFC850 = "Monday, 02-Jan-06 15:04:05 MST"
parseTest("Monday, 12-Jan-06 15:04:05 +0", "dddd, dd-MMM-yy HH:mm:ss z",
"2006-01-12T15:04:05+00:00", 11)
"2006-01-12T15:04:05Z", 11)
# RFC1123 = "Mon, 02 Jan 2006 15:04:05 MST"
parseTest("Sun, 01 Mar 2015 15:04:05 +0", "ddd, dd MMM yyyy HH:mm:ss z",
"2015-03-01T15:04:05+00:00", 59) # day after february in non-leap year
"2015-03-01T15:04:05Z", 59) # day after february in non-leap year
# RFC1123Z = "Mon, 02 Jan 2006 15:04:05 -0700" # RFC1123 with numeric zone
parseTest("Thu, 12 Jan 2006 15:04:05 -07:00", "ddd, dd MMM yyyy HH:mm:ss zzz",
"2006-01-12T22:04:05+00:00", 11)
"2006-01-12T22:04:05Z", 11)
# RFC3339 = "2006-01-02T15:04:05Z07:00"
parseTest("2006-01-12T15:04:05Z-07:00", "yyyy-MM-ddTHH:mm:ssZzzz",
"2006-01-12T22:04:05+00:00", 11)
parseTest("2006-01-12T15:04:05Z-07:00", "yyyy-MM-dd'T'HH:mm:ss'Z'zzz",
"2006-01-12T22:04:05+00:00", 11)
"2006-01-12T22:04:05Z", 11)
# RFC3339Nano = "2006-01-02T15:04:05.999999999Z07:00"
parseTest("2006-01-12T15:04:05.999999999Z-07:00",
"yyyy-MM-ddTHH:mm:ss.999999999Zzzz", "2006-01-12T22:04:05+00:00", 11)
"yyyy-MM-dd'T'HH:mm:ss'.999999999Z'zzz", "2006-01-12T22:04:05Z", 11)
for tzFormat in ["z", "zz", "zzz"]:
# formatting timezone as 'Z' for UTC
parseTest("2001-01-12T22:04:05Z", "yyyy-MM-dd'T'HH:mm:ss" & tzFormat,
"2001-01-12T22:04:05+00:00", 11)
"2001-01-12T22:04:05Z", 11)
# Kitchen = "3:04PM"
parseTestTimeOnly("3:04PM", "h:mmtt", "15:04:00")
#when not defined(testing):
# echo "Kitchen: " & $s.parse(f)
# var ti = timeToTimeInfo(getTime())
# echo "Todays date after decoding: ", ti
# var tint = timeToTimeInterval(getTime())
# echo "Todays date after decoding to interval: ", tint
# Bug with parse not setting DST properly if the current local DST differs from
# the date being parsed. Need to test parse dates both in and out of DST. We
@@ -195,8 +188,8 @@ template runTimezoneTests() =
let
parsedJan = parse("2016-01-05 04:00:00+01:00", "yyyy-MM-dd HH:mm:sszzz")
parsedJul = parse("2016-07-01 04:00:00+01:00", "yyyy-MM-dd HH:mm:sszzz")
doAssert toTime(parsedJan).toUnix == 1451962800
doAssert toTime(parsedJul).toUnix == 1467342000
check toTime(parsedJan).toUnix == 1451962800
check toTime(parsedJul).toUnix == 1467342000
suite "ttimes":
@@ -256,7 +249,7 @@ suite "ttimes":
check $(dt + initDuration(days = 1)) == "2017-03-26T13:00:00+02:00"
test "datetime before epoch":
check $fromUnix(-2147483648).utc == "1901-12-13T20:45:52+00:00"
check $fromUnix(-2147483648).utc == "1901-12-13T20:45:52Z"
test "adding/subtracting time across dst":
putenv("TZ", "Europe/Stockholm")
@@ -319,6 +312,15 @@ suite "ttimes":
test "incorrect inputs: timezone (zzz) 3":
parseTestExcp("2018-02-19 16:30:00 +01:0", "yyyy-MM-dd hh:mm:ss zzz")
test "incorrect inputs: year (yyyy/uuuu)":
parseTestExcp("-0001", "yyyy")
parseTestExcp("-0001", "YYYY")
parseTestExcp("1", "yyyy")
parseTestExcp("12345", "yyyy")
parseTestExcp("1", "uuuu")
parseTestExcp("12345", "uuuu")
parseTestExcp("-1 BC", "UUUU g")
test "dynamic timezone":
proc staticOffset(offset: int): Timezone =
proc zoneInfoFromTz(adjTime: Time): ZonedTime =
@@ -340,7 +342,7 @@ suite "ttimes":
check dt.utcOffset == -9000
check dt.isDst == false
check $dt == "2000-01-01T12:00:00+02:30"
check $dt.utc == "2000-01-01T09:30:00+00:00"
check $dt.utc == "2000-01-01T09:30:00Z"
check $dt.utc.inZone(tz) == $dt
test "isLeapYear":
@@ -351,12 +353,12 @@ suite "ttimes":
test "subtract months":
var dt = initDateTime(1, mFeb, 2017, 00, 00, 00, utc())
check $(dt - initTimeInterval(months = 1)) == "2017-01-01T00:00:00+00:00"
check $(dt - initTimeInterval(months = 1)) == "2017-01-01T00:00:00Z"
dt = initDateTime(15, mMar, 2017, 00, 00, 00, utc())
check $(dt - initTimeInterval(months = 1)) == "2017-02-15T00:00:00+00:00"
check $(dt - initTimeInterval(months = 1)) == "2017-02-15T00:00:00Z"
dt = initDateTime(31, mMar, 2017, 00, 00, 00, utc())
# This happens due to monthday overflow. It's consistent with Phobos.
check $(dt - initTimeInterval(months = 1)) == "2017-03-03T00:00:00+00:00"
check $(dt - initTimeInterval(months = 1)) == "2017-03-03T00:00:00Z"
test "duration":
let d = initDuration
@@ -384,11 +386,11 @@ suite "ttimes":
discard initDateTime(1, mJan, -35_000, 12, 00, 00)
discard initDateTime(1, mJan, 35_000, 12, 00, 00)
# with duration/timeinterval
let dt = initDateTime(1, mJan, 35_000, 12, 00, 00, utc()) +
let dt = initDateTime(1, mJan, -35_000, 12, 00, 00, utc()) +
initDuration(seconds = 1)
check dt.second == 1
let dt2 = dt + 35_001.years
check $dt2 == "0001-01-01T12:00:01+00:00"
check $dt2 == "0001-01-01T12:00:01Z"
test "compare datetimes":
var dt1 = now()
@@ -426,4 +428,90 @@ suite "ttimes":
check (-1).fromWinTime.nanosecond == convert(Seconds, Nanoseconds, 1) - 100
check -1.fromWinTime.toWinTime == -1
# One nanosecond is discarded due to differences in time resolution
check initTime(0, 101).toWinTime.fromWinTime.nanosecond == 100
check initTime(0, 101).toWinTime.fromWinTime.nanosecond == 100
check initTime(0, 101).toWinTime.fromWinTime.nanosecond == 100
test "issue 7620":
let layout = "M/d/yyyy' 'h:mm:ss' 'tt' 'z"
let t7620_am = parse("4/15/2017 12:01:02 AM +0", layout, utc())
check t7620_am.format(layout) == "4/15/2017 12:01:02 AM Z"
let t7620_pm = parse("4/15/2017 12:01:02 PM +0", layout, utc())
check t7620_pm.format(layout) == "4/15/2017 12:01:02 PM Z"
test "format":
var dt = initDateTime(1, mJan, -0001,
17, 01, 02, 123_456_789,
staticTz(hours = 1, minutes = 2, seconds = 3))
check dt.format("d") == "1"
check dt.format("dd") == "01"
check dt.format("ddd") == "Fri"
check dt.format("dddd") == "Friday"
check dt.format("h") == "5"
check dt.format("hh") == "05"
check dt.format("H") == "17"
check dt.format("HH") == "17"
check dt.format("m") == "1"
check dt.format("mm") == "01"
check dt.format("M") == "1"
check dt.format("MM") == "01"
check dt.format("MMM") == "Jan"
check dt.format("MMMM") == "January"
check dt.format("s") == "2"
check dt.format("ss") == "02"
check dt.format("t") == "P"
check dt.format("tt") == "PM"
check dt.format("yy") == "02"
check dt.format("yyyy") == "0002"
check dt.format("YYYY") == "2"
check dt.format("uuuu") == "-0001"
check dt.format("UUUU") == "-1"
check dt.format("z") == "-1"
check dt.format("zz") == "-01"
check dt.format("zzz") == "-01:02"
check dt.format("zzzz") == "-01:02:03"
check dt.format("g") == "BC"
check dt.format("fff") == "123"
check dt.format("ffffff") == "123456"
check dt.format("fffffffff") == "123456789"
dt.nanosecond = 1
check dt.format("fff") == "000"
check dt.format("ffffff") == "000000"
check dt.format("fffffffff") == "000000001"
dt.year = 12345
check dt.format("yyyy") == "+12345"
check dt.format("uuuu") == "+12345"
dt.year = -12345
check dt.format("yyyy") == "+12346"
check dt.format("uuuu") == "-12345"
expect ValueError:
discard initTimeFormat("'")
expect ValueError:
discard initTimeFormat("'foo")
expect ValueError:
discard initTimeFormat("foo'")
test "parse":
check $parse("20180101", "yyyyMMdd", utc()) == "2018-01-01T00:00:00Z"
parseTestExcp("+120180101", "yyyyMMdd")
check parse("1", "YYYY", utc()).year == 1
check parse("1 BC", "YYYY g", utc()).year == 0
check parse("0001 BC", "yyyy g", utc()).year == 0
check parse("+12345 BC", "yyyy g", utc()).year == -12344
check parse("1 AD", "YYYY g", utc()).year == 1
check parse("0001 AD", "yyyy g", utc()).year == 1
check parse("+12345 AD", "yyyy g", utc()).year == 12345
check parse("-1", "UUUU", utc()).year == -1
check parse("-0001", "uuuu", utc()).year == -1
discard parse("foobar", "'foobar'")
discard parse("foo'bar", "'foo''''bar'")
discard parse("'", "''")
parseTestExcp("2000 A", "yyyy g")