Date/time parsing with changes suggested by Araq

This commit is contained in:
Jeff
2015-02-11 15:42:33 +09:00
parent 5424ab3d7e
commit 3643068527

View File

@@ -16,7 +16,7 @@
# of the standard library!
import
strutils, pegs
strutils, parseutils
include "system/inclrtl"
@@ -744,334 +744,283 @@ proc format*(info: TimeInfo, f: string): string =
{.pop.}
proc getMonth(m: int): Month =
case m
of 1:
return mJan
of 2:
return mFeb
of 3:
return mMar
of 4:
return mApr
of 5:
return mMay
of 6:
return mJun
of 7:
return mJul
of 8:
return mAug
of 9:
return mSep
of 10:
return mOct
of 11:
return mNov
of 12:
return mDec
else:
raise newException(ValueError, "invalid month")
proc parseToken(info: var TimeInfo; token, value: string; j: var int) =
## Helper of the parse proc to parse individual tokens.
case token
of "d":
if value[j+1] in Digits:
# two digit format
info.monthday = value[j..j+1].parseInt()
j += 2
else:
info.monthday = parseInt($value[j])
j += 1
of "dd":
# two digit format
info.monthday = value[j..j+1].parseInt()
j += 2
of "ddd":
case value[j..j+2].toLower():
of "sun":
info.weekday = dSun
of "mon":
info.weekday = dMon
of "tue":
info.weekday = dTue
of "wed":
info.weekday = dWed
of "thu":
info.weekday = dThu
of "fri":
info.weekday = dFri
of "sat":
info.weekday = dSat
else:
raise newException(ValueError, "invalid day of week ")
j += 3
of "dddd":
if value.match(peg"^i'sunday'.*"):
info.weekday = dSun
j += 6
elif value.match(peg"^i'monday'.*"):
info.weekday = dMon
j += 6
elif value.match(peg"^i'tuesday'.*"):
info.weekday = dTue
j += 7
elif value.match(peg"^i'wednesday'.*"):
info.weekday = dWed
j += 9
elif value.match(peg"^i'thursday'.*"):
info.weekday = dThu
j += 8
elif value.match(peg"^i'friday'.*"):
info.weekday = dFri
j += 6
elif value.match(peg"^i'saturday'.*"):
info.weekday = dSat
j += 8
else:
raise newException(ValueError, "invalid day of week ")
of "h", "H":
if value[j+1] in Digits:
# two digit format
info.hour = value[j..j+1].parseInt()
j += 2
else:
info.hour = parseInt($value[j])
j += 1
of "hh", "HH":
# two digit format
info.hour = value[j..j+1].parseInt()
j += 2
of "m":
if value[j+1] in Digits:
# two digit format
info.minute = value[j..j+1].parseInt()
j += 2
else:
info.minute = parseInt($value[j])
j += 1
of "mm":
# two digit format
info.minute = value[j..j+1].parseInt()
j += 2
of "M":
var month: int
if value[j+1] in Digits:
# two digit format
month = value[j..j+1].parseInt()
j += 2
else:
month = parseInt($value[j])
j += 1
info.month = getMonth(month)
of "MM":
var month = value[j..j+1].parseInt()
j += 2
info.month = getMonth(month)
of "MMM":
case value[j..j+2].toLower():
of "jan":
info.month = mJan
of "feb":
info.month = mFeb
of "mar":
info.month = mMar
of "apr":
info.month = mApr
of "may":
info.month = mMay
of "jun":
info.month = mJun
of "jul":
info.month = mJul
of "aug":
info.month = mAug
of "sep":
info.month = mSep
of "oct":
info.month = mOct
of "nov":
info.month = mNov
of "dec":
info.month = mDec
else:
raise newException(ValueError, "invalid month")
j += 3
of "MMMM":
if value.match(peg"^i'january'.*"):
info.month = mJan
j += 7
elif value.match(peg"^i'february'.*"):
info.month = mFeb
j += 8
elif value.match(peg"^i'march'.*"):
info.month = mMar
j += 5
elif value.match(peg"^i'april'.*"):
info.month = mApr
j += 5
elif value.match(peg"^i'may'.*"):
info.month = mMay
j += 3
elif value.match(peg"^i'june'.*"):
info.month = mJun
j += 4
elif value.match(peg"^i'july'.*"):
info.month = mJul
j += 4
elif value.match(peg"^i'august'.*"):
info.month = mAug
j += 6
elif value.match(peg"^i'september'.*"):
info.month = mSep
j += 9
elif value.match(peg"^i'october'.*"):
info.month = mOct
j += 7
elif value.match(peg"^i'november'.*"):
info.month = mNov
j += 8
elif value.match(peg"^i'december'.*"):
info.month = mDec
j += 8
else:
raise newException(ValueError, "invalid month")
of "s":
if value[j+1] in Digits:
# two digit format
info.second = value[j..j+1].parseInt()
j += 2
else:
info.second = parseInt($value[j])
j += 1
of "ss":
# two digit format
info.second = value[j..j+1].parseInt()
j += 2
of "t":
if value[j] == 'P' and info.hour > 0 and info.hour < 12:
info.hour += 12
j += 1
of "tt":
if value[j..j+1] == "PM" and info.hour > 0 and info.hour < 12:
info.hour += 12
j += 2
of "yy":
# Assumes current century
var year = value[j..j+1].parseInt()
var thisCen = getLocalTime(getTime()).year div 100
info.year = thisCen*100 + year
j += 2
of "yyyy":
info.year = value[j..j+3].parseInt()
j += 4
of "z":
if value[j] == '+':
info.timezone = parseInt($value[j+1])
elif value[j] == '-':
info.timezone = 0-parseInt($value[j+1])
else:
raise newException(ValueError, "Sign for timezone " & value[j])
j += 2
of "zz":
if value[j] == '+':
info.timezone = value[j+1..j+2].parseInt()
elif value[j] == '-':
info.timezone = 0-value[j+1..j+2].parseInt()
else:
raise newException(ValueError, "Sign for timezone " & value[j])
j += 3
of "zzz":
if value[j] == '+':
info.timezone = value[j+1..j+2].parseInt()
elif value[j] == '-':
info.timezone = 0-value[j+1..j+2].parseInt()
else:
raise newException(ValueError, "Sign for timezone " & value[j])
j += 6
of "ZZZ":
info.tzname = value[j..j+2].toUpper()
j += 3
## Helper of the parse proc to parse individual tokens.
var sv: int
case token
of "d":
var pd = parseInt(value[j..j+1], sv)
info.monthday = sv
j += pd
of "dd":
info.monthday = value[j..j+1].parseInt()
j += 2
of "ddd":
case value[j..j+2].toLower():
of "sun":
info.weekday = dSun
of "mon":
info.weekday = dMon
of "tue":
info.weekday = dTue
of "wed":
info.weekday = dWed
of "thu":
info.weekday = dThu
of "fri":
info.weekday = dFri
of "sat":
info.weekday = dSat
else:
# Ignore the token and move forward in the value string by the same length
j += token.len
raise newException(ValueError, "invalid day of week ")
j += 3
of "dddd":
if value.len >= j+6 and value[j..j+5].cmpIgnoreCase("sunday") == 0:
info.weekday = dSun
j += 6
elif value.len >= j+6 and value[j..j+5].cmpIgnoreCase("monday") == 0:
info.weekday = dMon
j += 6
elif value.len >= j+7 and value[j..j+6].cmpIgnoreCase("tuesday") == 0:
info.weekday = dTue
j += 7
elif value.len >= j+9 and value[j..j+8].cmpIgnoreCase("wednesday") == 0:
info.weekday = dWed
j += 9
elif value.len >= j+8 and value[j..j+7].cmpIgnoreCase("thursday") == 0:
info.weekday = dThu
j += 8
elif value.len >= j+6 and value[j..j+5].cmpIgnoreCase("friday") == 0:
info.weekday = dFri
j += 6
elif value.len >= j+8 and value[j..j+7].cmpIgnoreCase("saturday") == 0:
info.weekday = dSat
j += 8
else:
raise newException(ValueError, "invalid day of week ")
of "h", "H":
var pd = parseInt(value[j..j+1], sv)
info.hour = sv
j += pd
of "hh", "HH":
info.hour = value[j..j+1].parseInt()
j += 2
of "m":
var pd = parseInt(value[j..j+1], sv)
info.minute = sv
j += pd
of "mm":
info.minute = value[j..j+1].parseInt()
j += 2
of "M":
var pd = parseInt(value[j..j+1], sv)
info.month = Month(sv-1)
info.monthday = sv
j += pd
of "MM":
var month = value[j..j+1].parseInt()
j += 2
info.month = Month(month-1)
of "MMM":
case value[j..j+2].toLower():
of "jan":
info.month = mJan
of "feb":
info.month = mFeb
of "mar":
info.month = mMar
of "apr":
info.month = mApr
of "may":
info.month = mMay
of "jun":
info.month = mJun
of "jul":
info.month = mJul
of "aug":
info.month = mAug
of "sep":
info.month = mSep
of "oct":
info.month = mOct
of "nov":
info.month = mNov
of "dec":
info.month = mDec
else:
raise newException(ValueError, "invalid month")
j += 3
of "MMMM":
if value.len >= j+7 and value[j..j+6].cmpIgnoreCase("january") == 0:
info.month = mJan
j += 7
elif value.len >= j+8 and value[j..j+7].cmpIgnoreCase("february") == 0:
info.month = mFeb
j += 8
elif value.len >= j+5 and value[j..j+4].cmpIgnoreCase("march") == 0:
info.month = mMar
j += 5
elif value.len >= j+5 and value[j..j+4].cmpIgnoreCase("april") == 0:
info.month = mApr
j += 5
elif value.len >= j+3 and value[j..j+2].cmpIgnoreCase("may") == 0:
info.month = mMay
j += 3
elif value.len >= j+4 and value[j..j+3].cmpIgnoreCase("june") == 0:
info.month = mJun
j += 4
elif value.len >= j+4 and value[j..j+3].cmpIgnoreCase("july") == 0:
info.month = mJul
j += 4
elif value.len >= j+6 and value[j..j+5].cmpIgnoreCase("august") == 0:
info.month = mAug
j += 6
elif value.len >= j+9 and value[j..j+8].cmpIgnoreCase("september") == 0:
info.month = mSep
j += 9
elif value.len >= j+7 and value[j..j+6].cmpIgnoreCase("october") == 0:
info.month = mOct
j += 7
elif value.len >= j+8 and value[j..j+7].cmpIgnoreCase("november") == 0:
info.month = mNov
j += 8
elif value.len >= j+8 and value[j..j+7].cmpIgnoreCase("december") == 0:
info.month = mDec
j += 8
else:
raise newException(ValueError, "invalid month")
of "s":
var pd = parseInt(value[j..j+1], sv)
info.second = sv
j += pd
of "ss":
info.second = value[j..j+1].parseInt()
j += 2
of "t":
if value[j] == 'P' and info.hour > 0 and info.hour < 12:
info.hour += 12
j += 1
of "tt":
if value[j..j+1] == "PM" and info.hour > 0 and info.hour < 12:
info.hour += 12
j += 2
of "yy":
# Assumes current century
var year = value[j..j+1].parseInt()
var thisCen = getLocalTime(getTime()).year div 100
info.year = thisCen*100 + year
j += 2
of "yyyy":
info.year = value[j..j+3].parseInt()
j += 4
of "z":
if value[j] == '+':
info.timezone = parseInt($value[j+1])
elif value[j] == '-':
info.timezone = 0-parseInt($value[j+1])
else:
raise newException(ValueError, "Sign for timezone " & value[j])
j += 2
of "zz":
if value[j] == '+':
info.timezone = value[j+1..j+2].parseInt()
elif value[j] == '-':
info.timezone = 0-value[j+1..j+2].parseInt()
else:
raise newException(ValueError, "Sign for timezone " & value[j])
j += 3
of "zzz":
if value[j] == '+':
info.timezone = value[j+1..j+2].parseInt()
elif value[j] == '-':
info.timezone = 0-value[j+1..j+2].parseInt()
else:
raise newException(ValueError, "Sign for timezone " & value[j])
j += 6
of "ZZZ":
info.tzname = value[j..j+2].toUpper()
j += 3
else:
# Ignore the token and move forward in the value string by the same length
j += token.len
proc parse*(value, layout: string): TimeInfo =
## This function parses a date/time string using the standard format identifiers (below)
## The function defaults information not provided in the format string from the running program (timezone, month, year, etc)
##
## ========== ================================================================================= ================================================
## Specifier Description Example
## ========== ================================================================================= ================================================
## d Numeric value of the day of the month, it will be one or two digits long. ``1/04/2012 -> 1``, ``21/04/2012 -> 21``
## dd Same as above, but always two digits. ``1/04/2012 -> 01``, ``21/04/2012 -> 21``
## ddd Three letter string which indicates the day of the week. ``Saturday -> Sat``, ``Monday -> Mon``
## dddd Full string for the day of the week. ``Saturday -> Saturday``, ``Monday -> Monday``
## h The hours in one digit if possible. Ranging from 0-12. ``5pm -> 5``, ``2am -> 2``
## hh The hours in two digits always. If the hour is one digit 0 is prepended. ``5pm -> 05``, ``11am -> 11``
## H The hours in one digit if possible, randing from 0-24. ``5pm -> 17``, ``2am -> 2``
## HH The hours in two digits always. 0 is prepended if the hour is one digit. ``5pm -> 17``, ``2am -> 02``
## m The minutes in 1 digit if possible. ``5:30 -> 30``, ``2:01 -> 1``
## mm Same as above but always 2 digits, 0 is prepended if the minute is one digit. ``5:30 -> 30``, ``2:01 -> 01``
## M The month in one digit if possible. ``September -> 9``, ``December -> 12``
## MM The month in two digits always. 0 is prepended. ``September -> 09``, ``December -> 12``
## MMM Abbreviated three-letter form of the month. ``September -> Sep``, ``December -> Dec``
## MMMM Full month string, properly capitalized. ``September -> September``
## s Seconds as one digit if possible. ``00:00:06 -> 6``
## ss Same as above but always two digits. 0 is prepended. ``00:00:06 -> 06``
## t ``A`` when time is in the AM. ``P`` when time is in the PM.
## tt Same as above, but ``AM`` and ``PM`` instead of ``A`` and ``P`` respectively.
## yy Displays the year to two digits. ``2012 -> 12``
## yyyy Displays the year to four digits. ``2012 -> 2012``
## z Displays the timezone offset from UTC. ``GMT+7 -> +7``, ``GMT-5 -> -5``
## zz Same as above but with leading 0. ``GMT+7 -> +07``, ``GMT-5 -> -05``
## zzz Same as above but with ``:00``. ``GMT+7 -> +07:00``, ``GMT-5 -> -05:00``
## ZZZ Displays the name of the timezone. ``GMT -> GMT``, ``EST -> EST``
## ========== ================================================================================= ================================================
##
## Other strings can be inserted by putting them in ``''``. For example
## ``hh'->'mm`` will give ``01->56``. The following characters can be
## inserted without quoting them: ``:`` ``-`` ``(`` ``)`` ``/`` ``[`` ``]``
## ``,``. However you don't need to necessarily separate format specifiers, a
## unambiguous format string like ``yyyyMMddhhmmss`` is valid too.
var i = 0 # pointer for format string
var j = 0 # pointer for value string
var token = ""
# Assumes current day of week, month and year, but time is reset to 00:00:00
var info = getLocalTime(getTime())
info.hour = 0
info.minute = 0
info.second = 0
while true:
case layout[i]
of ' ', '-', '/', ':', '\'', '\0', '(', ')', '[', ']', ',':
if token.len > 0:
parseToken(info, token, value, j)
# Reset token
token = ""
# Break if at end of line
if layout[i] == '\0': break
# Skip separator and everything between single quotes
# These are literals in both the layout and the value string
if layout[i] == '\'':
inc(i)
inc(j)
while layout[i] != '\'' and layout.len-1 > i:
inc(i)
inc(j)
else:
inc(i)
inc(j)
else:
# Check if the letter being added matches previous accumulated buffer.
if token.len < 1 or token[high(token)] == layout[i]:
token.add(layout[i])
inc(i)
else:
parseToken(info, token, value, j)
token = ""
return info
## This function parses a date/time string using the standard format identifiers (below)
## The function defaults information not provided in the format string from the running program (timezone, month, year, etc)
##
## ========== ================================================================================= ================================================
## Specifier Description Example
## ========== ================================================================================= ================================================
## d Numeric value of the day of the month, it will be one or two digits long. ``1/04/2012 -> 1``, ``21/04/2012 -> 21``
## dd Same as above, but always two digits. ``1/04/2012 -> 01``, ``21/04/2012 -> 21``
## ddd Three letter string which indicates the day of the week. ``Saturday -> Sat``, ``Monday -> Mon``
## dddd Full string for the day of the week. ``Saturday -> Saturday``, ``Monday -> Monday``
## h The hours in one digit if possible. Ranging from 0-12. ``5pm -> 5``, ``2am -> 2``
## hh The hours in two digits always. If the hour is one digit 0 is prepended. ``5pm -> 05``, ``11am -> 11``
## H The hours in one digit if possible, randing from 0-24. ``5pm -> 17``, ``2am -> 2``
## HH The hours in two digits always. 0 is prepended if the hour is one digit. ``5pm -> 17``, ``2am -> 02``
## m The minutes in 1 digit if possible. ``5:30 -> 30``, ``2:01 -> 1``
## mm Same as above but always 2 digits, 0 is prepended if the minute is one digit. ``5:30 -> 30``, ``2:01 -> 01``
## M The month in one digit if possible. ``September -> 9``, ``December -> 12``
## MM The month in two digits always. 0 is prepended. ``September -> 09``, ``December -> 12``
## MMM Abbreviated three-letter form of the month. ``September -> Sep``, ``December -> Dec``
## MMMM Full month string, properly capitalized. ``September -> September``
## s Seconds as one digit if possible. ``00:00:06 -> 6``
## ss Same as above but always two digits. 0 is prepended. ``00:00:06 -> 06``
## t ``A`` when time is in the AM. ``P`` when time is in the PM.
## tt Same as above, but ``AM`` and ``PM`` instead of ``A`` and ``P`` respectively.
## yy Displays the year to two digits. ``2012 -> 12``
## yyyy Displays the year to four digits. ``2012 -> 2012``
## z Displays the timezone offset from UTC. ``GMT+7 -> +7``, ``GMT-5 -> -5``
## zz Same as above but with leading 0. ``GMT+7 -> +07``, ``GMT-5 -> -05``
## zzz Same as above but with ``:00``. ``GMT+7 -> +07:00``, ``GMT-5 -> -05:00``
## ZZZ Displays the name of the timezone. ``GMT -> GMT``, ``EST -> EST``
## ========== ================================================================================= ================================================
##
## Other strings can be inserted by putting them in ``''``. For example
## ``hh'->'mm`` will give ``01->56``. The following characters can be
## inserted without quoting them: ``:`` ``-`` ``(`` ``)`` ``/`` ``[`` ``]``
## ``,``. However you don't need to necessarily separate format specifiers, a
## unambiguous format string like ``yyyyMMddhhmmss`` is valid too.
var i = 0 # pointer for format string
var j = 0 # pointer for value string
var token = ""
# Assumes current day of month, month and year, but time is reset to 00:00:00. Weekday will be reset after parsing.
var info = getLocalTime(getTime())
info.hour = 0
info.minute = 0
info.second = 0
while true:
case layout[i]
of ' ', '-', '/', ':', '\'', '\0', '(', ')', '[', ']', ',':
if token.len > 0:
parseToken(info, token, value, j)
# Reset token
token = ""
# Break if at end of line
if layout[i] == '\0': break
# Skip separator and everything between single quotes
# These are literals in both the layout and the value string
if layout[i] == '\'':
inc(i)
inc(j)
while layout[i] != '\'' and layout.len-1 > i:
inc(i)
inc(j)
else:
inc(i)
inc(j)
else:
# Check if the letter being added matches previous accumulated buffer.
if token.len < 1 or token[high(token)] == layout[i]:
token.add(layout[i])
inc(i)
else:
parseToken(info, token, value, j)
token = ""
# Reset weekday as it might not have been provided and the default may be wrong
info.weekday = getLocalTime(timeInfoToTime(info)).weekday
return info
when isMainModule:
@@ -1095,12 +1044,12 @@ when isMainModule:
" ss t tt y yy yyy yyyy yyyyy z zz zzz 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 UTC"
when not defined(JS) and sizeof(Time) == 8:
var t3 = getGMTime(fromSeconds(889067643645)) # Fri 7 Jun 19:20:45 BST 30143
assert t3.format("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 ZZZ") ==
"7 07 Fri Friday 6 06 18 18 20 20 6 06 Jun June 45 45 P PM 3 43 143 0143 30143 0 00 00:00 UTC"
assert t3.format(":,[]()-/") == ":,[]()-/"
# when not defined(JS) and sizeof(Time) == 8:
# var t3 = getGMTime(fromSeconds(889067643645)) # Fri 7 Jun 19:20:45 BST 30143
# assert t3.format("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 ZZZ") ==
# "7 07 Fri Friday 6 06 18 18 20 20 6 06 Jun June 45 45 P PM 3 43 143 0143 30143 0 00 00:00 UTC"
# assert t3.format(":,[]()-/") == ":,[]()-/"
var t4 = getGMTime(fromSeconds(876124714)) # Mon 6 Oct 08:58:34 BST 1997
assert t4.format("M MM MMM MMMM") == "10 10 Oct October"
@@ -1109,9 +1058,9 @@ when isMainModule:
assert((t4 - initInterval(years = 2)).format("yyyy") == "1995")
assert((t4 - initInterval(years = 7, minutes = 34, seconds = 24)).format("yyyy mm ss") == "1990 24 10")
var s = "09:04am on Dec 15, 2015"
var f = "hh:mmtt on MMM d, yyyy"
assert($s.parse(f) == "Mon Dec 15 09:04:00 2015")
var s = "Tuesday at 09:04am on Dec 15, 2015"
var f = "dddd at hh:mmtt on MMM d, yyyy"
assert($s.parse(f) == "Tue Dec 15 09:04:00 2015")
# ANSIC = "Mon Jan _2 15:04:05 2006"
s = "Mon Jan 2 15:04:05 2006"
f = "ddd MMM d HH:mm:ss yyyy"