Don't use parseutils.parseInt in the times module (#10028)

This commit is contained in:
Oscar Nihlgård
2018-12-22 10:41:54 +01:00
committed by Andreas Rumpf
parent ca672ec62e
commit 2539363851
2 changed files with 78 additions and 39 deletions

View File

@@ -122,12 +122,8 @@
only for years in the range 1..9999).
]##
{.push debugger:off.} # the user does not want to trace a part
# of the standard library!
import
strutils, parseutils, algorithm, math, options, strformat
strutils, algorithm, math, options, strformat
include "system/inclrtl"
@@ -304,7 +300,6 @@ const
secondsInMin = 60
secondsInHour = 60*60
secondsInDay = 60*60*24
minutesInHour = 60
rateDiff = 10000000'i64 # 100 nsecs
# The number of hectonanoseconds between 1601/01/01 (windows epoch)
# and 1970/01/01 (unix epoch).
@@ -1602,6 +1597,12 @@ type
## be encoded as ``@[Lit.byte, 3.byte, 'f'.byte, 'o'.byte, 'o'.byte]``.
formatStr: string
TimeParseError* = object of ValueError ## \
## Raised when parsing input using a ``TimeFormat`` fails.
TimeFormatParseError* = object of ValueError ## \
## Raised when parsing a ``TimeFormat`` string fails.
const FormatLiterals = { ' ', '-', '/', ':', '(', ')', '[', ']', ',' }
proc `$`*(f: TimeFormat): string =
@@ -1612,9 +1613,34 @@ proc `$`*(f: TimeFormat): string =
f.formatStr
proc raiseParseException(f: TimeFormat, input: string, msg: string) =
raise newException(ValueError,
raise newException(TimeParseError,
&"Failed to parse '{input}' with format '{f}'. {msg}")
proc parseInt(s: string, b: var int, start = 0, maxLen = int.high,
allowSign = false): int =
var sign = -1
var i = start
let stop = start + min(s.high - start + 1, maxLen) - 1
if allowSign and i <= stop:
if s[i] == '+':
inc(i)
elif s[i] == '-':
inc(i)
sign = 1
if i <= stop and s[i] in {'0'..'9'}:
b = 0
while i <= stop and s[i] in {'0'..'9'}:
let c = ord(s[i]) - ord('0')
if b >= (low(int) + c) div 10:
b = b * 10 - c
else:
return 0
inc(i)
if sign == -1 and b == low(int):
return 0
b = b * sign
result = i - start
iterator tokens(f: string): tuple[kind: FormatTokenKind, token: string] =
var i = 0
var currToken = ""
@@ -1639,7 +1665,7 @@ iterator tokens(f: string): tuple[kind: FormatTokenKind, token: string] =
i.inc
if i > f.high:
raise newException(ValueError,
raise newException(TimeFormatParseError,
&"Unclosed ' in time format string. " &
"For a literal ', use ''.")
i.inc
@@ -1696,7 +1722,8 @@ proc stringToPattern(str: string): FormatPattern =
of "zzz": result = zzz
of "zzzz": result = zzzz
of "g": result = g
else: raise newException(ValueError, &"'{str}' is not a valid pattern")
else: raise newException(TimeFormatParseError,
&"'{str}' is not a valid pattern")
proc initTimeFormat*(format: string): TimeFormat =
## Construct a new time format for parsing & formatting time types.
@@ -1715,7 +1742,7 @@ proc initTimeFormat*(format: string): TimeFormat =
else:
result.patterns.add(FormatPattern.Lit.byte)
if token.len > 255:
raise newException(ValueError,
raise newException(TimeFormatParseError,
"Format literal is to long:" & token)
result.patterns.add(token.len.byte)
for c in token:
@@ -1833,15 +1860,10 @@ proc formatPattern(dt: DateTime, pattern: FormatPattern, result: var string) =
proc parsePattern(input: string, pattern: FormatPattern, i: var int,
parsed: var ParsedTime): bool =
template takeInt(allowedWidth: Slice[int]): int =
template takeInt(allowedWidth: Slice[int], allowSign = false): int =
var sv: int
let max = i + allowedWidth.b - 1
var pd =
if max > input.high:
parseInt(input, sv, i)
else:
parseInt(input[i..max], sv)
if pd notin allowedWidth:
var pd = parseInt(input, sv, i, allowedWidth.b, allowSign)
if pd < allowedWidth.a:
return false
i.inc pd
sv
@@ -1853,11 +1875,13 @@ proc parsePattern(input: string, pattern: FormatPattern, i: var int,
case pattern
of d:
parsed.monthday = some(takeInt(1..2))
result = parsed.monthday.get() in MonthdayRange
let monthday = takeInt(1..2)
parsed.monthday = some(monthday)
result = monthday in MonthdayRange
of dd:
parsed.monthday = some(takeInt(2..2))
result = parsed.monthday.get() in MonthdayRange
let monthday = takeInt(2..2)
parsed.monthday = some(monthday)
result = monthday in MonthdayRange
of ddd:
result = input.substr(i, i+2).toLowerAscii() in [
"sun", "mon", "tue", "wed", "thu", "fri", "sat"]
@@ -1993,7 +2017,7 @@ proc parsePattern(input: string, pattern: FormatPattern, i: var int,
of yyyy:
let year =
if input[i] in { '+', '-' }:
takeInt(4..high(int))
takeInt(4..high(int), allowSign = true)
else:
takeInt(4..4)
result = year > 0
@@ -2005,12 +2029,12 @@ proc parsePattern(input: string, pattern: FormatPattern, i: var int,
of uuuu:
let year =
if input[i] in { '+', '-' }:
takeInt(4..high(int))
takeInt(4..high(int), allowSign = true)
else:
takeInt(4..4)
parsed.year = some(year)
of UUUU:
parsed.year = some(takeInt(1..high(int)))
parsed.year = some(takeInt(1..high(int), allowSign = true))
of z, zz, zzz, zzzz:
case input[i]
of '+', '-':
@@ -2055,9 +2079,8 @@ proc parsePattern(input: string, pattern: FormatPattern, i: var int,
else:
result = false
of y, yyy, yyyyy:
raise newException(ValueError,
&"The pattern '{pattern}' is only valid for formatting")
of Lit: assert false # Can't happen
raiseAssert "Pattern is invalid for parsing: " & $pattern
of Lit: doAssert false, "Can't happen"
proc toDateTime(p: ParsedTime, zone: Timezone, f: TimeFormat,
input: string): DateTime =
@@ -2147,7 +2170,8 @@ proc format*(dt: DateTime, f: TimeFormat): string {.raises: [].} =
formatPattern(dt, f.patterns[idx].FormatPattern, result = result)
idx.inc
proc format*(dt: DateTime, f: string): string =
proc format*(dt: DateTime, f: string): string
{.raises: [TimeFormatParseError].} =
## Shorthand for constructing a ``TimeFormat`` and using it to format ``dt``.
##
## See `Parsing and formatting dates`_ for documentation of the
@@ -2163,7 +2187,8 @@ proc format*(dt: DateTime, f: static[string]): string {.raises: [].} =
const f2 = initTimeFormat(f)
result = dt.format(f2)
proc format*(time: Time, f: string, zone: Timezone = local()): string {.tags: [].} =
proc format*(time: Time, f: string, zone: Timezone = local()): string
{.raises: [TimeFormatParseError].} =
## Shorthand for constructing a ``TimeFormat`` and using it to format
## ``time``. Will use the timezone specified by ``zone``.
##
@@ -2175,13 +2200,14 @@ proc format*(time: Time, f: string, zone: Timezone = local()): string {.tags: []
doAssert format(tm, "yyyy-MM-dd'T'HH:mm:ss", utc()) == "1970-01-01T00:00:00"
time.inZone(zone).format(f)
proc format*(time: Time, f: static[string],
zone: Timezone = local()): string {.tags: [].} =
proc format*(time: Time, f: static[string], zone: Timezone = local()): string
{.raises: [].} =
## Overload that validates ``f`` at compile time.
const f2 = initTimeFormat(f)
result = time.inZone(zone).format(f2)
proc parse*(input: string, f: TimeFormat, zone: Timezone = local()): DateTime =
proc parse*(input: string, f: TimeFormat, zone: Timezone = local()): DateTime
{.raises: [TimeParseError, Defect].} =
## Parses ``input`` as a ``DateTime`` using the format specified by ``f``.
## If no UTC offset was parsed, then ``input`` is assumed to be specified in
## the ``zone`` timezone. If a UTC offset was parsed, the result will be
@@ -2221,7 +2247,8 @@ proc parse*(input: string, f: TimeFormat, zone: Timezone = local()): DateTime =
result = toDateTime(parsed, zone, f, input)
proc parse*(input, f: string, tz: Timezone = local()): DateTime =
proc parse*(input, f: string, tz: Timezone = local()): DateTime
{.raises: [TimeParseError, TimeFormatParseError, Defect].} =
## Shorthand for constructing a ``TimeFormat`` and using it to parse
## ``input`` as a ``DateTime``.
##
@@ -2233,12 +2260,14 @@ proc parse*(input, f: string, tz: Timezone = local()): DateTime =
let dtFormat = initTimeFormat(f)
result = input.parse(dtFormat, tz)
proc parse*(input: string, f: static[string], zone: Timezone = local()): DateTime =
proc parse*(input: string, f: static[string], zone: Timezone = local()): DateTime
{.raises: [TimeParseError, Defect].} =
## Overload that validates ``f`` at compile time.
const f2 = initTimeFormat(f)
result = input.parse(f2, zone)
proc parseTime*(input, f: string, zone: Timezone): Time =
proc parseTime*(input, f: string, zone: Timezone): Time
{.raises: [TimeParseError, TimeFormatParseError, Defect].} =
## Shorthand for constructing a ``TimeFormat`` and using it to parse
## ``input`` as a ``DateTime``, then converting it a ``Time``.
##
@@ -2249,7 +2278,8 @@ proc parseTime*(input, f: string, zone: Timezone): Time =
doAssert parseTime(tStr, "yyyy-MM-dd'T'HH:mm:sszzz", utc()) == fromUnix(0)
parse(input, f, zone).toTime()
proc parseTime*(input: string, f: static[string], zone: Timezone): Time =
proc parseTime*(input: string, f: static[string], zone: Timezone): Time
{.raises: [TimeParseError, Defect].} =
## Overload that validates ``format`` at compile time.
const f2 = initTimeFormat(f)
result = input.parse(f2, zone).toTime()
@@ -2275,8 +2305,6 @@ proc `$`*(time: Time): string {.tags: [], raises: [], benign.} =
doAssert $tm == "1970-01-01T00:00:00" & format(dt, "zzz")
$time.local
{.pop.}
proc countLeapYears*(yearSpan: int): int =
## Returns the number of leap years spanned by a given number of years.
##

View File

@@ -245,6 +245,17 @@ suite "ttimes":
parseTestExcp("12345", "uuuu")
parseTestExcp("-1 BC", "UUUU g")
test "incorrect inputs: invalid sign":
parseTestExcp("+1", "YYYY")
parseTestExcp("+1", "dd")
parseTestExcp("+1", "MM")
parseTestExcp("+1", "hh")
parseTestExcp("+1", "mm")
parseTestExcp("+1", "ss")
test "_ as a seperator":
discard parse("2000_01_01", "YYYY'_'MM'_'dd")
test "dynamic timezone":
let tz = staticTz(seconds = -9000)
let dt = initDateTime(1, mJan, 2000, 12, 00, 00, tz)