From f2ba3d174c3e6b65d5785d1e621ce702c4bc36e1 Mon Sep 17 00:00:00 2001 From: Oscar Nihlgård Date: Tue, 10 Oct 2017 15:47:12 +0200 Subject: Fix countLeapYears --- tests/stdlib/ttimes.nim | 6 ++++++ 1 file changed, 6 insertions(+) (limited to 'tests/stdlib') diff --git a/tests/stdlib/ttimes.nim b/tests/stdlib/ttimes.nim index 05c91ccb2..84e00f8de 100644 --- a/tests/stdlib/ttimes.nim +++ b/tests/stdlib/ttimes.nim @@ -235,3 +235,9 @@ block dstTest: parsedJul = parse("2016-07-01 04:00:00+01:00", "yyyy-MM-dd HH:mm:sszzz") doAssert toTime(parsedJan) == fromSeconds(1451962800) doAssert toTime(parsedJul) == fromSeconds(1467342000) + +block countLeapYears: + # 1920, 2004 and 2020 are leap years, and should be counted starting at the following year + doAssert countLeapYears(1920) + 1 == countLeapYears(1921) + doAssert countLeapYears(2004) + 1 == countLeapYears(2005) + doAssert countLeapYears(2020) + 1 == countLeapYears(2021) \ No newline at end of file -- cgit 1.4.1-2-gfad0 From ce04288d6492c36f5021198d9d7fe8a6932959e4 Mon Sep 17 00:00:00 2001 From: Viktor Marosvary Date: Tue, 24 Oct 2017 10:22:18 +0200 Subject: isAlphaNumberic and isDigit improvement + tests (#6579) if we encounter a character that does not satisfy the proc, we return immediately, without continuing to loop over the rest of the chars in the string. --- lib/pure/strutils.nim | 6 ++++-- tests/stdlib/tstrutil.nim | 19 +++++++++++++++++++ 2 files changed, 23 insertions(+), 2 deletions(-) (limited to 'tests/stdlib') diff --git a/lib/pure/strutils.nim b/lib/pure/strutils.nim index 3da8094f5..1f56704f7 100644 --- a/lib/pure/strutils.nim +++ b/lib/pure/strutils.nim @@ -138,7 +138,8 @@ proc isAlphaNumeric*(s: string): bool {.noSideEffect, procvar, result = true for c in s: - result = c.isAlphaNumeric() and result + if not c.isAlphaNumeric(): + return false proc isDigit*(s: string): bool {.noSideEffect, procvar, rtl, extern: "nsuIsDigitStr".}= @@ -153,7 +154,8 @@ proc isDigit*(s: string): bool {.noSideEffect, procvar, result = true for c in s: - result = c.isDigit() and result + if not c.isDigit(): + return false proc isSpaceAscii*(s: string): bool {.noSideEffect, procvar, rtl, extern: "nsuIsSpaceAsciiStr".}= diff --git a/tests/stdlib/tstrutil.nim b/tests/stdlib/tstrutil.nim index fef1b38c2..57968df13 100644 --- a/tests/stdlib/tstrutil.nim +++ b/tests/stdlib/tstrutil.nim @@ -64,6 +64,25 @@ proc testDelete = delete(s, 0, 0) assert s == "1236789ABCDEFG" + +proc testIsAlphaNumeric = + assert isAlphaNumeric("abcdABC1234") == true + assert isAlphaNumeric("a") == true + assert isAlphaNumeric("abcABC?1234") == false + assert isAlphaNumeric("abcABC 1234") == false + assert isAlphaNumeric(".") == false + +testIsAlphaNumeric() + +proc testIsDigit = + assert isDigit("1") == true + assert isDigit("1234") == true + assert isDigit("abcABC?1234") == false + assert isDigit(".") == false + assert isDigit(":") == false + +testIsDigit() + proc testFind = assert "0123456789ABCDEFGH".find('A') == 10 assert "0123456789ABCDEFGH".find('A', 5) == 10 -- cgit 1.4.1-2-gfad0 From e13513546981167e4d1ee38993b2589e14531fb9 Mon Sep 17 00:00:00 2001 From: Bo Lingen Date: Sat, 28 Oct 2017 03:47:23 -0500 Subject: add `strutils.removePrefix` proc (#6473) --- lib/pure/strutils.nim | 40 ++++++++++++++++++++++++++++-- tests/stdlib/tstrutil.nim | 62 +++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 100 insertions(+), 2 deletions(-) (limited to 'tests/stdlib') diff --git a/lib/pure/strutils.nim b/lib/pure/strutils.nim index 9400e5c02..1556f7c68 100644 --- a/lib/pure/strutils.nim +++ b/lib/pure/strutils.nim @@ -2345,8 +2345,7 @@ proc removeSuffix*(s: var string, chars: set[char] = Newlines) {. proc removeSuffix*(s: var string, c: char) {. rtl, extern: "nsuRemoveSuffixChar".} = - ## Removes a single character (in-place) from a string. - ## + ## Removes a single character (in-place) from the end of a string. ## .. code-block:: nim ## var ## table = "users" @@ -2368,6 +2367,43 @@ proc removeSuffix*(s: var string, suffix: string) {. newLen -= len(suffix) s.setLen(newLen) +proc removePrefix*(s: var string, chars: set[char] = Newlines) {. + rtl, extern: "nsuRemovePrefixCharSet".} = + ## Removes all characters from `chars` from the start of the string `s` + ## (in-place). + ## .. code-block:: nim + ## var userInput = "\r\n*~Hello World!" + ## userInput.removePrefix + ## doAssert userInput == "*~Hello World!" + ## userInput.removePrefix({'~', '*'}) + ## doAssert userInput == "Hello World!" + ## + ## var otherInput = "?!?Hello!?!" + ## otherInput.removePrefix({'!', '?'}) + ## doAssert otherInput == "Hello!?!" + var start = 0 + while start < s.len and s[start] in chars: start += 1 + if start > 0: s.delete(0, start - 1) + +proc removePrefix*(s: var string, c: char) {. + rtl, extern: "nsuRemovePrefixChar".} = + ## Removes a single character (in-place) from the start of a string. + ## .. code-block:: nim + ## var ident = "pControl" + ## ident.removePrefix('p') + ## doAssert ident == "Control" + removePrefix(s, chars = {c}) + +proc removePrefix*(s: var string, prefix: string) {. + rtl, extern: "nsuRemovePrefixString".} = + ## Remove the first matching prefix (in-place) from a string. + ## .. code-block:: nim + ## var answers = "yesyes" + ## answers.removePrefix("yes") + ## doAssert answers == "yes" + if s.startsWith(prefix): + s.delete(0, prefix.len - 1) + when isMainModule: doAssert align("abc", 4) == " abc" doAssert align("a", 0) == "a" diff --git a/tests/stdlib/tstrutil.nim b/tests/stdlib/tstrutil.nim index 57968df13..2fb0c371f 100644 --- a/tests/stdlib/tstrutil.nim +++ b/tests/stdlib/tstrutil.nim @@ -49,9 +49,71 @@ proc testRemoveSuffix = s.removeSuffix("") assert s == "hello\r\n\r\n" +proc testRemovePrefix = + var s = "\n\rhello" + s.removePrefix + assert s == "hello" + s.removePrefix + assert s == "hello" + + s = "\n\nhello" + s.removePrefix + assert s == "hello" + + s = "\rhello" + s.removePrefix + assert s == "hello" + + s = "hello \n there" + s.removePrefix + assert s == "hello \n there" + + s = "hello" + s.removePrefix("hel") + assert s == "lo" + s.removePrefix('l') + assert s == "o" + + s = "hellos" + s.removePrefix({'h','e'}) + assert s == "llos" + s.removePrefix({'l','o'}) + assert s == "s" + + s = "aeiou" + s.removePrefix("") + assert s == "aeiou" + + s = "" + s.removePrefix("") + assert s == "" + + s = " " + s.removePrefix + assert s == " " + + s = " " + s.removePrefix("") + assert s == " " + + s = " " + s.removePrefix(" ") + assert s == " " + + s = " " + s.removePrefix(' ') + assert s == "" + + # Contrary to Chomp in other languages + # empty string does not change behaviour + s = "\r\n\r\nhello" + s.removePrefix("") + assert s == "\r\n\r\nhello" + proc main() = testStrip() testRemoveSuffix() + testRemovePrefix() for p in split("/home/a1:xyz:/usr/bin", {':'}): write(stdout, p) -- cgit 1.4.1-2-gfad0 From f1dab3908699aef2978b428ed9c15086a2c4bf8c Mon Sep 17 00:00:00 2001 From: Andreas Rumpf Date: Sun, 29 Oct 2017 20:36:07 +0100 Subject: remove old implementation of the roof operator; make tests green again; close #6292 --- compiler/semdata.nim | 1 - compiler/semexprs.nim | 11 +---------- compiler/semmagic.nim | 32 +------------------------------- compiler/semtempl.nim | 18 ------------------ lib/impure/nre.nim | 36 +++++++++++++++++------------------- lib/pure/matchers.nim | 2 +- lib/system.nim | 2 +- tests/array/troof1.nim | 27 +-------------------------- tests/array/troof2.nim | 10 ---------- tests/array/troof3.nim | 5 ++--- tests/array/troof4.nim | 37 ------------------------------------- tests/stdlib/nre/captures.nim | 6 +++--- tests/stdlib/nre/find.nim | 2 +- 13 files changed, 28 insertions(+), 161 deletions(-) delete mode 100644 tests/array/troof2.nim delete mode 100644 tests/array/troof4.nim (limited to 'tests/stdlib') diff --git a/compiler/semdata.nim b/compiler/semdata.nim index b6b5db101..3e57d1104 100644 --- a/compiler/semdata.nim +++ b/compiler/semdata.nim @@ -37,7 +37,6 @@ type # in standalone ``except`` and ``finally`` next*: PProcCon # used for stacking procedure contexts wasForwarded*: bool # whether the current proc has a separate header - bracketExpr*: PNode # current bracket expression (for ^ support) mapping*: TIdTable TMatchedConcept* = object diff --git a/compiler/semexprs.nim b/compiler/semexprs.nim index 195625489..c3aead18e 100644 --- a/compiler/semexprs.nim +++ b/compiler/semexprs.nim @@ -1189,7 +1189,6 @@ proc semSubscript(c: PContext, n: PNode, flags: TExprFlags): PNode = tyCString: if n.len != 2: return nil n.sons[0] = makeDeref(n.sons[0]) - c.p.bracketExpr = n.sons[0] for i in countup(1, sonsLen(n) - 1): n.sons[i] = semExprWithType(c, n.sons[i], flags*{efInTypeof, efDetermineType}) @@ -1210,7 +1209,6 @@ proc semSubscript(c: PContext, n: PNode, flags: TExprFlags): PNode = of tyTuple: if n.len != 2: return nil n.sons[0] = makeDeref(n.sons[0]) - c.p.bracketExpr = n.sons[0] # [] operator for tuples requires constant expression: n.sons[1] = semConstExpr(c, n.sons[1]) if skipTypes(n.sons[1].typ, {tyGenericInst, tyRange, tyOrdinal, tyAlias}).kind in @@ -1248,17 +1246,13 @@ proc semSubscript(c: PContext, n: PNode, flags: TExprFlags): PNode = of skType: result = symNodeFromType(c, semTypeNode(c, n, nil), n.info) else: - c.p.bracketExpr = n.sons[0] - else: - c.p.bracketExpr = n.sons[0] + discard proc semArrayAccess(c: PContext, n: PNode, flags: TExprFlags): PNode = - let oldBracketExpr = c.p.bracketExpr result = semSubscript(c, n, flags) if result == nil: # overloaded [] operator: result = semExpr(c, buildOverloadedSubscripts(n, getIdent"[]")) - c.p.bracketExpr = oldBracketExpr proc propertyWriteAccess(c: PContext, n, nOrig, a: PNode): PNode = var id = considerQuotedIdent(a[1], a) @@ -1330,7 +1324,6 @@ proc semAsgn(c: PContext, n: PNode; mode=asgnNormal): PNode = of nkBracketExpr: # a[i] = x # --> `[]=`(a, i, x) - let oldBracketExpr = c.p.bracketExpr a = semSubscript(c, a, {efLValue}) if a == nil: result = buildOverloadedSubscripts(n.sons[0], getIdent"[]=") @@ -1340,9 +1333,7 @@ proc semAsgn(c: PContext, n: PNode; mode=asgnNormal): PNode = return n else: result = semExprNoType(c, result) - c.p.bracketExpr = oldBracketExpr return result - c.p.bracketExpr = oldBracketExpr of nkCurlyExpr: # a{i} = x --> `{}=`(a, i, x) result = buildOverloadedSubscripts(n.sons[0], getIdent"{}=") diff --git a/compiler/semmagic.nim b/compiler/semmagic.nim index ba19e0865..d721f42ab 100644 --- a/compiler/semmagic.nim +++ b/compiler/semmagic.nim @@ -38,9 +38,7 @@ proc skipAddr(n: PNode): PNode {.inline.} = proc semArrGet(c: PContext; n: PNode; flags: TExprFlags): PNode = result = newNodeI(nkBracketExpr, n.info) for i in 1..\w)").captures["letter"] == "a"`` ## - ``"abc".match(re"(\w)\w").captures[-1] == "ab"`` ## - ## ``captureBounds[]: Option[Slice[int]]`` + ## ``captureBounds[]: Option[Slice[int, int]]`` ## gets the bounds of the given capture according to the same rules as ## the above. If the capture is not filled, then ``None`` is returned. ## The bounds are both inclusive. @@ -167,7 +167,7 @@ type ## ``match: string`` ## the full text of the match. ## - ## ``matchBounds: Slice[int]`` + ## ``matchBounds: Slice[int, int]`` ## the bounds of the match, as in ``captureBounds[]`` ## ## ``(captureBounds|captures).toTable`` @@ -182,9 +182,9 @@ type ## Not nil. str*: string ## The string that was matched against. ## Not nil. - pcreMatchBounds: seq[Slice[cint]] ## First item is the bounds of the match - ## Other items are the captures - ## `a` is inclusive start, `b` is exclusive end + pcreMatchBounds: seq[Slice[cint, cint]] ## First item is the bounds of the match + ## Other items are the captures + ## `a` is inclusive start, `b` is exclusive end Captures* = distinct RegexMatch CaptureBounds* = distinct RegexMatch @@ -251,13 +251,13 @@ proc captureBounds*(pattern: RegexMatch): CaptureBounds = return CaptureBounds(p proc captures*(pattern: RegexMatch): Captures = return Captures(pattern) -proc `[]`*(pattern: CaptureBounds, i: int): Option[Slice[int]] = +proc `[]`*(pattern: CaptureBounds, i: int): Option[Slice[int, int]] = let pattern = RegexMatch(pattern) if pattern.pcreMatchBounds[i + 1].a != -1: let bounds = pattern.pcreMatchBounds[i + 1] return some(int(bounds.a) .. int(bounds.b-1)) else: - return none(Slice[int]) + return none(Slice[int, int]) proc `[]`*(pattern: Captures, i: int): string = let pattern = RegexMatch(pattern) @@ -272,10 +272,10 @@ proc `[]`*(pattern: Captures, i: int): string = proc match*(pattern: RegexMatch): string = return pattern.captures[-1] -proc matchBounds*(pattern: RegexMatch): Slice[int] = +proc matchBounds*(pattern: RegexMatch): Slice[int, int] = return pattern.captureBounds[-1].get -proc `[]`*(pattern: CaptureBounds, name: string): Option[Slice[int]] = +proc `[]`*(pattern: CaptureBounds, name: string): Option[Slice[int, int]] = let pattern = RegexMatch(pattern) return pattern.captureBounds[pattern.pattern.captureNameToId.fget(name)] @@ -295,13 +295,13 @@ proc toTable*(pattern: Captures, default: string = nil): Table[string, string] = result = initTable[string, string]() toTableImpl(nextVal == nil) -proc toTable*(pattern: CaptureBounds, default = none(Slice[int])): - Table[string, Option[Slice[int]]] = - result = initTable[string, Option[Slice[int]]]() +proc toTable*(pattern: CaptureBounds, default = none(Slice[int, int])): + Table[string, Option[Slice[int, int]]] = + result = initTable[string, Option[Slice[int, int]]]() toTableImpl(nextVal.isNone) template itemsImpl(cond: untyped) {.dirty.} = - for i in 0 .. foo)(?bar)?")) check(ex1.captureBounds["foo"] == some(0..2)) - check(ex1.captureBounds["bar"] == none(Slice[int])) + check(ex1.captureBounds["bar"] == none(Slice[int, int])) test "capture count": let ex1 = re("(?foo)(?bar)?") @@ -42,7 +42,7 @@ suite "captures": test "named capture table": let ex1 = "foo".find(re("(?foo)(?bar)?")) check(ex1.captures.toTable == {"foo" : "foo", "bar" : nil}.toTable()) - check(ex1.captureBounds.toTable == {"foo" : some(0..2), "bar" : none(Slice[int])}.toTable()) + check(ex1.captureBounds.toTable == {"foo" : some(0..2), "bar" : none(Slice[int, int])}.toTable()) check(ex1.captures.toTable("") == {"foo" : "foo", "bar" : ""}.toTable()) let ex2 = "foobar".find(re("(?foo)(?bar)?")) @@ -51,7 +51,7 @@ suite "captures": test "capture sequence": let ex1 = "foo".find(re("(?foo)(?bar)?")) check(ex1.captures.toSeq == @["foo", nil]) - check(ex1.captureBounds.toSeq == @[some(0..2), none(Slice[int])]) + check(ex1.captureBounds.toSeq == @[some(0..2), none(Slice[int, int])]) check(ex1.captures.toSeq("") == @["foo", ""]) let ex2 = "foobar".find(re("(?foo)(?bar)?")) diff --git a/tests/stdlib/nre/find.nim b/tests/stdlib/nre/find.nim index caa953ff4..c37ac56ba 100644 --- a/tests/stdlib/nre/find.nim +++ b/tests/stdlib/nre/find.nim @@ -12,7 +12,7 @@ suite "find": test "find bounds": check(toSeq(findIter("1 2 3 4 5 ", re" ")).map( - proc (a: RegexMatch): Slice[int] = a.matchBounds + proc (a: RegexMatch): Slice[int, int] = a.matchBounds ) == @[1..1, 3..3, 5..5, 7..7, 9..9]) test "overlapping find": -- cgit 1.4.1-2-gfad0 From c182d37f4505e6b5acab5b7d1aecaa5ddc8b0044 Mon Sep 17 00:00:00 2001 From: Bo Lingen Date: Mon, 30 Oct 2017 16:45:13 -0500 Subject: Update `removeSuffix` implementations to match `removePrefix` (#6636) --- lib/pure/strutils.nim | 54 +++++++++++++++++++++++------------------------ tests/stdlib/tstrutil.nim | 30 ++++++++++++++++++++++---- 2 files changed, 53 insertions(+), 31 deletions(-) (limited to 'tests/stdlib') diff --git a/lib/pure/strutils.nim b/lib/pure/strutils.nim index 71dcd9269..18b0954f3 100644 --- a/lib/pure/strutils.nim +++ b/lib/pure/strutils.nim @@ -2326,40 +2326,37 @@ proc format*(formatstr: string, a: varargs[string, `$`]): string {.noSideEffect, proc removeSuffix*(s: var string, chars: set[char] = Newlines) {. rtl, extern: "nsuRemoveSuffixCharSet".} = - ## Removes the first matching character from the string (in-place) given a - ## set of characters. If the set of characters is only equal to `Newlines` - ## then it will remove both the newline and return feed. + ## Removes all characters from `chars` from the end of the string `s` + ## (in-place). ## ## .. code-block:: nim - ## var - ## userInput = "Hello World!\r\n" - ## otherInput = "Hello!?!" + ## var userInput = "Hello World!*~\r\n" ## userInput.removeSuffix - ## userInput == "Hello World!" - ## userInput.removeSuffix({'!', '?'}) - ## userInput == "Hello World" + ## doAssert userInput == "Hello World!*~" + ## userInput.removeSuffix({'~', '*'}) + ## doAssert userInput == "Hello World!" + ## + ## var otherInput = "Hello!?!" ## otherInput.removeSuffix({'!', '?'}) - ## otherInput == "Hello!?" + ## doAssert otherInput == "Hello" if s.len == 0: return - var last = len(s) - 1 - if chars == Newlines: - if s[last] == '\10': - last -= 1 - if s[last] == '\13': - last -= 1 - else: - if s[last] in chars: - last -= 1 + var last = s.high + while last > -1 and s[last] in chars: last -= 1 s.setLen(last + 1) proc removeSuffix*(s: var string, c: char) {. rtl, extern: "nsuRemoveSuffixChar".} = - ## Removes a single character (in-place) from the end of a string. + ## Removes all occurrences of a single character (in-place) from the end + ## of a string. + ## ## .. code-block:: nim - ## var - ## table = "users" + ## var table = "users" ## table.removeSuffix('s') - ## table == "user" + ## doAssert table == "user" + ## + ## var dots = "Trailing dots......." + ## dots.removeSuffix('.') + ## doAssert dots == "Trailing dots" removeSuffix(s, chars = {c}) proc removeSuffix*(s: var string, suffix: string) {. @@ -2367,10 +2364,9 @@ proc removeSuffix*(s: var string, suffix: string) {. ## Remove the first matching suffix (in-place) from a string. ## ## .. code-block:: nim - ## var - ## answers = "yeses" + ## var answers = "yeses" ## answers.removeSuffix("es") - ## answers == "yes" + ## doAssert answers == "yes" var newLen = s.len if s.endsWith(suffix): newLen -= len(suffix) @@ -2380,6 +2376,7 @@ proc removePrefix*(s: var string, chars: set[char] = Newlines) {. rtl, extern: "nsuRemovePrefixCharSet".} = ## Removes all characters from `chars` from the start of the string `s` ## (in-place). + ## ## .. code-block:: nim ## var userInput = "\r\n*~Hello World!" ## userInput.removePrefix @@ -2396,7 +2393,9 @@ proc removePrefix*(s: var string, chars: set[char] = Newlines) {. proc removePrefix*(s: var string, c: char) {. rtl, extern: "nsuRemovePrefixChar".} = - ## Removes a single character (in-place) from the start of a string. + ## Removes all occurrences of a single character (in-place) from the start + ## of a string. + ## ## .. code-block:: nim ## var ident = "pControl" ## ident.removePrefix('p') @@ -2406,6 +2405,7 @@ proc removePrefix*(s: var string, c: char) {. proc removePrefix*(s: var string, prefix: string) {. rtl, extern: "nsuRemovePrefixString".} = ## Remove the first matching prefix (in-place) from a string. + ## ## .. code-block:: nim ## var answers = "yesyes" ## answers.removePrefix("yes") diff --git a/tests/stdlib/tstrutil.nim b/tests/stdlib/tstrutil.nim index 2fb0c371f..071dae5a7 100644 --- a/tests/stdlib/tstrutil.nim +++ b/tests/stdlib/tstrutil.nim @@ -13,15 +13,13 @@ proc testStrip() = proc testRemoveSuffix = var s = "hello\n\r" s.removeSuffix - assert s == "hello\n" - s.removeSuffix assert s == "hello" s.removeSuffix assert s == "hello" s = "hello\n\n" s.removeSuffix - assert s == "hello\n" + assert s == "hello" s = "hello\r" s.removeSuffix @@ -41,7 +39,31 @@ proc testRemoveSuffix = s.removeSuffix({'s','z'}) assert s == "hello" s.removeSuffix({'l','o'}) - assert s == "hell" + assert s == "he" + + s = "aeiou" + s.removeSuffix("") + assert s == "aeiou" + + s = "" + s.removeSuffix("") + assert s == "" + + s = " " + s.removeSuffix + assert s == " " + + s = " " + s.removeSuffix("") + assert s == " " + + s = " " + s.removeSuffix(" ") + assert s == " " + + s = " " + s.removeSuffix(' ') + assert s == "" # Contrary to Chomp in other languages # empty string does not change behaviour -- cgit 1.4.1-2-gfad0 From 3174cfe55c6da6c83dfe83667744ccd8acb7a1ce Mon Sep 17 00:00:00 2001 From: Araq Date: Wed, 1 Nov 2017 00:20:40 +0100 Subject: make tests green again --- lib/pure/strutils.nim | 3 ++- tests/misc/tvarnums.nim | 2 +- tests/stdlib/nre/captures.nim | 6 +++--- tests/stdlib/nre/find.nim | 2 +- tests/system/toString.nim | 4 ++-- tests/vm/trgba.nim | 10 +++++----- 6 files changed, 14 insertions(+), 13 deletions(-) (limited to 'tests/stdlib') diff --git a/lib/pure/strutils.nim b/lib/pure/strutils.nim index 18b0954f3..2b87e0d43 100644 --- a/lib/pure/strutils.nim +++ b/lib/pure/strutils.nim @@ -2434,7 +2434,8 @@ when isMainModule: doAssert formatBiggestFloat(0.00000000001, ffScientific, 1, ',') in ["1,0e-11", "1,0e-011"] # bug #6589 - doAssert formatFloat(123.456, ffScientific, precision=0) == "1.234560e+02" + doAssert formatFloat(123.456, ffScientific, precision=0) in + ["1.234560e+02", "1.234560e+002"] doAssert "$# $3 $# $#" % ["a", "b", "c"] == "a c b c" doAssert "${1}12 ${-1}$2" % ["a", "b"] == "a12 bb" diff --git a/tests/misc/tvarnums.nim b/tests/misc/tvarnums.nim index a5c30c7eb..5daa2c4b8 100644 --- a/tests/misc/tvarnums.nim +++ b/tests/misc/tvarnums.nim @@ -67,7 +67,7 @@ proc toNum64(b: TBuffer): int64 = proc toNum(b: TBuffer): int32 = # treat first byte different: - result = ze(b[0]) and 63 + result = int32 ze(b[0]) and 63 var i = 0 Shift = 6'i32 diff --git a/tests/stdlib/nre/captures.nim b/tests/stdlib/nre/captures.nim index fa01a2000..19c344a8d 100644 --- a/tests/stdlib/nre/captures.nim +++ b/tests/stdlib/nre/captures.nim @@ -32,7 +32,7 @@ suite "captures": test "named capture bounds": let ex1 = "foo".find(re("(?foo)(?bar)?")) check(ex1.captureBounds["foo"] == some(0..2)) - check(ex1.captureBounds["bar"] == none(Slice[int, int])) + check(ex1.captureBounds["bar"] == none(Slice[int])) test "capture count": let ex1 = re("(?foo)(?bar)?") @@ -42,7 +42,7 @@ suite "captures": test "named capture table": let ex1 = "foo".find(re("(?foo)(?bar)?")) check(ex1.captures.toTable == {"foo" : "foo", "bar" : nil}.toTable()) - check(ex1.captureBounds.toTable == {"foo" : some(0..2), "bar" : none(Slice[int, int])}.toTable()) + check(ex1.captureBounds.toTable == {"foo" : some(0..2), "bar" : none(Slice[int])}.toTable()) check(ex1.captures.toTable("") == {"foo" : "foo", "bar" : ""}.toTable()) let ex2 = "foobar".find(re("(?foo)(?bar)?")) @@ -51,7 +51,7 @@ suite "captures": test "capture sequence": let ex1 = "foo".find(re("(?foo)(?bar)?")) check(ex1.captures.toSeq == @["foo", nil]) - check(ex1.captureBounds.toSeq == @[some(0..2), none(Slice[int, int])]) + check(ex1.captureBounds.toSeq == @[some(0..2), none(Slice[int])]) check(ex1.captures.toSeq("") == @["foo", ""]) let ex2 = "foobar".find(re("(?foo)(?bar)?")) diff --git a/tests/stdlib/nre/find.nim b/tests/stdlib/nre/find.nim index c37ac56ba..caa953ff4 100644 --- a/tests/stdlib/nre/find.nim +++ b/tests/stdlib/nre/find.nim @@ -12,7 +12,7 @@ suite "find": test "find bounds": check(toSeq(findIter("1 2 3 4 5 ", re" ")).map( - proc (a: RegexMatch): Slice[int, int] = a.matchBounds + proc (a: RegexMatch): Slice[int] = a.matchBounds ) == @[1..1, 3..3, 5..5, 7..7, 9..9]) test "overlapping find": diff --git a/tests/system/toString.nim b/tests/system/toString.nim index 1279897a7..3e7fc7ddb 100644 --- a/tests/system/toString.nim +++ b/tests/system/toString.nim @@ -3,9 +3,9 @@ discard """ """ doAssert "@[23, 45]" == $(@[23, 45]) -doAssert "[32, 45]" == $([32, 45]) +doAssert "[32, 45]" == $([32, 45]) doAssert "@[, foo, bar]" == $(@["", "foo", "bar"]) -doAssert "[, foo, bar]" == $(["", "foo", "bar"]) +doAssert "[, foo, bar]" == $(["", "foo", "bar"]) # bug #2395 let alphaSet: set[char] = {'a'..'c'} diff --git a/tests/vm/trgba.nim b/tests/vm/trgba.nim index da1a2d0c5..923ea1b2e 100644 --- a/tests/vm/trgba.nim +++ b/tests/vm/trgba.nim @@ -22,14 +22,14 @@ template `B=`*(self: TAggRgba8, val: byte) = template `A=`*(self: TAggRgba8, val: byte) = self[3] = val -proc ABGR* (val: int| int64): TAggRgba8 = +proc ABGR*(val: int| int64): TAggRgba8 = var V = val - result.R = V and 0xFF + result.R = byte(V and 0xFF) V = V shr 8 - result.G = V and 0xFF + result.G = byte(V and 0xFF) V = V shr 8 - result.B = V and 0xFF - result.A = (V shr 8) and 0xFF + result.B = byte(V and 0xFF) + result.A = byte((V shr 8) and 0xFF) const c1 = ABGR(0xFF007F7F) -- cgit 1.4.1-2-gfad0 From 4ea09e4df5c47cbca064d4f3b2388237f09c605f Mon Sep 17 00:00:00 2001 From: Andreas Rumpf Date: Sun, 5 Nov 2017 21:44:22 +0100 Subject: attempt to make travis green again --- tests/async/tasyncdial.nim | 1 + tests/stdlib/thttpclient.nim | 1 + tests/stdlib/tnetdial.nim | 1 + tests/testament/specs.nim | 4 ++++ 4 files changed, 7 insertions(+) (limited to 'tests/stdlib') diff --git a/tests/async/tasyncdial.nim b/tests/async/tasyncdial.nim index d70e14020..fa81235fe 100644 --- a/tests/async/tasyncdial.nim +++ b/tests/async/tasyncdial.nim @@ -4,6 +4,7 @@ discard """ OK AF_INET OK AF_INET6 ''' + disabled: "travis" """ import diff --git a/tests/stdlib/thttpclient.nim b/tests/stdlib/thttpclient.nim index e759e2977..54588d3f0 100644 --- a/tests/stdlib/thttpclient.nim +++ b/tests/stdlib/thttpclient.nim @@ -2,6 +2,7 @@ discard """ cmd: "nim c --threads:on -d:ssl $file" exitcode: 0 output: "OK" + disabled: "travis" """ import strutils diff --git a/tests/stdlib/tnetdial.nim b/tests/stdlib/tnetdial.nim index da6088d70..695150179 100644 --- a/tests/stdlib/tnetdial.nim +++ b/tests/stdlib/tnetdial.nim @@ -2,6 +2,7 @@ discard """ cmd: "nim c --threads:on $file" exitcode: 0 output: "OK" + disabled: "travis" """ import os, net, nativesockets, asyncdispatch diff --git a/tests/testament/specs.nim b/tests/testament/specs.nim index 89e786d48..e5506e796 100644 --- a/tests/testament/specs.nim +++ b/tests/testament/specs.nim @@ -12,6 +12,8 @@ import parseutils, strutils, os, osproc, streams, parsecfg var compilerPrefix* = "compiler" / "nim " +let isTravis = existsEnv("TRAVIS") + proc cmdTemplate*(): string = compilerPrefix & "$target --lib:lib --hints:on -d:testing $options $file" @@ -174,6 +176,8 @@ proc parseSpec*(filename: string): TSpec = when defined(unix): result.err = reIgnored of "posix": when defined(posix): result.err = reIgnored + of "travis": + if isTravis: result.err = reIgnored else: raise newException(ValueError, "cannot interpret as a bool: " & e.value) of "cmd": -- cgit 1.4.1-2-gfad0 From 3d5d6931f01550cd384805fd5c7474eaabc2c962 Mon Sep 17 00:00:00 2001 From: Fredrik Høisæther Rasch Date: Wed, 15 Nov 2017 17:25:48 +0100 Subject: Appveyor thttpclient (#6744) * App option value for disabling tests for AppVeyor * Disable thttpclient on AppVeyor --- tests/stdlib/thttpclient.nim | 1 + tests/testament/specs.nim | 3 +++ 2 files changed, 4 insertions(+) (limited to 'tests/stdlib') diff --git a/tests/stdlib/thttpclient.nim b/tests/stdlib/thttpclient.nim index 54588d3f0..fff02722a 100644 --- a/tests/stdlib/thttpclient.nim +++ b/tests/stdlib/thttpclient.nim @@ -3,6 +3,7 @@ discard """ exitcode: 0 output: "OK" disabled: "travis" + disabled: "appveyor" """ import strutils diff --git a/tests/testament/specs.nim b/tests/testament/specs.nim index e5506e796..dcf5f2831 100644 --- a/tests/testament/specs.nim +++ b/tests/testament/specs.nim @@ -13,6 +13,7 @@ import parseutils, strutils, os, osproc, streams, parsecfg var compilerPrefix* = "compiler" / "nim " let isTravis = existsEnv("TRAVIS") +let isAppVeyor = existsEnv("APPVEYOR") proc cmdTemplate*(): string = compilerPrefix & "$target --lib:lib --hints:on -d:testing $options $file" @@ -178,6 +179,8 @@ proc parseSpec*(filename: string): TSpec = when defined(posix): result.err = reIgnored of "travis": if isTravis: result.err = reIgnored + of "appveyor": + if isAppVeyor: result.err = reIgnored else: raise newException(ValueError, "cannot interpret as a bool: " & e.value) of "cmd": -- cgit 1.4.1-2-gfad0 From 58187f212067beff458e76d7c819750a9cfa100c Mon Sep 17 00:00:00 2001 From: Andreas Rumpf Date: Tue, 21 Nov 2017 10:48:19 +0100 Subject: added a warning that the .deprecate statement is unreliable for routines --- compiler/pragmas.nim | 3 ++- lib/pure/cgi.nim | 5 +---- lib/pure/collections/sequtils.nim | 2 -- lib/pure/includes/oserr.nim | 3 --- lib/pure/scgi.nim | 2 +- lib/system.nim | 3 --- tests/stdlib/tgetfileinfo.nim | 2 +- 7 files changed, 5 insertions(+), 15 deletions(-) (limited to 'tests/stdlib') diff --git a/compiler/pragmas.nim b/compiler/pragmas.nim index 5acfbc919..f1d81f798 100644 --- a/compiler/pragmas.nim +++ b/compiler/pragmas.nim @@ -626,7 +626,8 @@ proc deprecatedStmt(c: PContext; pragma: PNode) = for n in pragma: if n.kind in {nkExprColonExpr, nkExprEqExpr}: let dest = qualifiedLookUp(c, n[1], {checkUndeclared}) - assert dest != nil + if dest == nil or dest.kind in routineKinds: + localError(n.info, warnUser, "the .deprecated pragma is unreliable for routines") let src = considerQuotedIdent(n[0]) let alias = newSym(skAlias, src, dest, n[0].info) incl(alias.flags, sfExported) diff --git a/lib/pure/cgi.nim b/lib/pure/cgi.nim index 200a4adf1..fcf2cf99f 100644 --- a/lib/pure/cgi.nim +++ b/lib/pure/cgi.nim @@ -74,8 +74,6 @@ proc decodeUrl*(s: string): string = inc(j) setLen(result, j) -{.deprecated: [URLDecode: decodeUrl, URLEncode: encodeUrl].} - proc addXmlChar(dest: var string, c: char) {.inline.} = case c of '&': add(dest, "&") @@ -101,8 +99,7 @@ type methodPost, ## query uses the POST method methodGet ## query uses the GET method -{.deprecated: [TRequestMethod: RequestMethod, ECgi: CgiError, - XMLencode: xmlEncode].} +{.deprecated: [TRequestMethod: RequestMethod, ECgi: CgiError].} proc cgiError*(msg: string) {.noreturn.} = ## raises an ECgi exception with message `msg`. diff --git a/lib/pure/collections/sequtils.nim b/lib/pure/collections/sequtils.nim index 3f8a9574d..d0f5c78e0 100644 --- a/lib/pure/collections/sequtils.nim +++ b/lib/pure/collections/sequtils.nim @@ -110,8 +110,6 @@ proc deduplicate*[T](s: openArray[T]): seq[T] = for itm in items(s): if not result.contains(itm): result.add(itm) -{.deprecated: [distnct: deduplicate].} - proc zip*[S, T](s1: openArray[S], s2: openArray[T]): seq[tuple[a: S, b: T]] = ## Returns a new sequence with a combination of the two input containers. ## diff --git a/lib/pure/includes/oserr.nim b/lib/pure/includes/oserr.nim index dbb709f1b..0889d7383 100644 --- a/lib/pure/includes/oserr.nim +++ b/lib/pure/includes/oserr.nim @@ -56,9 +56,6 @@ proc raiseOSError*(msg: string = "") {.noinline, rtl, extern: "nos$1", raise newException(OSError, msg) {.pop.} -when not defined(nimfix): - {.deprecated: [osError: raiseOSError].} - proc `==`*(err1, err2: OSErrorCode): bool {.borrow.} proc `$`*(err: OSErrorCode): string {.borrow.} diff --git a/lib/pure/scgi.nim b/lib/pure/scgi.nim index 711e4a897..1ff26954e 100644 --- a/lib/pure/scgi.nim +++ b/lib/pure/scgi.nim @@ -95,7 +95,7 @@ type AsyncScgiState* = ref AsyncScgiStateObj {.deprecated: [EScgi: ScgiError, TScgiState: ScgiState, - PAsyncScgiState: AsyncScgiState, scgiError: raiseScgiError].} + PAsyncScgiState: AsyncScgiState].} proc recvBuffer(s: var ScgiState, L: int) = if L > s.bufLen: diff --git a/lib/system.nim b/lib/system.nim index c1bf1a919..1b53bf9f5 100644 --- a/lib/system.nim +++ b/lib/system.nim @@ -3118,9 +3118,6 @@ when not defined(JS): #and not defined(nimscript): ## returns the OS file handle of the file ``f``. This is only useful for ## platform specific programming. - when not defined(nimfix): - {.deprecated: [fileHandle: getFileHandle].} - when declared(newSeq): proc cstringArrayToSeq*(a: cstringArray, len: Natural): seq[string] = ## converts a ``cstringArray`` to a ``seq[string]``. `a` is supposed to be diff --git a/tests/stdlib/tgetfileinfo.nim b/tests/stdlib/tgetfileinfo.nim index 1c897b702..019c2eb7f 100644 --- a/tests/stdlib/tgetfileinfo.nim +++ b/tests/stdlib/tgetfileinfo.nim @@ -61,7 +61,7 @@ proc testGetFileInfo = block: let testFile = open(getAppFilename()) - testHandle = fileHandle(testFile) + testHandle = getFileHandle(testFile) try: discard getFileInfo(testFile) #echo("Handle : Valid File : Success") -- cgit 1.4.1-2-gfad0 From 25831a83d799a0400fbd086ea9a6f704d4d6b216 Mon Sep 17 00:00:00 2001 From: Federico Ceratto Date: Sat, 11 Nov 2017 16:59:42 +0000 Subject: Add unittest suite/test name filters Support simple globbing --- lib/pure/unittest.nim | 91 ++++++++++++++++++++++++++++++++++++++++------ tests/stdlib/tunittest.nim | 38 +++++++++++++++++++ 2 files changed, 118 insertions(+), 11 deletions(-) (limited to 'tests/stdlib') diff --git a/lib/pure/unittest.nim b/lib/pure/unittest.nim index 7a8d1dad0..fbce087ff 100644 --- a/lib/pure/unittest.nim +++ b/lib/pure/unittest.nim @@ -21,13 +21,41 @@ ## ``nim c -r `` exits with 0 or 1 ## ## Running a single test -## --------------------- +## ===================== ## -## Simply specify the test name as a command line argument. +## Specify the test name as a command line argument. ## ## .. code:: ## -## nim c -r test "my super awesome test name" +## nim c -r test "my test name" "another test" +## +## Multiple arguments can be used. +## +## Running a single test suite +## =========================== +## +## Specify the suite name delimited by ``"::"``. +## +## .. code:: +## +## nim c -r test "my test name::" +## +## Selecting tests by pattern +## ========================== +## +## A single ``"*"`` can be used for globbing. +## +## Delimit the end of a suite name with ``"::"``. +## +## Tests matching **any** of the arguments are executed. +## +## .. code:: +## +## nim c -r test fast_suite::mytest1 fast_suite::mytest2 +## nim c -r test "fast_suite::mytest*" +## nim c -r test "auth*::" "crypto::hashing*" +## # Run suites starting with 'bug #' and standalone tests starting with '#' +## nim c -r test 'bug #*::' '::#*' ## ## Example ## ------- @@ -121,7 +149,7 @@ var checkpoints {.threadvar.}: seq[string] formatters {.threadvar.}: seq[OutputFormatter] - testsToRun {.threadvar.}: HashSet[string] + testsFilters {.threadvar.}: HashSet[string] when declared(stdout): abortOnError = existsEnv("NIMTEST_ABORT_ON_ERROR") @@ -300,22 +328,63 @@ method testEnded*(formatter: JUnitOutputFormatter, testResult: TestResult) = method suiteEnded*(formatter: JUnitOutputFormatter) = formatter.stream.writeLine("\t") -proc shouldRun(testName: string): bool = - if testsToRun.len == 0: +proc glob(matcher, filter: string): bool = + ## Globbing using a single `*`. Empty `filter` matches everything. + if filter.len == 0: return true - result = testName in testsToRun + if not filter.contains('*'): + return matcher == filter + + let beforeAndAfter = filter.split('*', maxsplit=1) + if beforeAndAfter.len == 1: + # "foo*" + return matcher.startswith(beforeAndAfter[0]) + + if matcher.len < filter.len - 1: + return false # "12345" should not match "123*345" + + return matcher.startsWith(beforeAndAfter[0]) and matcher.endsWith(beforeAndAfter[1]) + +proc matchFilter(suiteName, testName, filter: string): bool = + if filter == "": + return true + if testName == filter: + # corner case for tests containing "::" in their name + return true + let suiteAndTestFilters = filter.split("::", maxsplit=1) + + if suiteAndTestFilters.len == 1: + # no suite specified + let test_f = suiteAndTestFilters[0] + return glob(testName, test_f) + + return glob(suiteName, suiteAndTestFilters[0]) and glob(testName, suiteAndTestFilters[1]) + +when defined(testing): export matchFilter + +proc shouldRun(currentSuiteName, testName: string): bool = + ## Check if a test should be run by matching suiteName and testName against + ## test filters. + if testsFilters.len == 0: + return true + + for f in testsFilters: + if matchFilter(currentSuiteName, testName, f): + return true + + return false proc ensureInitialized() = if formatters == nil: formatters = @[OutputFormatter(defaultConsoleFormatter())] - if not testsToRun.isValid: - testsToRun.init() + if not testsFilters.isValid: + testsFilters.init() when declared(paramCount): # Read tests to run from the command line. for i in 1 .. paramCount(): - testsToRun.incl(paramStr(i)) + testsFilters.incl(paramStr(i)) # These two procs are added as workarounds for # https://github.com/nim-lang/Nim/issues/5549 @@ -395,7 +464,7 @@ template test*(name, body) {.dirty.} = ensureInitialized() - if shouldRun(name): + if shouldRun(when declared(testSuiteName): testSuiteName else: "", name): checkpoints = @[] var testStatusIMPL {.inject.} = OK diff --git a/tests/stdlib/tunittest.nim b/tests/stdlib/tunittest.nim index e4a801871..86b9fd037 100644 --- a/tests/stdlib/tunittest.nim +++ b/tests/stdlib/tunittest.nim @@ -13,6 +13,8 @@ discard """ [Suite] bug #5784 +[Suite] test name filtering + ''' """ @@ -120,3 +122,39 @@ suite "bug #5784": field: int var obj: Obj check obj.isNil or obj.field == 0 + +when defined(testing): + suite "test name filtering": + test "test name": + check matchFilter("suite1", "foo", "") + check matchFilter("suite1", "foo", "foo") + check matchFilter("suite1", "foo", "::") + check matchFilter("suite1", "foo", "*") + check matchFilter("suite1", "foo", "::foo") + check matchFilter("suite1", "::foo", "::foo") + + test "test name - glob": + check matchFilter("suite1", "foo", "f*") + check matchFilter("suite1", "foo", "*oo") + check matchFilter("suite1", "12345", "12*345") + check matchFilter("suite1", "q*wefoo", "q*wefoo") + check false == matchFilter("suite1", "foo", "::x") + check false == matchFilter("suite1", "foo", "::x*") + check false == matchFilter("suite1", "foo", "::*x") + # overlap + check false == matchFilter("suite1", "12345", "123*345") + check matchFilter("suite1", "ab*c::d*e::f", "ab*c::d*e::f") + + test "suite name": + check matchFilter("suite1", "foo", "suite1::") + check false == matchFilter("suite1", "foo", "suite2::") + check matchFilter("suite1", "qwe::foo", "qwe::foo") + check matchFilter("suite1", "qwe::foo", "suite1::qwe::foo") + + test "suite name - glob": + check matchFilter("suite1", "foo", "::*") + check matchFilter("suite1", "foo", "*::*") + check matchFilter("suite1", "foo", "*::foo") + check false == matchFilter("suite1", "foo", "*ite2::") + check matchFilter("suite1", "q**we::foo", "q**we::foo") + check matchFilter("suite1", "a::b*c::d*e", "a::b*c::d*e") -- cgit 1.4.1-2-gfad0 From b74a5148a9e7bf646ee6a13cad0ce046d7b9d8b4 Mon Sep 17 00:00:00 2001 From: Dominik Picheta Date: Sun, 26 Nov 2017 19:31:59 +0000 Subject: Fixes #6223. --- changelog.md | 3 +++ lib/pure/unicode.nim | 12 ++++++------ lib/system.nim | 5 ++++- tests/stdlib/tstring.nim | 4 ++++ 4 files changed, 17 insertions(+), 7 deletions(-) (limited to 'tests/stdlib') diff --git a/changelog.md b/changelog.md index aaee99cfb..bd40d7e99 100644 --- a/changelog.md +++ b/changelog.md @@ -105,3 +105,6 @@ This now needs to be written as: :test: # shows how the 'if' statement works if true: echo "yes" +- The ``[]`` proc for strings now raises an ``IndexError`` exception when + the specified slice is out of bounds. See issue + [#6223](https://github.com/nim-lang/Nim/issues/6223) for more details. \ No newline at end of file diff --git a/lib/pure/unicode.nim b/lib/pure/unicode.nim index 7d9c3108b..257c620f7 100644 --- a/lib/pure/unicode.nim +++ b/lib/pure/unicode.nim @@ -293,33 +293,33 @@ proc runeSubStr*(s: string, pos:int, len:int = int.high): string = if pos < 0: let (o, rl) = runeReverseOffset(s, -pos) if len >= rl: - result = s[o.. s.len-1] + result = s.substr(o, s.len-1) elif len < 0: let e = rl + len if e < 0: result = "" else: - result = s[o.. runeOffset(s, e-(rl+pos) , o)-1] + result = s.substr(o, runeOffset(s, e-(rl+pos) , o)-1) else: - result = s[o.. runeOffset(s, len, o)-1] + result = s.substr(o, runeOffset(s, len, o)-1) else: let o = runeOffset(s, pos) if o < 0: result = "" elif len == int.high: - result = s[o.. s.len-1] + result = s.substr(o, s.len-1) elif len < 0: let (e, rl) = runeReverseOffset(s, -len) discard rl if e <= 0: result = "" else: - result = s[o.. e-1] + result = s.substr(o, e-1) else: var e = runeOffset(s, len, o) if e < 0: e = s.len - result = s[o.. e-1] + result = s.substr(o, e-1) const alphaRanges = [ diff --git a/lib/system.nim b/lib/system.nim index 387973f4b..b9f01c306 100644 --- a/lib/system.nim +++ b/lib/system.nim @@ -3529,7 +3529,10 @@ when hasAlloc or defined(nimscript): ## .. code-block:: nim ## var s = "abcdef" ## assert s[1..3] == "bcd" - result = s.substr(s ^^ x.a, s ^^ x.b) + let a = s ^^ x.a + let L = (s ^^ x.b) - a + 1 + result = newString(L) + for i in 0 ..< L: result[i] = s[i + a] proc `[]=`*[T, U](s: var string, x: HSlice[T, U], b: string) = ## slice assignment for strings. If diff --git a/tests/stdlib/tstring.nim b/tests/stdlib/tstring.nim index ddf533a17..904bc462a 100644 --- a/tests/stdlib/tstring.nim +++ b/tests/stdlib/tstring.nim @@ -50,6 +50,10 @@ proc test_string_slice() = s[2..0] = numbers doAssert s == "ab1234567890cdefghijklmnopqrstuvwxyz" + # bug #6223 + doAssertRaises(IndexError): + discard s[0..999] + echo("OK") test_string_slice() -- cgit 1.4.1-2-gfad0 From 11fcae57052b3c886b4d2b593acb3ac0d717edd1 Mon Sep 17 00:00:00 2001 From: Dominik Picheta Date: Tue, 28 Nov 2017 21:49:34 +0000 Subject: Fixes #5856. Code based on @loloiccl's PR (#5879). --- lib/pure/json.nim | 28 +++++++++++++++++++++++----- tests/stdlib/tjsonmacro.nim | 15 ++++++++++++++- 2 files changed, 37 insertions(+), 6 deletions(-) (limited to 'tests/stdlib') diff --git a/lib/pure/json.nim b/lib/pure/json.nim index cea485c43..1b887a0c5 100644 --- a/lib/pure/json.nim +++ b/lib/pure/json.nim @@ -1524,6 +1524,27 @@ proc processObjField(field, jsonNode: NimNode): seq[NimNode] = doAssert result.len > 0 +proc processObjFields(obj: NimNode, + jsonNode: NimNode): seq[NimNode] {.compileTime.} = + ## Process all the fields of an ``ObjectTy`` and any of its + ## parent type's fields (via inheritance). + result = @[] + + expectKind(obj[2], nnkRecList) + for field in obj[2]: + let nodes = processObjField(field, jsonNode) + result.add(nodes) + + # process parent type fields + case obj[1].kind + of nnkBracketExpr: + assert $obj[1][0] == "ref" + result.add(processObjFields(getType(obj[1][1]), jsonNode)) + of nnkSym: + result.add(processObjFields(getType(obj[1]), jsonNode)) + else: + discard + proc processType(typeName: NimNode, obj: NimNode, jsonNode: NimNode, isRef: bool): NimNode {.compileTime.} = ## Process a type such as ``Sym "float"`` or ``ObjectTy ...``. @@ -1533,7 +1554,7 @@ proc processType(typeName: NimNode, obj: NimNode, ## .. code-block::plain ## ObjectTy ## Empty - ## Empty + ## InheritanceInformation ## RecList ## Sym "events" case obj.kind @@ -1543,10 +1564,7 @@ proc processType(typeName: NimNode, obj: NimNode, result.add(typeName) # Name of the type to construct. # Process each object field and add it as an exprColonExpr - expectKind(obj[2], nnkRecList) - for field in obj[2]: - let nodes = processObjField(field, jsonNode) - result.add(nodes) + result.add(processObjFields(obj, jsonNode)) # Object might be null. So we need to check for that. if isRef: diff --git a/tests/stdlib/tjsonmacro.nim b/tests/stdlib/tjsonmacro.nim index 153cf8556..2d20063ab 100644 --- a/tests/stdlib/tjsonmacro.nim +++ b/tests/stdlib/tjsonmacro.nim @@ -246,4 +246,17 @@ when isMainModule: var b = Bird(age: 3, height: 1.734, name: "bardo", colors: [red, blue]) let jnode = %b let data = jnode.to(Bird) - doAssert data == b \ No newline at end of file + doAssert data == b + + block: + type + MsgBase = ref object of RootObj + name*: string + + MsgChallenge = ref object of MsgBase + challenge*: string + + let data = %*{"name": "foo", "challenge": "bar"} + let msg = data.to(MsgChallenge) + doAssert msg.name == "foo" + doAssert msg.challenge == "bar" \ No newline at end of file -- cgit 1.4.1-2-gfad0 From e0681715dc9c54135937f54510015f66d384aa29 Mon Sep 17 00:00:00 2001 From: Dominik Picheta Date: Wed, 29 Nov 2017 14:48:17 +0000 Subject: Fixes #6095. --- lib/pure/json.nim | 57 ++++++++++++++++++++++++++++----------------- tests/stdlib/tjsonmacro.nim | 18 +++++++++++++- 2 files changed, 53 insertions(+), 22 deletions(-) (limited to 'tests/stdlib') diff --git a/lib/pure/json.nim b/lib/pure/json.nim index 1b887a0c5..b9f49f0bd 100644 --- a/lib/pure/json.nim +++ b/lib/pure/json.nim @@ -1524,26 +1524,34 @@ proc processObjField(field, jsonNode: NimNode): seq[NimNode] = doAssert result.len > 0 -proc processObjFields(obj: NimNode, - jsonNode: NimNode): seq[NimNode] {.compileTime.} = +proc processFields(obj: NimNode, + jsonNode: NimNode): seq[NimNode] {.compileTime.} = ## Process all the fields of an ``ObjectTy`` and any of its ## parent type's fields (via inheritance). result = @[] - - expectKind(obj[2], nnkRecList) - for field in obj[2]: - let nodes = processObjField(field, jsonNode) - result.add(nodes) - - # process parent type fields - case obj[1].kind - of nnkBracketExpr: - assert $obj[1][0] == "ref" - result.add(processObjFields(getType(obj[1][1]), jsonNode)) - of nnkSym: - result.add(processObjFields(getType(obj[1]), jsonNode)) + case obj.kind + of nnkObjectTy: + expectKind(obj[2], nnkRecList) + for field in obj[2]: + let nodes = processObjField(field, jsonNode) + result.add(nodes) + + # process parent type fields + case obj[1].kind + of nnkBracketExpr: + assert $obj[1][0] == "ref" + result.add(processFields(getType(obj[1][1]), jsonNode)) + of nnkSym: + result.add(processFields(getType(obj[1]), jsonNode)) + else: + discard + of nnkTupleTy: + for identDefs in obj: + expectKind(identDefs, nnkIdentDefs) + let nodes = processObjField(identDefs[0], jsonNode) + result.add(nodes) else: - discard + doAssert false, "Unable to process field type: " & $obj.kind proc processType(typeName: NimNode, obj: NimNode, jsonNode: NimNode, isRef: bool): NimNode {.compileTime.} = @@ -1558,13 +1566,17 @@ proc processType(typeName: NimNode, obj: NimNode, ## RecList ## Sym "events" case obj.kind - of nnkObjectTy: + of nnkObjectTy, nnkTupleTy: # Create object constructor. - result = newNimNode(nnkObjConstr) - result.add(typeName) # Name of the type to construct. + result = + if obj.kind == nnkObjectTy: newNimNode(nnkObjConstr) + else: newNimNode(nnkPar) + + if obj.kind == nnkObjectTy: + result.add(typeName) # Name of the type to construct. - # Process each object field and add it as an exprColonExpr - result.add(processObjFields(obj, jsonNode)) + # Process each object/tuple field and add it as an exprColonExpr + result.add(processFields(obj, jsonNode)) # Object might be null. So we need to check for that. if isRef: @@ -1687,6 +1699,8 @@ proc createConstructor(typeSym, jsonNode: NimNode): NimNode = result = createConstructor(obj, jsonNode) else: result = processType(typeSym, obj, jsonNode, false) + of nnkTupleTy: + result = processType(typeSym, typeSym, jsonNode, false) else: doAssert false, "Unable to create constructor for: " & $typeSym.kind @@ -1818,6 +1832,7 @@ macro to*(node: JsonNode, T: typedesc): untyped = # TODO: Rename postProcessValue and move it (?) result = postProcessValue(result) + # echo(treeRepr(result)) # echo(toStrLit(result)) when false: diff --git a/tests/stdlib/tjsonmacro.nim b/tests/stdlib/tjsonmacro.nim index 2d20063ab..388d4d534 100644 --- a/tests/stdlib/tjsonmacro.nim +++ b/tests/stdlib/tjsonmacro.nim @@ -259,4 +259,20 @@ when isMainModule: let data = %*{"name": "foo", "challenge": "bar"} let msg = data.to(MsgChallenge) doAssert msg.name == "foo" - doAssert msg.challenge == "bar" \ No newline at end of file + doAssert msg.challenge == "bar" + + block: + type + Color = enum Red, Brown + Thing = object + animal: tuple[fur: bool, legs: int] + color: Color + + var j = parseJson(""" + {"animal":{"fur":true,"legs":6},"color":"Red"} + """) + + let parsed = to(j, Thing) + doAssert parsed.animal.fur + doAssert parsed.animal.legs == 6 + doAssert parsed.color == Red \ No newline at end of file -- cgit 1.4.1-2-gfad0 From d3c9b58c005e7cd537cbdf3dfd3f69e72fa40722 Mon Sep 17 00:00:00 2001 From: Dominik Picheta Date: Wed, 29 Nov 2017 15:56:46 +0000 Subject: Fixes #6604. Rejects unnamed tuples with error. --- lib/pure/json.nim | 15 +++++++++++++-- tests/stdlib/tjsonmacro.nim | 22 +++++++++++++++++++++- tests/stdlib/tjsonmacro_reject.nim | 18 ++++++++++++++++++ 3 files changed, 52 insertions(+), 3 deletions(-) create mode 100644 tests/stdlib/tjsonmacro_reject.nim (limited to 'tests/stdlib') diff --git a/lib/pure/json.nim b/lib/pure/json.nim index b9f49f0bd..90cf7b8c9 100644 --- a/lib/pure/json.nim +++ b/lib/pure/json.nim @@ -1701,6 +1701,10 @@ proc createConstructor(typeSym, jsonNode: NimNode): NimNode = result = processType(typeSym, obj, jsonNode, false) of nnkTupleTy: result = processType(typeSym, typeSym, jsonNode, false) + of nnkPar: + # TODO: The fact that `jsonNode` here works to give a good line number + # is weird. Specifying typeSym should work but doesn't. + error("Use a named tuple instead of: " & $toStrLit(typeSym), jsonNode) else: doAssert false, "Unable to create constructor for: " & $typeSym.kind @@ -1828,9 +1832,16 @@ macro to*(node: JsonNode, T: typedesc): untyped = expectKind(typeNode, nnkBracketExpr) doAssert(($typeNode[0]).normalize == "typedesc") - result = createConstructor(typeNode[1], node) + # Create `temp` variable to store the result in case the user calls this + # on `parseJson` (see bug #6604). + result = newNimNode(nnkStmtListExpr) + let temp = genSym(nskLet, "temp") + result.add quote do: + let `temp` = `node` + + let constructor = createConstructor(typeNode[1], temp) # TODO: Rename postProcessValue and move it (?) - result = postProcessValue(result) + result.add(postProcessValue(constructor)) # echo(treeRepr(result)) # echo(toStrLit(result)) diff --git a/tests/stdlib/tjsonmacro.nim b/tests/stdlib/tjsonmacro.nim index 388d4d534..af9633e9e 100644 --- a/tests/stdlib/tjsonmacro.nim +++ b/tests/stdlib/tjsonmacro.nim @@ -275,4 +275,24 @@ when isMainModule: let parsed = to(j, Thing) doAssert parsed.animal.fur doAssert parsed.animal.legs == 6 - doAssert parsed.color == Red \ No newline at end of file + doAssert parsed.color == Red + + block: + type + Car = object + engine: tuple[name: string, capacity: float] + model: string + + let j = """ + {"engine": {"name": "V8", "capacity": 5.5}, "model": "Skyline"} + """ + + var i = 0 + proc mulTest: JsonNode = + i.inc() + return parseJson(j) + + let parsed = mulTest().to(Car) + doAssert parsed.engine.name == "V8" + + doAssert i == 1 \ No newline at end of file diff --git a/tests/stdlib/tjsonmacro_reject.nim b/tests/stdlib/tjsonmacro_reject.nim new file mode 100644 index 000000000..00506449f --- /dev/null +++ b/tests/stdlib/tjsonmacro_reject.nim @@ -0,0 +1,18 @@ +discard """ + file: "tjsonmacro_reject.nim" + line: 11 + errormsg: "Use a named tuple instead of: (string, float)" +""" + +import json + +type + Car = object + engine: (string, float) + model: string + +let j = """ + {"engine": {"name": "V8", "capacity": 5.5}, model: "Skyline"} +""" +let parsed = parseJson(j) +echo(to(parsed, Car)) \ No newline at end of file -- cgit 1.4.1-2-gfad0 From 8ca41ce637106c734cd819a1f49606db880cf075 Mon Sep 17 00:00:00 2001 From: Dominik Picheta Date: Wed, 29 Nov 2017 19:15:25 +0000 Subject: Implement support for Option[T] in json.to macro. Fixes #5848. --- lib/pure/json.nim | 27 +++++++++++++++++++++++++++ tests/stdlib/tjsonmacro.nim | 25 +++++++++++++++++++++++-- 2 files changed, 50 insertions(+), 2 deletions(-) (limited to 'tests/stdlib') diff --git a/lib/pure/json.nim b/lib/pure/json.nim index 90cf7b8c9..b057aa7c6 100644 --- a/lib/pure/json.nim +++ b/lib/pure/json.nim @@ -1346,6 +1346,16 @@ proc createJsonIndexer(jsonNode: NimNode, indexNode ) +proc transformJsonIndexer(jsonNode: NimNode): NimNode = + case jsonNode.kind + of nnkBracketExpr: + result = newNimNode(nnkCurlyExpr) + else: + result = jsonNode.copy() + + for child in jsonNode: + result.add(transformJsonIndexer(child)) + template verifyJsonKind(node: JsonNode, kinds: set[JsonNodeKind], ast: string) = if node.kind notin kinds: @@ -1637,6 +1647,10 @@ proc processType(typeName: NimNode, obj: NimNode, doAssert(not result.isNil(), "processType not initialised.") +import options +proc workaroundMacroNone[T](): Option[T] = + none(T) + proc createConstructor(typeSym, jsonNode: NimNode): NimNode = ## Accepts a type description, i.e. "ref Type", "seq[Type]", "Type" etc. ## @@ -1650,6 +1664,19 @@ proc createConstructor(typeSym, jsonNode: NimNode): NimNode = of nnkBracketExpr: var bracketName = ($typeSym[0]).normalize case bracketName + of "option": + # TODO: Would be good to verify that this is Option[T] from + # options module I suppose. + let lenientJsonNode = transformJsonIndexer(jsonNode) + + let optionGeneric = typeSym[1] + let value = createConstructor(typeSym[1], jsonNode) + let workaround = bindSym("workaroundMacroNone") # TODO: Nim Bug: This shouldn't be necessary. + + result = quote do: + ( + if `lenientJsonNode`.isNil: `workaround`[`optionGeneric`]() else: some[`optionGeneric`](`value`) + ) of "ref": # Ref type. var typeName = $typeSym[1] diff --git a/tests/stdlib/tjsonmacro.nim b/tests/stdlib/tjsonmacro.nim index af9633e9e..f9f94606c 100644 --- a/tests/stdlib/tjsonmacro.nim +++ b/tests/stdlib/tjsonmacro.nim @@ -2,7 +2,7 @@ discard """ file: "tjsonmacro.nim" output: "" """ -import json, strutils +import json, strutils, options when isMainModule: # Tests inspired by own use case (with some additional tests). @@ -295,4 +295,25 @@ when isMainModule: let parsed = mulTest().to(Car) doAssert parsed.engine.name == "V8" - doAssert i == 1 \ No newline at end of file + doAssert i == 1 + + block: + # Option[T] support! + type + Car1 = object # TODO: Codegen bug when `Car` + engine: tuple[name: string, capacity: Option[float]] + model: string + year: Option[int] + + let noYear = """ + {"engine": {"name": "V8", "capacity": 5.5}, "model": "Skyline"} + """ + + let noYearParsed = parseJson(noYear) + let noYearDeser = to(noYearParsed, Car1) + doAssert noYearDeser.engine.capacity == some(5.5) + doAssert noYearDeser.year.isNone + doAssert noYearDeser.engine.name == "V8" + + # TODO: Table[T, Y] support. + # TODO: JsonNode support \ No newline at end of file -- cgit 1.4.1-2-gfad0 From 8187e83645bbc9d536eebfab2af3b2437c3485fb Mon Sep 17 00:00:00 2001 From: Dominik Picheta Date: Wed, 29 Nov 2017 20:30:40 +0000 Subject: Implement Table/OrderedTable support for json.to macro. --- lib/pure/json.nim | 24 ++++++++++++++++++++++++ tests/stdlib/tjsonmacro.nim | 27 +++++++++++++++++++++++++-- 2 files changed, 49 insertions(+), 2 deletions(-) (limited to 'tests/stdlib') diff --git a/lib/pure/json.nim b/lib/pure/json.nim index b057aa7c6..6153e2f03 100644 --- a/lib/pure/json.nim +++ b/lib/pure/json.nim @@ -1677,6 +1677,30 @@ proc createConstructor(typeSym, jsonNode: NimNode): NimNode = ( if `lenientJsonNode`.isNil: `workaround`[`optionGeneric`]() else: some[`optionGeneric`](`value`) ) + of "table", "orderedtable": + let tableKeyType = typeSym[1] + if ($tableKeyType).cmpIgnoreStyle("string") != 0: + error("JSON doesn't support keys of type " & $tableKeyType) + let tableValueType = typeSym[2] + + let forLoopKey = genSym(nskForVar, "key") + let indexerNode = createJsonIndexer(jsonNode, forLoopKey) + let constructorNode = createConstructor(tableValueType, indexerNode) + + let tableInit = + if bracketName == "table": + bindSym("initTable") + else: + bindSym("initOrderedTable") + + # Create a statement expression containing a for loop. + result = quote do: + ( + var map = `tableInit`[`tableKeyType`, `tableValueType`](); + verifyJsonKind(`jsonNode`, {JObject}, astToStr(`jsonNode`)); + for `forLoopKey` in keys(`jsonNode`.fields): map[`forLoopKey`] = `constructorNode`; + map + ) of "ref": # Ref type. var typeName = $typeSym[1] diff --git a/tests/stdlib/tjsonmacro.nim b/tests/stdlib/tjsonmacro.nim index f9f94606c..01fa43aa7 100644 --- a/tests/stdlib/tjsonmacro.nim +++ b/tests/stdlib/tjsonmacro.nim @@ -2,7 +2,7 @@ discard """ file: "tjsonmacro.nim" output: "" """ -import json, strutils, options +import json, strutils, options, tables when isMainModule: # Tests inspired by own use case (with some additional tests). @@ -315,5 +315,28 @@ when isMainModule: doAssert noYearDeser.year.isNone doAssert noYearDeser.engine.name == "V8" - # TODO: Table[T, Y] support. + # Table[T, Y] support. + block: + type + Friend = object + name: string + age: int + + Dynamic = object + name: string + friends: Table[string, Friend] + + let data = """ + {"friends": { + "John": {"name": "John", "age": 35}, + "Elizabeth": {"name": "Elizabeth", "age": 23} + }, "name": "Dominik"} + """ + + let dataParsed = parseJson(data) + let dataDeser = to(dataParsed, Dynamic) + doAssert dataDeser.name == "Dominik" + doAssert dataDeser.friends["John"].age == 35 + doAssert dataDeser.friends["Elizabeth"].age == 23 + # TODO: JsonNode support \ No newline at end of file -- cgit 1.4.1-2-gfad0 From 8d6126237226a80ca4c78206c625009ce285c348 Mon Sep 17 00:00:00 2001 From: Dominik Picheta Date: Wed, 29 Nov 2017 20:47:56 +0000 Subject: Implement support for JsonNode in json.to. --- lib/pure/json.nim | 5 +++++ tests/stdlib/tjsonmacro.nim | 19 ++++++++++++++++++- 2 files changed, 23 insertions(+), 1 deletion(-) (limited to 'tests/stdlib') diff --git a/lib/pure/json.nim b/lib/pure/json.nim index 6153e2f03..1d2f480c4 100644 --- a/lib/pure/json.nim +++ b/lib/pure/json.nim @@ -1744,6 +1744,11 @@ proc createConstructor(typeSym, jsonNode: NimNode): NimNode = let obj = getType(typeSym) result = processType(typeSym, obj, jsonNode, false) of nnkSym: + # Handle JsonNode. + if ($typeSym).cmpIgnoreStyle("jsonnode") == 0: + return jsonNode + + # Handle all other types. let obj = getType(typeSym) if obj.kind == nnkBracketExpr: # When `Sym "Foo"` turns out to be a `ref object`. diff --git a/tests/stdlib/tjsonmacro.nim b/tests/stdlib/tjsonmacro.nim index 01fa43aa7..2baa7bed1 100644 --- a/tests/stdlib/tjsonmacro.nim +++ b/tests/stdlib/tjsonmacro.nim @@ -339,4 +339,21 @@ when isMainModule: doAssert dataDeser.friends["John"].age == 35 doAssert dataDeser.friends["Elizabeth"].age == 23 - # TODO: JsonNode support \ No newline at end of file + # JsonNode support + block: + type + Test = object + name: string + fallback: JsonNode + + let data = """ + {"name": "FooBar", "fallback": 56.42} + """ + + let dataParsed = parseJson(data) + let dataDeser = to(dataParsed, Test) + doAssert dataDeser.name == "FooBar" + doAssert dataDeser.fallback.kind == JFloat + doAssert dataDeser.fallback.getFloat() == 56.42 + + # TODO: Cycles lead to infinite loops. \ No newline at end of file -- cgit 1.4.1-2-gfad0 From 2bb2e6975e397bef1b320cd5dbafb6b3338fdaf0 Mon Sep 17 00:00:00 2001 From: Dominik Picheta Date: Thu, 30 Nov 2017 18:43:34 +0000 Subject: Fix infinite recursion when using json.to on ref with cycle. --- lib/pure/json.nim | 10 ++++++++++ tests/stdlib/tjsonmacro.nim | 4 +--- tests/stdlib/tjsonmacro_reject2.nim | 21 +++++++++++++++++++++ 3 files changed, 32 insertions(+), 3 deletions(-) create mode 100644 tests/stdlib/tjsonmacro_reject2.nim (limited to 'tests/stdlib') diff --git a/lib/pure/json.nim b/lib/pure/json.nim index 1d2f480c4..9e7510e45 100644 --- a/lib/pure/json.nim +++ b/lib/pure/json.nim @@ -1651,6 +1651,13 @@ import options proc workaroundMacroNone[T](): Option[T] = none(T) +proc depth(n: NimNode, current = 0): int = + result = 1 + for child in n: + let d = 1 + child.depth(current + 1) + if d > result: + result = d + proc createConstructor(typeSym, jsonNode: NimNode): NimNode = ## Accepts a type description, i.e. "ref Type", "seq[Type]", "Type" etc. ## @@ -1660,6 +1667,9 @@ proc createConstructor(typeSym, jsonNode: NimNode): NimNode = # echo("--createConsuctor-- \n", treeRepr(typeSym)) # echo() + if depth(jsonNode) > 150: + error("The `to` macro does not support ref objects with cycles.", jsonNode) + case typeSym.kind of nnkBracketExpr: var bracketName = ($typeSym[0]).normalize diff --git a/tests/stdlib/tjsonmacro.nim b/tests/stdlib/tjsonmacro.nim index 2baa7bed1..e2d8c27cf 100644 --- a/tests/stdlib/tjsonmacro.nim +++ b/tests/stdlib/tjsonmacro.nim @@ -354,6 +354,4 @@ when isMainModule: let dataDeser = to(dataParsed, Test) doAssert dataDeser.name == "FooBar" doAssert dataDeser.fallback.kind == JFloat - doAssert dataDeser.fallback.getFloat() == 56.42 - - # TODO: Cycles lead to infinite loops. \ No newline at end of file + doAssert dataDeser.fallback.getFloat() == 56.42 \ No newline at end of file diff --git a/tests/stdlib/tjsonmacro_reject2.nim b/tests/stdlib/tjsonmacro_reject2.nim new file mode 100644 index 000000000..b01153553 --- /dev/null +++ b/tests/stdlib/tjsonmacro_reject2.nim @@ -0,0 +1,21 @@ +discard """ + file: "tjsonmacro_reject2.nim" + line: 10 + errormsg: "The `to` macro does not support ref objects with cycles." +""" +import json + +type + Misdirection = object + cycle: Cycle + + Cycle = ref object + foo: string + cycle: Misdirection + +let data = """ + {"cycle": null} +""" + +let dataParsed = parseJson(data) +let dataDeser = to(dataParsed, Cycle) \ No newline at end of file -- cgit 1.4.1-2-gfad0 From 578ab935cbb1a9b53c0192d389c1a01c4e6e95ac Mon Sep 17 00:00:00 2001 From: Dominik Picheta Date: Thu, 30 Nov 2017 18:56:34 +0000 Subject: Support all int, uint and float variants in json.to macro. --- lib/pure/json.nim | 30 ++++++++++++++++-------------- tests/stdlib/tjsonmacro.nim | 28 +++++++++++++++++++++++++++- 2 files changed, 43 insertions(+), 15 deletions(-) (limited to 'tests/stdlib') diff --git a/lib/pure/json.nim b/lib/pure/json.nim index 9e7510e45..b5b84863a 100644 --- a/lib/pure/json.nim +++ b/lib/pure/json.nim @@ -1609,25 +1609,14 @@ proc processType(typeName: NimNode, obj: NimNode, `getEnumCall` ) of nnkSym: - case ($typeName).normalize - of "float": - result = quote do: - ( - verifyJsonKind(`jsonNode`, {JFloat, JInt}, astToStr(`jsonNode`)); - if `jsonNode`.kind == JFloat: `jsonNode`.fnum else: `jsonNode`.num.float - ) + let name = ($typeName).normalize + case name of "string": result = quote do: ( verifyJsonKind(`jsonNode`, {JString, JNull}, astToStr(`jsonNode`)); if `jsonNode`.kind == JNull: nil else: `jsonNode`.str ) - of "int": - result = quote do: - ( - verifyJsonKind(`jsonNode`, {JInt}, astToStr(`jsonNode`)); - `jsonNode`.num.int - ) of "biggestint": result = quote do: ( @@ -1641,7 +1630,20 @@ proc processType(typeName: NimNode, obj: NimNode, `jsonNode`.bval ) else: - doAssert false, "Unable to process nnkSym " & $typeName + if name.startsWith("int") or name.startsWith("uint"): + result = quote do: + ( + verifyJsonKind(`jsonNode`, {JInt}, astToStr(`jsonNode`)); + `jsonNode`.num.`obj` + ) + elif name.startsWith("float"): + result = quote do: + ( + verifyJsonKind(`jsonNode`, {JInt, JFloat}, astToStr(`jsonNode`)); + if `jsonNode`.kind == JFloat: `jsonNode`.fnum.`obj` else: `jsonNode`.num.`obj` + ) + else: + doAssert false, "Unable to process nnkSym " & $typeName else: doAssert false, "Unable to process type: " & $obj.kind diff --git a/tests/stdlib/tjsonmacro.nim b/tests/stdlib/tjsonmacro.nim index e2d8c27cf..2cdd82305 100644 --- a/tests/stdlib/tjsonmacro.nim +++ b/tests/stdlib/tjsonmacro.nim @@ -354,4 +354,30 @@ when isMainModule: let dataDeser = to(dataParsed, Test) doAssert dataDeser.name == "FooBar" doAssert dataDeser.fallback.kind == JFloat - doAssert dataDeser.fallback.getFloat() == 56.42 \ No newline at end of file + doAssert dataDeser.fallback.getFloat() == 56.42 + + # int64, float64 etc support. + block: + type + Test1 = object + a: int8 + b: int16 + c: int32 + d: int64 + e: uint8 + f: uint16 + g: uint32 + h: uint64 + i: float32 + j: float64 + + let data = """ + {"a": 1, "b": 2, "c": 3, "d": 4, "e": 5, "f": 6, "g": 7, + "h": 8, "i": 9.9, "j": 10.10} + """ + + let dataParsed = parseJson(data) + let dataDeser = to(dataParsed, Test1) + doAssert dataDeser.a == 1 + doAssert dataDeser.f == 6 + doAssert dataDeser.i == 9.9'f32 \ No newline at end of file -- cgit 1.4.1-2-gfad0 From e860334377365d903b607583e6253209a356ed08 Mon Sep 17 00:00:00 2001 From: Andreas Rumpf Date: Thu, 14 Dec 2017 12:46:09 +0100 Subject: added SQL parser test --- tests/stdlib/somesql.sql | 298 ++++++++++++++++++++++++++++++++++++++++++++ tests/stdlib/tsqlparser.nim | 12 ++ 2 files changed, 310 insertions(+) create mode 100644 tests/stdlib/somesql.sql create mode 100644 tests/stdlib/tsqlparser.nim (limited to 'tests/stdlib') diff --git a/tests/stdlib/somesql.sql b/tests/stdlib/somesql.sql new file mode 100644 index 000000000..285f93cec --- /dev/null +++ b/tests/stdlib/somesql.sql @@ -0,0 +1,298 @@ +create table anon40( + anon41 anon42 primary key default anon43(), + anon44 text unique not null, + anon45 text unique not null, + anon46 text not null, + anon47 text not null, + anon48 text default null, + anon49 text default null, + anon50 text default null, + anon51 text default null, + anon52 text default null, + anon53 text default null, + anon54 text default null, + anon55 text default null, + anon56 text default null, + anon57 text default null, + anon58 text default null, + anon59 text default null, + anon60 text default null, + anon61 text default null, + anon62 varchar(30) default null, + anon63 varchar(30) default null); +create table anon64( + anon41 serial primary key, + anon65 varchar(30) not null unique, + anon46 varchar(30) not null, + anon66 varchar(30) not null, + anon47 varchar(30) not null, + anon67 text not null, + anon55 varchar(30) not null unique, + anon68 varchar(30) default 'o', + anon69 boolean default true, + anon70 int not null references anon40(anon41)); +create table anon71( + anon72 varchar(30) not null primary key, + anon73 varchar(30) not null unique, + anon70 int not null references anon40(anon41)); +create table anon74( + anon72 varchar(30) not null primary key, + anon73 varchar(30) not null unique, + anon75 varchar(30) not null, + anon70 int not null references anon40(anon41), + foreign key(anon75) references anon71(anon72)); +create table anon76( + anon41 serial primary key, + anon72 varchar(30) not null unique, + anon73 varchar(30) not null unique, + anon77 varchar(30) not null, + anon70 int not null references anon40(anon41), + foreign key(anon77) references anon74(anon72)); +create table anon78( + anon41 serial primary key, + anon72 varchar(30) not null unique, + anon73 varchar(30) not null unique, + anon79 int not null, + anon80 varchar(30) default null, + anon81 int not null, + anon69 boolean not null default true, + anon70 int not null references anon40(anon41), + foreign key(anon79) references anon78(anon41), + foreign key(anon81) references anon76(anon41)); +create table anon82( + anon41 serial primary key, + anon72 varchar(30) not null unique, + anon73 text not null unique, + anon79 int not null, + anon80 text default null, + anon83 varchar(30) not null default 'd', + anon84 decimal default 0.00, + anon69 boolean not null default true, + anon85 decimal default 0.00, + anon86 decimal default 0.00, + anon87 decimal default 0.00, + anon70 int not null references anon40(anon41), + foreign key(anon79) references anon78(anon41)); +create table anon88( + anon41 serial primary key, + anon72 varchar(30) not null unique, + anon80 text default '', + anon69 boolean not null default true, + anon70 int not null references anon40(anon41)); +create table anon89( + anon90 int not null primary key, + anon91 anon92 default 0.00, + anon93 varchar(30), + anon69 boolean not null default true, + anon70 int not null references anon40(anon41), + foreign key(anon90) references anon82(anon41)); +create table anon94( + anon41 serial primary key, + anon73 text unique not null, + anon80 text default null, + anon69 boolean not null default true, + anon70 int not null references anon40(anon41)); +create table anon95( + anon41 serial primary key, + anon73 text unique not null, + anon96 int not null references anon94(anon41), + anon80 text default null, + anon69 boolean not null default true, + anon70 int not null references anon40(anon41)); +create table anon97( + anon41 serial primary key, + anon73 text unique not null, + anon98 int not null references anon95(anon41), + anon80 text default null, + anon69 boolean not null default true, + anon70 int not null references anon40(anon41)); +create table anon99( + anon41 serial primary key, + anon73 varchar(30) unique not null, + anon100 varchar(30) default null, + anon101 anon102 default 0, + anon103 varchar(30) default 'g', + anon104 int not null, + anon105 decimal not null default 1, + anon69 boolean not null default true, + anon70 int not null references anon40(anon41)); +create table anon106( + anon107 varchar(30) default 'g', + anon108 int references anon99(anon41) not null, + anon109 decimal default 1, + anon110 int references anon99(anon41) not null, + anon70 int not null references anon40(anon41)); +create table anon111( + anon41 serial primary key, + anon112 text unique not null, + anon73 text unique not null, + anon113 anon102 references anon97(anon41) not null, + anon114 varchar(30) not null, + anon115 int not null references anon88(anon41), + anon116 int not null references anon82(anon41), + anon117 int not null references anon82(anon41), + anon118 int not null references anon82(anon41), + anon119 int not null references anon82(anon41), + anon120 int not null references anon82(anon41), + anon121 int not null references anon82(anon41), + anon122 int references anon99(anon41) not null, + anon123 decimal default 0.00, + anon124 decimal default 0.00, + anon69 boolean default true, + anon70 int not null references anon40(anon41)); +create table anon125( + anon41 serial primary key, + anon126 int references anon111(anon41) not null, + anon80 text not null, + anon127 varchar(30) not null, + anon128 decimal default 0.00, + anon129 decimal default 0, + anon130 decimal default 0, + anon131 decimal default 0, + anon132 decimal default 0, + anon133 decimal default 0.00, + anon134 decimal default 0.00, + anon135 decimal default 0.00, + anon70 int not null references anon40(anon41), constraint anon136 check anon137(anon126, anon127, anon129)); +create table anon138( + anon41 serial primary key, + anon126 int references anon111(anon41) not null, + anon80 text not null, + anon127 varchar(30) not null, + anon139 date not null, + anon129 decimal default 0, + anon130 decimal default 0, + anon131 decimal default 0, + anon132 decimal default 0, + anon70 int not null references anon40(anon41), constraint anon136 check anon137(anon127, anon129)); +create table anon140( + anon41 serial primary key, + anon141 text unique not null, + anon46 text default null, + anon47 text default null, + anon57 varchar(30) default null, + anon142 text default null, + anon51 text default null, + anon143 varchar(30) default null, + anon53 text default null, + anon54 text default null, + anon55 text default null, + anon45 text default null, + anon69 boolean default true, + anon70 int not null references anon40(anon41)); +create table anon144( + anon41 serial primary key, + anon72 varchar(30) unique not null, + anon73 varchar(30) unique not null, + anon80 varchar(30) default null, + anon69 boolean default true, + anon70 int not null references anon40(anon41)); +create table anon145( + anon41 serial primary key, + anon72 varchar(30) unique not null, + anon73 varchar(30) unique not null, + anon146 int not null, + anon147 anon92 default 1, + anon148 anon92 default 9999999, + anon80 varchar(30) default null, + anon69 boolean default true, + anon149 int default 0, + anon150 int not null, + anon151 anon92 default 0, + anon70 int not null references anon40(anon41), + foreign key(anon150) references anon82(anon41), + foreign key(anon146) references anon144(anon41)); +create table anon152( + anon41 serial primary key, + anon73 varchar(30) not null unique, + anon153 varchar(30) not null unique, + anon80 text default null, + anon69 boolean not null default true, + anon70 int not null references anon40(anon41)); +create table anon154( + anon41 serial primary key not null, + anon155 int not null unique, + date date default anon156 not null, + anon157 anon102 references anon140(anon41) not null, + anon158 anon102 references anon64(anon41) not null, + anon159 decimal default 0 not null, + anon160 decimal default 0 not null, + anon161 decimal default 0 not null, + anon162 decimal default 0 not null, + anon163 decimal default 0 not null, + anon164 decimal default 0 not null, + anon165 decimal default 0.00, + anon166 decimal default 0 not null, + anon167 decimal default 0.00, + anon168 decimal default 0 not null, + anon169 boolean default false, + anon170 varchar(30) default 'ca', + anon171 varchar(30) default 'n', + anon172 varchar(30) not null default 'd', + anon173 decimal default 0.00, + anon174 decimal default 0.00, + anon175 int, + anon176 varchar(30) default null, + anon177 varchar(30) default '', + anon178 varchar(30) default null, + anon70 int not null references anon40(anon41)); +create table anon179( + anon41 serial primary key not null, + anon180 anon102 references anon154(anon41) not null, + anon181 int references anon125(anon41) not null, + anon182 int references anon82(anon41) not null, + anon122 int references anon99(anon41) not null, + anon183 decimal not null, + anon184 decimal default 0.00, + anon174 decimal default 0, + anon160 decimal default 0.00, + anon185 decimal default 0, + anon162 decimal default 0.00, + anon186 decimal default 0, + anon163 decimal default 0.00, + anon187 decimal default 0, + anon164 decimal default 0.00, + anon188 decimal default 0, + anon161 decimal default 0.00, + anon189 decimal default 0.00, + anon168 decimal default 0.00, + anon190 decimal not null, + anon191 decimal default 0, + anon83 varchar(30) not null default 't', + anon192 decimal default 0, + anon193 decimal not null, + anon194 decimal not null, + anon70 int not null references anon40(anon41)); +create table anon195( + anon41 serial not null, + anon196 int not null, + anon175 char not null, + anon90 int not null references anon82, + anon165 decimal default 0.00, + anon70 int not null references anon40(anon41), primary key(anon196, anon175)); +create table anon197( + anon41 serial not null, + anon196 int not null, + anon175 char not null, + anon198 int not null, + anon189 decimal default 0.00, + anon199 varchar(30) default null, + anon200 varchar(30) default null, + anon70 int not null references anon40(anon41), + primary key(anon196, anon175), + foreign key(anon198) references anon145(anon41)); +create table anon201( + anon41 serial primary key, + anon202 varchar(30) not null, + anon203 varchar(30) not null, + anon204 varchar(30) not null, + anon205 varchar(30) not null, + anon206 boolean default null, + anon70 int not null references anon40(anon41)); +create table anon207( + anon41 serial primary key, + anon208 varchar(30) not null, + anon209 varchar(30) not null, + anon204 varchar(30) default null, + anon70 int not null references anon40(anon41)); + diff --git a/tests/stdlib/tsqlparser.nim b/tests/stdlib/tsqlparser.nim new file mode 100644 index 000000000..4a7b2f7d7 --- /dev/null +++ b/tests/stdlib/tsqlparser.nim @@ -0,0 +1,12 @@ +discard """ + output: '''true''' +""" + +# Just check that we can parse 'somesql' and render it without crashes. + +import parsesql, streams, os + +var tree = parseSql(newFileStream(getAppDir() / "somesql.sql"), "somesql") +discard renderSql(tree) + +echo "true" -- cgit 1.4.1-2-gfad0 From 6df6ec27ec573fc7f619f7bf9fece6d6b0dc931f Mon Sep 17 00:00:00 2001 From: Fabian Keller Date: Thu, 14 Dec 2017 14:02:13 +0100 Subject: Improved collection-to-string behavior (#6825) --- changelog.md | 3 + lib/pure/collections/critbits.nim | 6 +- lib/pure/collections/deques.nim | 2 +- lib/pure/collections/lists.nim | 2 +- lib/pure/collections/sets.nim | 2 +- lib/pure/collections/tables.nim | 4 +- lib/pure/strutils.nim | 26 ++----- lib/system.nim | 73 ++++++++++++++++-- tests/array/troof1.nim | 2 +- tests/ccgbugs/t6279.nim | 2 +- tests/ccgbugs/tbug1081.nim | 2 +- tests/ccgbugs/tconstobj.nim | 2 +- tests/ccgbugs/tobjconstr_regression.nim | 2 +- tests/collections/tcollections_to_string.nim | 106 +++++++++++++++++++++++++++ tests/collections/ttables.nim | 2 +- tests/collections/ttablesref.nim | 4 +- tests/concepts/t3330.nim | 2 +- tests/exprs/tstmtexprs.nim | 2 +- tests/fields/timplicitfieldswithpartial.nim | 2 +- tests/generics/treentranttypes.nim | 4 +- tests/js/trefbyvar.nim | 2 +- tests/metatype/ttypedesc2.nim | 2 +- tests/misc/tlocals.nim | 2 +- tests/objects/tobjconstr.nim | 20 ++--- tests/showoff/thello2.nim | 2 +- tests/stdlib/tlists.nim | 2 +- tests/stdlib/treloop.nim | 2 +- tests/system/toString.nim | 8 +- tests/typerel/typeof_in_template.nim | 2 +- tests/types/tinheritpartialgeneric.nim | 4 +- tests/types/tparameterizedparent2.nim | 2 +- tests/vm/tableinstatic.nim | 2 +- tests/vm/tconstobj.nim | 2 +- 33 files changed, 227 insertions(+), 75 deletions(-) create mode 100644 tests/collections/tcollections_to_string.nim (limited to 'tests/stdlib') diff --git a/changelog.md b/changelog.md index 94cea7dbf..6065ccd10 100644 --- a/changelog.md +++ b/changelog.md @@ -123,3 +123,6 @@ This now needs to be written as: to [http://www.gii.upv.es/tlsf/](http://www.gii.upv.es/tlsf/) the maximum fragmentation measured is lower than 25%. As a nice bonus ``alloc`` and ``dealloc`` became O(1) operations. +- The behavior of ``$`` has been changed for all standard library collections. The + collection-to-string implementations now perform proper quoting and escaping of + strings and chars. diff --git a/lib/pure/collections/critbits.nim b/lib/pure/collections/critbits.nim index 19f1f2e58..34f5c5470 100644 --- a/lib/pure/collections/critbits.nim +++ b/lib/pure/collections/critbits.nim @@ -141,8 +141,8 @@ proc excl*[T](c: var CritBitTree[T], key: string) = proc missingOrExcl*[T](c: var CritBitTree[T], key: string): bool = ## Returns true iff `c` does not contain the given `key`. If the key - ## does exist, c.excl(key) is performed. - let oldCount = c.count + ## does exist, c.excl(key) is performed. + let oldCount = c.count var n = exclImpl(c, key) result = c.count == oldCount @@ -326,7 +326,7 @@ proc `$`*[T](c: CritBitTree[T]): string = result.add($key) when T isnot void: result.add(": ") - result.add($val) + result.addQuoted(val) result.add("}") when isMainModule: diff --git a/lib/pure/collections/deques.nim b/lib/pure/collections/deques.nim index 1e0cb82d2..328308a9b 100644 --- a/lib/pure/collections/deques.nim +++ b/lib/pure/collections/deques.nim @@ -185,7 +185,7 @@ proc `$`*[T](deq: Deque[T]): string = result = "[" for x in deq: if result.len > 1: result.add(", ") - result.add($x) + result.addQuoted(x) result.add("]") when isMainModule: diff --git a/lib/pure/collections/lists.nim b/lib/pure/collections/lists.nim index 560273dfa..e69acc8d9 100644 --- a/lib/pure/collections/lists.nim +++ b/lib/pure/collections/lists.nim @@ -135,7 +135,7 @@ proc `$`*[T](L: SomeLinkedCollection[T]): string = result = "[" for x in nodes(L): if result.len > 1: result.add(", ") - result.add($x.value) + result.addQuoted(x.value) result.add("]") proc find*[T](L: SomeLinkedCollection[T], value: T): SomeLinkedNode[T] = diff --git a/lib/pure/collections/sets.nim b/lib/pure/collections/sets.nim index f936b3eca..9e9152fc8 100644 --- a/lib/pure/collections/sets.nim +++ b/lib/pure/collections/sets.nim @@ -406,7 +406,7 @@ template dollarImpl() {.dirty.} = result = "{" for key in items(s): if result.len > 1: result.add(", ") - result.add($key) + result.addQuoted(key) result.add("}") proc `$`*[A](s: HashSet[A]): string = diff --git a/lib/pure/collections/tables.nim b/lib/pure/collections/tables.nim index 28fbaa632..38f8f97f5 100644 --- a/lib/pure/collections/tables.nim +++ b/lib/pure/collections/tables.nim @@ -338,9 +338,9 @@ template dollarImpl(): untyped {.dirty.} = result = "{" for key, val in pairs(t): if result.len > 1: result.add(", ") - result.add($key) + result.addQuoted(key) result.add(": ") - result.add($val) + result.addQuoted(val) result.add("}") proc `$`*[A, B](t: Table[A, B]): string = diff --git a/lib/pure/strutils.nim b/lib/pure/strutils.nim index 62ceaa2e8..dbb4db781 100644 --- a/lib/pure/strutils.nim +++ b/lib/pure/strutils.nim @@ -1761,29 +1761,15 @@ proc insertSep*(s: string, sep = '_', digits = 3): string {.noSideEffect, proc escape*(s: string, prefix = "\"", suffix = "\""): string {.noSideEffect, rtl, extern: "nsuEscape".} = - ## Escapes a string `s`. - ## - ## This does these operations (at the same time): - ## * replaces any ``\`` by ``\\`` - ## * replaces any ``'`` by ``\'`` - ## * replaces any ``"`` by ``\"`` - ## * replaces any other character in the set ``{'\0'..'\31', '\127'..'\255'}`` - ## by ``\xHH`` where ``HH`` is its hexadecimal value. - ## The procedure has been designed so that its output is usable for many - ## different common syntaxes. The resulting string is prefixed with - ## `prefix` and suffixed with `suffix`. Both may be empty strings. - ## **Note**: This is not correct for producing Ansi C code! + ## Escapes a string `s`. See `system.addEscapedChar `_ + ## for the escaping scheme. + ## + ## The resulting string is prefixed with `prefix` and suffixed with `suffix`. + ## Both may be empty strings. result = newStringOfCap(s.len + s.len shr 2) result.add(prefix) for c in items(s): - case c - of '\0'..'\31', '\127'..'\255': - add(result, "\\x") - add(result, toHex(ord(c), 2)) - of '\\': add(result, "\\\\") - of '\'': add(result, "\\'") - of '\"': add(result, "\\\"") - else: add(result, c) + result.addEscapedChar(c) add(result, suffix) proc unescape*(s: string, prefix = "\"", suffix = "\""): string {.noSideEffect, diff --git a/lib/system.nim b/lib/system.nim index b9f01c306..e66699ae4 100644 --- a/lib/system.nim +++ b/lib/system.nim @@ -1325,6 +1325,7 @@ proc add*(x: var string, y: string) {.magic: "AppendStrStr", noSideEffect.} ## tmp.add("cd") ## assert(tmp == "abcd") + type Endianness* = enum ## is a type describing the endianness of a processor. littleEndian, bigEndian @@ -2503,9 +2504,9 @@ proc `$`*[T: tuple|object](x: T): string = when compiles($value): when compiles(value.isNil): if value.isNil: result.add "nil" - else: result.add($value) + else: result.addQuoted(value) else: - result.add($value) + result.addQuoted(value) firstElement = false else: result.add("...") @@ -2525,12 +2526,9 @@ proc collectionToString[T](x: T, prefix, separator, suffix: string): string = if value.isNil: result.add "nil" else: - result.add($value) - # prevent temporary string allocation - elif compiles(result.add(value)): - result.add(value) + result.addQuoted(value) else: - result.add($value) + result.addQuoted(value) result.add(suffix) @@ -3893,6 +3891,65 @@ proc compiles*(x: untyped): bool {.magic: "Compiles", noSideEffect, compileTime. when declared(initDebugger): initDebugger() +proc addEscapedChar*(s: var string, c: char) {.noSideEffect, inline.} = + ## Adds a char to string `s` and applies the following escaping: + ## + ## * replaces any ``\`` by ``\\`` + ## * replaces any ``'`` by ``\'`` + ## * replaces any ``"`` by ``\"`` + ## * replaces any other character in the set ``{'\0'..'\31', '\127'..'\255'}`` + ## by ``\xHH`` where ``HH`` is its hexadecimal value. + ## + ## The procedure has been designed so that its output is usable for many + ## different common syntaxes. + ## **Note**: This is not correct for producing Ansi C code! + case c + of '\0'..'\31', '\127'..'\255': + add(s, "\\x") + const HexChars = "0123456789ABCDEF" + let n = ord(c) + s.add(HexChars[int((n and 0xF0) shr 4)]) + s.add(HexChars[int(n and 0xF)]) + of '\\': add(s, "\\\\") + of '\'': add(s, "\\'") + of '\"': add(s, "\\\"") + else: add(s, c) + +proc addQuoted*[T](s: var string, x: T) = + ## Appends `x` to string `s` in place, applying quoting and escaping + ## if `x` is a string or char. See + ## `addEscapedChar `_ + ## for the escaping scheme. + ## + ## The Nim standard library uses this function on the elements of + ## collections when producing a string representation of a collection. + ## It is recommended to use this function as well for user-side collections. + ## Users may overload `addQuoted` for custom (string-like) types if + ## they want to implement a customized element representation. + ## + ## .. code-block:: Nim + ## var tmp = "" + ## tmp.addQuoted(1) + ## tmp.add(", ") + ## tmp.addQuoted("string") + ## tmp.add(", ") + ## tmp.addQuoted('c') + ## assert(tmp == """1, "string", 'c'""") + when T is string: + s.add("\"") + for c in x: + s.addEscapedChar(c) + s.add("\"") + elif T is char: + s.add("'") + s.addEscapedChar(x) + s.add("'") + # prevent temporary string allocation + elif compiles(s.add(x)): + s.add(x) + else: + s.add($x) + when hasAlloc: # XXX: make these the default (or implement the NilObject optimization) proc safeAdd*[T](x: var seq[T], y: T) {.noSideEffect.} = @@ -4034,4 +4091,4 @@ template doAssertRaises*(exception, code: untyped): typed = except Exception as exc: raiseAssert(astToStr(exception) & " wasn't raised, another error was raised instead by:\n"& - astToStr(code)) \ No newline at end of file + astToStr(code)) diff --git a/tests/array/troof1.nim b/tests/array/troof1.nim index 594ad98a5..b486c3448 100644 --- a/tests/array/troof1.nim +++ b/tests/array/troof1.nim @@ -4,7 +4,7 @@ discard """ 3 @[(Field0: 1, Field1: 2), (Field0: 3, Field1: 5)] 2 -@[a, new one, c] +@["a", "new one", "c"] @[1, 2, 3]''' """ diff --git a/tests/ccgbugs/t6279.nim b/tests/ccgbugs/t6279.nim index 37345d807..5a3d6768c 100644 --- a/tests/ccgbugs/t6279.nim +++ b/tests/ccgbugs/t6279.nim @@ -1,6 +1,6 @@ discard """ cmd: "nim c -r -d:fulldebug -d:smokeCycles --gc:refc $file" -output: '''@[a]''' +output: '''@["a"]''' """ # bug #6279 diff --git a/tests/ccgbugs/tbug1081.nim b/tests/ccgbugs/tbug1081.nim index c9a9e6aa4..baef99d84 100644 --- a/tests/ccgbugs/tbug1081.nim +++ b/tests/ccgbugs/tbug1081.nim @@ -3,7 +3,7 @@ discard """ 0 0 0 -x = [a, b, c, 0, 1, 2, 3, 4, 5, 6] and y = [a, b, c, 0, 1, 2, 3, 4, 5, 6]''' +x = ['a', 'b', 'c', '0', '1', '2', '3', '4', '5', '6'] and y = ['a', 'b', 'c', '0', '1', '2', '3', '4', '5', '6']''' """ proc `1/1`() = echo(1 div 1) diff --git a/tests/ccgbugs/tconstobj.nim b/tests/ccgbugs/tconstobj.nim index 98f441e83..51cf661ee 100644 --- a/tests/ccgbugs/tconstobj.nim +++ b/tests/ccgbugs/tconstobj.nim @@ -1,5 +1,5 @@ discard """ - output: '''(FirstName: James, LastName: Franco)''' + output: '''(FirstName: "James", LastName: "Franco")''' """ # bug #1547 diff --git a/tests/ccgbugs/tobjconstr_regression.nim b/tests/ccgbugs/tobjconstr_regression.nim index 87d037894..d29abad97 100644 --- a/tests/ccgbugs/tobjconstr_regression.nim +++ b/tests/ccgbugs/tobjconstr_regression.nim @@ -1,5 +1,5 @@ discard """ - output: "@[(username: user, role: admin, description: desc, email_addr: email), (username: user, role: admin, description: desc, email_addr: email)]" + output: '''@[(username: "user", role: "admin", description: "desc", email_addr: "email"), (username: "user", role: "admin", description: "desc", email_addr: "email")]''' """ type diff --git a/tests/collections/tcollections_to_string.nim b/tests/collections/tcollections_to_string.nim new file mode 100644 index 000000000..6cc8a84ff --- /dev/null +++ b/tests/collections/tcollections_to_string.nim @@ -0,0 +1,106 @@ +discard """ + exitcode: 0 + output: "" +""" +import sets +import tables +import deques +import lists +import critbits + +# Tests for tuples +doAssert $(1, 2, 3) == "(Field0: 1, Field1: 2, Field2: 3)" +doAssert $("1", "2", "3") == """(Field0: "1", Field1: "2", Field2: "3")""" +doAssert $('1', '2', '3') == """(Field0: '1', Field1: '2', Field2: '3')""" + +# Tests for seqs +doAssert $(@[1, 2, 3]) == "@[1, 2, 3]" +doAssert $(@["1", "2", "3"]) == """@["1", "2", "3"]""" +doAssert $(@['1', '2', '3']) == """@['1', '2', '3']""" + +# Tests for sets +doAssert $(toSet([1])) == "{1}" +doAssert $(toSet(["1"])) == """{"1"}""" +doAssert $(toSet(['1'])) == """{'1'}""" +doAssert $(toOrderedSet([1, 2, 3])) == "{1, 2, 3}" +doAssert $(toOrderedSet(["1", "2", "3"])) == """{"1", "2", "3"}""" +doAssert $(toOrderedSet(['1', '2', '3'])) == """{'1', '2', '3'}""" + +# Tests for tables +doAssert $({1: "1", 2: "2"}.toTable) == """{1: "1", 2: "2"}""" +doAssert $({"1": 1, "2": 2}.toTable) == """{"1": 1, "2": 2}""" + +# Tests for deques +block: + var d = initDeque[int]() + d.addLast(1) + doAssert $d == "[1]" +block: + var d = initDeque[string]() + d.addLast("1") + doAssert $d == """["1"]""" +block: + var d = initDeque[char]() + d.addLast('1') + doAssert $d == "['1']" + +# Tests for lists +block: + var l = initDoublyLinkedList[int]() + l.append(1) + l.append(2) + l.append(3) + doAssert $l == "[1, 2, 3]" +block: + var l = initDoublyLinkedList[string]() + l.append("1") + l.append("2") + l.append("3") + doAssert $l == """["1", "2", "3"]""" +block: + var l = initDoublyLinkedList[char]() + l.append('1') + l.append('2') + l.append('3') + doAssert $l == """['1', '2', '3']""" + +# Tests for critbits +block: + var t: CritBitTree[int] + t["a"] = 1 + doAssert $t == "{a: 1}" +block: + var t: CritBitTree[string] + t["a"] = "1" + doAssert $t == """{a: "1"}""" +block: + var t: CritBitTree[char] + t["a"] = '1' + doAssert $t == "{a: '1'}" + + +# Test escaping behavior +block: + var s = "" + s.addQuoted('\0') + s.addQuoted('\31') + s.addQuoted('\127') + s.addQuoted('\255') + doAssert s == "'\\x00''\\x1F''\\x7F''\\xFF'" +block: + var s = "" + s.addQuoted('\\') + s.addQuoted('\'') + s.addQuoted('\"') + doAssert s == """'\\''\'''\"'""" + +# Test customized element representation +type CustomString = object + +proc addQuoted(s: var string, x: CustomString) = + s.add("") + +block: + let s = @[CustomString()] + doAssert $s == "@[]" + diff --git a/tests/collections/ttables.nim b/tests/collections/ttables.nim index 01dab44fc..2b8af5bd9 100644 --- a/tests/collections/ttables.nim +++ b/tests/collections/ttables.nim @@ -48,7 +48,7 @@ block tableTest1: for y in 0..1: assert t[(x,y)] == $x & $y assert($t == - "{(x: 0, y: 1): 01, (x: 0, y: 0): 00, (x: 1, y: 0): 10, (x: 1, y: 1): 11}") + "{(x: 0, y: 1): \"01\", (x: 0, y: 0): \"00\", (x: 1, y: 0): \"10\", (x: 1, y: 1): \"11\"}") block tableTest2: var t = initTable[string, float]() diff --git a/tests/collections/ttablesref.nim b/tests/collections/ttablesref.nim index 12af1ccbb..a4030e0dc 100644 --- a/tests/collections/ttablesref.nim +++ b/tests/collections/ttablesref.nim @@ -47,7 +47,7 @@ block tableTest1: for y in 0..1: assert t[(x,y)] == $x & $y assert($t == - "{(x: 0, y: 1): 01, (x: 0, y: 0): 00, (x: 1, y: 0): 10, (x: 1, y: 1): 11}") + "{(x: 0, y: 1): \"01\", (x: 0, y: 0): \"00\", (x: 1, y: 0): \"10\", (x: 1, y: 1): \"11\"}") block tableTest2: var t = newTable[string, float]() @@ -139,7 +139,7 @@ proc orderedTableSortTest() = block anonZipTest: let keys = @['a','b','c'] let values = @[1, 2, 3] - doAssert "{a: 1, b: 2, c: 3}" == $ toTable zip(keys, values) + doAssert "{'a': 1, 'b': 2, 'c': 3}" == $ toTable zip(keys, values) block clearTableTest: var t = newTable[string, float]() diff --git a/tests/concepts/t3330.nim b/tests/concepts/t3330.nim index fcd5054ef..722c0a0e0 100644 --- a/tests/concepts/t3330.nim +++ b/tests/concepts/t3330.nim @@ -6,10 +6,10 @@ but expected one of: proc test(foo: Foo[int]) t3330.nim(25, 8) Hint: Non-matching candidates for add(k, string, T) proc add(x: var string; y: string) +proc add(result: var string; x: float) proc add(x: var string; y: char) proc add(result: var string; x: int64) proc add(x: var string; y: cstring) -proc add(result: var string; x: float) proc add[T](x: var seq[T]; y: openArray[T]) proc add[T](x: var seq[T]; y: T) diff --git a/tests/exprs/tstmtexprs.nim b/tests/exprs/tstmtexprs.nim index 01f429b07..9283f7268 100644 --- a/tests/exprs/tstmtexprs.nim +++ b/tests/exprs/tstmtexprs.nim @@ -1,6 +1,6 @@ discard """ output: '''24 -(bar: bar) +(bar: "bar") 1244 6 abcdefghijklmnopqrstuvwxyz diff --git a/tests/fields/timplicitfieldswithpartial.nim b/tests/fields/timplicitfieldswithpartial.nim index a315cc5d0..937833257 100644 --- a/tests/fields/timplicitfieldswithpartial.nim +++ b/tests/fields/timplicitfieldswithpartial.nim @@ -1,5 +1,5 @@ discard """ - output: '''(foo: 38, other: string here) + output: '''(foo: 38, other: "string here") 43 100 90''' diff --git a/tests/generics/treentranttypes.nim b/tests/generics/treentranttypes.nim index 9b4774e9b..2ef049ce2 100644 --- a/tests/generics/treentranttypes.nim +++ b/tests/generics/treentranttypes.nim @@ -1,6 +1,6 @@ discard """ output: ''' -(Field0: 10, Field1: (Field0: test, Field1: 1.2)) +(Field0: 10, Field1: (Field0: "test", Field1: 1.2)) 3x3 Matrix [[0.0, 2.0, 3.0], [2.0, 0.0, 5.0], [2.0, 0.0, 5.0]] 2x3 Matrix [[0.0, 2.0, 3.0], [2.0, 0.0, 5.0]] @@ -43,7 +43,7 @@ type Matrix*[M: static[int]; N: static[int]; T] = Vector[M, Vector[N, T]] - + proc arrayTest = # every kind of square matrix works just fine let mat_good: Matrix[3, 3, float] = [[0.0, 2.0, 3.0], diff --git a/tests/js/trefbyvar.nim b/tests/js/trefbyvar.nim index d440fcc64..5b168044e 100644 --- a/tests/js/trefbyvar.nim +++ b/tests/js/trefbyvar.nim @@ -66,4 +66,4 @@ proc initTypeA1(a: int; b: string; c: pointer = nil): TypeA1 = result.c_impl = c let x = initTypeA1(1, "a") -doAssert($x == "(a_impl: 1, b_impl: a, c_impl: ...)") +doAssert($x == "(a_impl: 1, b_impl: \"a\", c_impl: ...)") diff --git a/tests/metatype/ttypedesc2.nim b/tests/metatype/ttypedesc2.nim index 7650a6f6b..4b6cfe6bc 100644 --- a/tests/metatype/ttypedesc2.nim +++ b/tests/metatype/ttypedesc2.nim @@ -1,5 +1,5 @@ discard """ - output: "(x: a)" + output: '''(x: 'a')''' """ type diff --git a/tests/misc/tlocals.nim b/tests/misc/tlocals.nim index 3e240d3c8..09b7432f5 100644 --- a/tests/misc/tlocals.nim +++ b/tests/misc/tlocals.nim @@ -1,5 +1,5 @@ discard """ - output: "(x: string here, a: 1)" + output: '''(x: "string here", a: 1)''' """ proc simple[T](a: T) = diff --git a/tests/objects/tobjconstr.nim b/tests/objects/tobjconstr.nim index 12478f621..b7da176aa 100644 --- a/tests/objects/tobjconstr.nim +++ b/tests/objects/tobjconstr.nim @@ -1,14 +1,14 @@ discard """ - output: '''(k: kindA, a: (x: abc, z: [1, 1, 3]), method: ()) -(k: kindA, a: (x: abc, z: [1, 2, 3]), method: ()) -(k: kindA, a: (x: abc, z: [1, 3, 3]), method: ()) -(k: kindA, a: (x: abc, z: [1, 4, 3]), method: ()) -(k: kindA, a: (x: abc, z: [1, 5, 3]), method: ()) -(k: kindA, a: (x: abc, z: [1, 6, 3]), method: ()) -(k: kindA, a: (x: abc, z: [1, 7, 3]), method: ()) -(k: kindA, a: (x: abc, z: [1, 8, 3]), method: ()) -(k: kindA, a: (x: abc, z: [1, 9, 3]), method: ()) -(k: kindA, a: (x: abc, z: [1, 10, 3]), method: ()) + output: '''(k: kindA, a: (x: "abc", z: [1, 1, 3]), method: ()) +(k: kindA, a: (x: "abc", z: [1, 2, 3]), method: ()) +(k: kindA, a: (x: "abc", z: [1, 3, 3]), method: ()) +(k: kindA, a: (x: "abc", z: [1, 4, 3]), method: ()) +(k: kindA, a: (x: "abc", z: [1, 5, 3]), method: ()) +(k: kindA, a: (x: "abc", z: [1, 6, 3]), method: ()) +(k: kindA, a: (x: "abc", z: [1, 7, 3]), method: ()) +(k: kindA, a: (x: "abc", z: [1, 8, 3]), method: ()) +(k: kindA, a: (x: "abc", z: [1, 9, 3]), method: ()) +(k: kindA, a: (x: "abc", z: [1, 10, 3]), method: ()) (x: 123) (x: 123) (z: 89, y: 0, x: 128) diff --git a/tests/showoff/thello2.nim b/tests/showoff/thello2.nim index d2e2f6227..3ccb4e3be 100644 --- a/tests/showoff/thello2.nim +++ b/tests/showoff/thello2.nim @@ -1,5 +1,5 @@ discard """ - output: '''(a: 3, b: 4, s: abc)''' + output: '''(a: 3, b: 4, s: "abc")''' """ type diff --git a/tests/stdlib/tlists.nim b/tests/stdlib/tlists.nim index 4caa05c90..37e73c53f 100644 --- a/tests/stdlib/tlists.nim +++ b/tests/stdlib/tlists.nim @@ -17,7 +17,7 @@ block SinglyLinkedListTest1: block SinglyLinkedListTest2: var L: TSinglyLinkedList[string] for d in items(data): L.prepend($d) - assert($L == "[6, 5, 4, 3, 2, 1]") + assert($L == """["6", "5", "4", "3", "2", "1"]""") assert("4" in L) diff --git a/tests/stdlib/treloop.nim b/tests/stdlib/treloop.nim index 35236708c..b4221525d 100644 --- a/tests/stdlib/treloop.nim +++ b/tests/stdlib/treloop.nim @@ -1,5 +1,5 @@ discard """ - output: "@[(, +, 1, 2, )]" + output: '''@["(", "+", " 1", " 2", ")"]''' """ import re diff --git a/tests/system/toString.nim b/tests/system/toString.nim index 3e7fc7ddb..ea9d6b05b 100644 --- a/tests/system/toString.nim +++ b/tests/system/toString.nim @@ -4,12 +4,12 @@ discard """ doAssert "@[23, 45]" == $(@[23, 45]) doAssert "[32, 45]" == $([32, 45]) -doAssert "@[, foo, bar]" == $(@["", "foo", "bar"]) -doAssert "[, foo, bar]" == $(["", "foo", "bar"]) +doAssert """@["", "foo", "bar"]""" == $(@["", "foo", "bar"]) +doAssert """["", "foo", "bar"]""" == $(["", "foo", "bar"]) # bug #2395 let alphaSet: set[char] = {'a'..'c'} -doAssert "{a, b, c}" == $alphaSet +doAssert "{'a', 'b', 'c'}" == $alphaSet doAssert "2.3242" == $(2.3242) doAssert "2.982" == $(2.982) doAssert "123912.1" == $(123912.1) @@ -49,5 +49,5 @@ import strutils # array test let arr = ['H','e','l','l','o',' ','W','o','r','l','d','!','\0'] -doAssert $arr == "[H, e, l, l, o, , W, o, r, l, d, !, \0]" +doAssert $arr == "['H', 'e', 'l', 'l', 'o', ' ', 'W', 'o', 'r', 'l', 'd', '!', '\\x00']" doAssert $cstring(unsafeAddr arr) == "Hello World!" diff --git a/tests/typerel/typeof_in_template.nim b/tests/typerel/typeof_in_template.nim index 9ec06f2e3..3724cc994 100644 --- a/tests/typerel/typeof_in_template.nim +++ b/tests/typerel/typeof_in_template.nim @@ -1,5 +1,5 @@ discard """ - output: '''@[a, c]''' + output: '''@["a", "c"]''' """ # bug #3230 diff --git a/tests/types/tinheritpartialgeneric.nim b/tests/types/tinheritpartialgeneric.nim index a00df26fa..1845778bf 100644 --- a/tests/types/tinheritpartialgeneric.nim +++ b/tests/types/tinheritpartialgeneric.nim @@ -1,6 +1,6 @@ discard """ - output: '''(c: hello, a: 10, b: 12.0) -(a: 15.5, b: hello) + output: '''(c: "hello", a: 10, b: 12.0) +(a: 15.5, b: "hello") (a: 11.75, b: 123)''' """ diff --git a/tests/types/tparameterizedparent2.nim b/tests/types/tparameterizedparent2.nim index 999db2ac5..e96b9edbe 100644 --- a/tests/types/tparameterizedparent2.nim +++ b/tests/types/tparameterizedparent2.nim @@ -2,7 +2,7 @@ discard """ output: '''(width: 11, color: 13) (width: 15, weight: 13, taste: 11, color: 14) (width: 17, color: 16) -(width: 12.0, taste: yummy, color: 13) +(width: 12.0, taste: "yummy", color: 13) (width: 0, tast_e: 0.0, kind: Smooth, skin: 1.5, color: 12)''' """ # bug #5264 diff --git a/tests/vm/tableinstatic.nim b/tests/vm/tableinstatic.nim index 54e7c11f0..b0d24b477 100644 --- a/tests/vm/tableinstatic.nim +++ b/tests/vm/tableinstatic.nim @@ -2,7 +2,7 @@ discard """ nimout: '''0 0 0 -{hallo: 123, welt: 456}''' +{"hallo": "123", "welt": "456"}''' """ import tables diff --git a/tests/vm/tconstobj.nim b/tests/vm/tconstobj.nim index 51f30fb78..38fcdd844 100644 --- a/tests/vm/tconstobj.nim +++ b/tests/vm/tconstobj.nim @@ -1,5 +1,5 @@ discard """ - output: '''(name: hello) + output: '''(name: "hello") (-1, 0)''' """ -- cgit 1.4.1-2-gfad0 From c6b33de127ada9d715c16c7215f88cde7bb5a0c6 Mon Sep 17 00:00:00 2001 From: treeform Date: Wed, 13 Dec 2017 23:32:54 +0000 Subject: fix --- lib/pure/parsesql.nim | 415 ++++++++++++++++++++++++++---------------- tests/stdlib/tparsesql.nim | 438 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 694 insertions(+), 159 deletions(-) create mode 100644 tests/stdlib/tparsesql.nim (limited to 'tests/stdlib') diff --git a/lib/pure/parsesql.nim b/lib/pure/parsesql.nim index 6891e2ff7..b53f46f82 100644 --- a/lib/pure/parsesql.nim +++ b/lib/pure/parsesql.nim @@ -462,27 +462,27 @@ proc errorStr(L: SqlLexer, msg: string): string = # ----------------------------- parser ---------------------------------------- -# Operator/Element Associativity Description -# . left table/column name separator -# :: left PostgreSQL-style typecast -# [ ] left array element selection -# - right unary minus -# ^ left exponentiation -# * / % left multiplication, division, modulo -# + - left addition, subtraction -# IS IS TRUE, IS FALSE, IS UNKNOWN, IS NULL -# ISNULL test for null -# NOTNULL test for not null -# (any other) left all other native and user-defined oprs -# IN set membership -# BETWEEN range containment -# OVERLAPS time interval overlap -# LIKE ILIKE SIMILAR string pattern matching -# < > less than, greater than -# = right equality, assignment -# NOT right logical negation -# AND left logical conjunction -# OR left logical disjunction +# Operator/Element Associativity Description +# . left table/column name separator +# :: left PostgreSQL-style typecast +# [ ] left array element selection +# - right unary minus +# ^ left exponentiation +# * / % left multiplication, division, modulo +# + - left addition, subtraction +# IS IS TRUE, IS FALSE, IS UNKNOWN, IS NULL +# ISNULL test for null +# NOTNULL test for not null +# (any other) left all other native and user-defined oprs +# IN set membership +# BETWEEN range containment +# OVERLAPS time interval overlap +# LIKE ILIKE SIMILAR string pattern matching +# < > less than, greater than +# = right equality, assignment +# NOT right logical negation +# AND left logical conjunction +# OR left logical disjunction type SqlNodeKind* = enum ## kind of SQL abstract syntax tree @@ -518,11 +518,15 @@ type nkSelect, nkSelectDistinct, nkSelectColumns, + nkSelectPair, nkAsgn, nkFrom, + nkFromItemPair, nkGroup, + nkLimit, nkHaving, nkOrder, + nkJoin, nkDesc, nkUnion, nkIntersect, @@ -670,6 +674,7 @@ proc getPrecedence(p: SqlParser): int = result = - 1 proc parseExpr(p: var SqlParser): SqlNode +proc parseSelect(p: var SqlParser): SqlNode proc identOrLiteral(p: var SqlParser): SqlNode = case p.tok.kind @@ -921,6 +926,19 @@ proc parseWhere(p: var SqlParser): SqlNode = result = newNode(nkWhere) result.add(parseExpr(p)) +proc parseFromItem(p: var SqlParser): SqlNode = + result = newNode(nkFromItemPair) + if p.tok.kind == tkParLe: + getTok(p) + var select = parseSelect(p) + result.add(select) + eat(p, tkParRi) + else: + result.add(parseExpr(p)) + if isKeyw(p, "as"): + getTok(p) + result.add(parseExpr(p)) + proc parseIndexDef(p: var SqlParser): SqlNode = result = parseIfNotExists(p, nkCreateIndex) if isKeyw(p, "primary"): @@ -1019,7 +1037,12 @@ proc parseSelect(p: var SqlParser): SqlNode = a.add(newNode(nkIdent, "*")) getTok(p) else: - a.add(parseExpr(p)) + var pair = newNode(nkSelectPair) + pair.add(parseExpr(p)) + a.add(pair) + if isKeyw(p, "as"): + getTok(p) + pair.add(parseExpr(p)) if p.tok.kind != tkComma: break getTok(p) result.add(a) @@ -1027,7 +1050,7 @@ proc parseSelect(p: var SqlParser): SqlNode = var f = newNode(nkFrom) while true: getTok(p) - f.add(parseExpr(p)) + f.add(parseFromItem(p)) if p.tok.kind != tkComma: break result.add(f) if isKeyw(p, "where"): @@ -1041,6 +1064,11 @@ proc parseSelect(p: var SqlParser): SqlNode = if p.tok.kind != tkComma: break getTok(p) result.add(g) + if isKeyw(p, "limit"): + getTok(p) + var l = newNode(nkLimit) + l.add(parseExpr(p)) + result.add(l) if isKeyw(p, "having"): var h = newNode(nkHaving) while true: @@ -1073,6 +1101,18 @@ proc parseSelect(p: var SqlParser): SqlNode = if p.tok.kind != tkComma: break getTok(p) result.add(n) + if isKeyw(p, "join") or isKeyw(p, "inner") or isKeyw(p, "outer") or isKeyw(p, "cross"): + var join = newNode(nkJoin) + result.add(join) + if isKeyw(p, "join"): + join.add(newNode(nkIdent, "")) + getTok(p) + else: + join.add(parseExpr(p)) + eat(p, "join") + join.add(parseFromItem(p)) + eat(p, "on") + join.add(parseExpr(p)) proc parseStmt(p: var SqlParser; parent: SqlNode) = if isKeyw(p, "create"): @@ -1104,7 +1144,7 @@ proc parseStmt(p: var SqlParser; parent: SqlNode) = elif isKeyw(p, "begin"): getTok(p) else: - sqlError(p, "CREATE expected") + sqlError(p, "SELECT, CREATE, UPDATE or DELETE expected") proc open(p: var SqlParser, input: Stream, filename: string) = ## opens the parser `p` and assigns the input stream `input` to it. @@ -1120,6 +1160,8 @@ proc parse(p: var SqlParser): SqlNode = result = newNode(nkStmtList) while p.tok.kind != tkEof: parseStmt(p, result) + if p.tok.kind == tkEof: + break eat(p, tkSemicolon) if result.len == 1: result = result.sons[0] @@ -1139,19 +1181,69 @@ proc parseSQL*(input: Stream, filename: string): SqlNode = finally: close(p) -proc ra(n: SqlNode, s: var string, indent: int) +proc parseSQL*(input: string, filename=""): SqlNode = + ## parses the SQL from `input` into an AST and returns the AST. + ## `filename` is only used for error messages. + ## Syntax errors raise an `EInvalidSql` exception. + parseSQL(newStringStream(input), "") -proc rs(n: SqlNode, s: var string, indent: int, - prefix = "(", suffix = ")", - sep = ", ") = + +type + SqlWriter = object + indent: int + upperCase: bool + buffer: string + +proc add(s: var SqlWriter, thing: string) = + s.buffer.add(thing) + +proc add(s: var SqlWriter, thing: char) = + s.buffer.add(thing) + +proc addKeyw(s: var SqlWriter, thing: string) = + if s.buffer.len > 0 and s.buffer[^1] notin " ,\L(": + s.buffer.add(" ") + if s.upperCase: + s.buffer.add(thing.toUpper()) + else: + s.buffer.add(thing) + s.buffer.add(" ") + +proc rm(s: var SqlWriter, chars = " \L,") = + while s.buffer[^1] in chars: + s.buffer = s.buffer[0..^2] + +proc newLine(s: var SqlWriter) = + s.rm(" \L") + s.buffer.add("\L") + for i in 0.. 0: s.add(prefix) for i in 0 .. n.len-1: if i > 0: s.add(sep) - ra(n.sons[i], s, indent) + ra(n.sons[i], s) s.add(suffix) -proc ra(n: SqlNode, s: var string, indent: int) = +proc ra(n: SqlNode, s: var SqlWriter) = if n == nil: return case n.kind of nkNone: discard @@ -1169,217 +1261,222 @@ proc ra(n: SqlNode, s: var string, indent: int) = of nkIntegerLit, nkNumericLit: s.add(n.strVal) of nkPrimaryKey: - s.add(" primary key") - rs(n, s, indent) + s.addKeyw("primary key") + rs(n, s) of nkForeignKey: - s.add(" foreign key") - rs(n, s, indent) + s.addKeyw("foreign key") + rs(n, s) of nkNotNull: - s.add(" not null") + s.addKeyw("not null") of nkNull: - s.add(" null") + s.addKeyw("null") of nkDot: - ra(n.sons[0], s, indent) + ra(n.sons[0], s) s.add(".") - ra(n.sons[1], s, indent) + ra(n.sons[1], s) of nkDotDot: - ra(n.sons[0], s, indent) + ra(n.sons[0], s) s.add(". .") - ra(n.sons[1], s, indent) + ra(n.sons[1], s) of nkPrefix: s.add('(') - ra(n.sons[0], s, indent) + ra(n.sons[0], s) s.add(' ') - ra(n.sons[1], s, indent) + ra(n.sons[1], s) s.add(')') of nkInfix: s.add('(') - ra(n.sons[1], s, indent) + ra(n.sons[1], s) s.add(' ') - ra(n.sons[0], s, indent) + ra(n.sons[0], s) s.add(' ') - ra(n.sons[2], s, indent) + ra(n.sons[2], s) s.add(')') of nkCall, nkColumnReference: - ra(n.sons[0], s, indent) + ra(n.sons[0], s) s.add('(') for i in 1..n.len-1: if i > 1: s.add(", ") - ra(n.sons[i], s, indent) + ra(n.sons[i], s) s.add(')') of nkReferences: - s.add(" references ") - ra(n.sons[0], s, indent) + s.addKeyw("references") + ra(n.sons[0], s) of nkDefault: - s.add(" default ") - ra(n.sons[0], s, indent) + s.addKeyw("default") + ra(n.sons[0], s) of nkCheck: - s.add(" check ") - ra(n.sons[0], s, indent) + s.addKeyw("check") + ra(n.sons[0], s) of nkConstraint: - s.add(" constraint ") - ra(n.sons[0], s, indent) - s.add(" check ") - ra(n.sons[1], s, indent) + s.addKeyw("constraint") + ra(n.sons[0], s) + s.addKeyw("check") + ra(n.sons[1], s) of nkUnique: - s.add(" unique") - rs(n, s, indent) + s.addKeyw("unique") + rs(n, s) of nkIdentity: - s.add(" identity") + s.addKeyw("identity") of nkColumnDef: s.add("\n ") - rs(n, s, indent, "", "", " ") + rs(n, s, "", "", " ") of nkStmtList: for i in 0..n.len-1: - ra(n.sons[i], s, indent) + ra(n.sons[i], s) s.add("\n") of nkInsert: assert n.len == 3 - s.add("insert into ") - ra(n.sons[0], s, indent) - ra(n.sons[1], s, indent) + s.addKeyw("insert into") + ra(n.sons[0], s) + ra(n.sons[1], s) if n.sons[2].kind == nkDefault: - s.add("default values") + s.addKeyw("default values") else: - s.add("\n") - ra(n.sons[2], s, indent) + s.newLine() + ra(n.sons[2], s) s.add(';') of nkUpdate: - s.add("update ") - ra(n.sons[0], s, indent) - s.add(" set ") + s.addKeyw("update") + ra(n.sons[0], s) + s.addKeyw("set") var L = n.len for i in 1 .. L-2: if i > 1: s.add(", ") var it = n.sons[i] assert it.kind == nkAsgn - ra(it, s, indent) - ra(n.sons[L-1], s, indent) + ra(it, s) + ra(n.sons[L-1], s) s.add(';') of nkDelete: - s.add("delete from ") - ra(n.sons[0], s, indent) - ra(n.sons[1], s, indent) + s.addKeyw("delete from") + ra(n.sons[0], s) + ra(n.sons[1], s) s.add(';') of nkSelect, nkSelectDistinct: - s.add("select ") + s.addKeyw("select") if n.kind == nkSelectDistinct: - s.add("distinct ") - rs(n.sons[0], s, indent, "", "", ", ") - for i in 1 .. n.len-1: ra(n.sons[i], s, indent) + s.addKeyw("distinct") + s.inner: + for son in n.sons[0].sons: + ra(son, s) + s.add(',') + s.newLine() + s.rm() + for i in 1 .. n.len-1: + ra(n.sons[i], s) s.add(';') of nkSelectColumns: assert(false) + of nkSelectPair: + ra(n.sons[0], s) + if n.sons.len == 2: + s.addKeyw("as") + ra(n.sons[1], s) + of nkFromItemPair: + if n.sons[0].kind == nkIdent: + ra(n.sons[0], s) + else: + assert n.sons[0].kind == nkSelect + s.add("(") + s.inner: + ra(n.sons[0], s) + s.rm("; \L") + s.newLine() + s.add(")") + if n.sons.len == 2: + s.addKeyw("as") + ra(n.sons[1], s) of nkAsgn: - ra(n.sons[0], s, indent) + ra(n.sons[0], s) s.add(" = ") - ra(n.sons[1], s, indent) + ra(n.sons[1], s) of nkFrom: - s.add("\nfrom ") - rs(n, s, indent, "", "", ", ") + s.innerKeyw("from"): + rs(n, s, "", "", ", ") of nkGroup: - s.add("\ngroup by") - rs(n, s, indent, "", "", ", ") + s.innerKeyw("group by"): + rs(n, s, "", "", ", ") + of nkLimit: + s.innerKeyw("limit"): + rs(n, s, "", "", ", ") of nkHaving: - s.add("\nhaving") - rs(n, s, indent, "", "", ", ") + s.innerKeyw("having"): + rs(n, s, "", "", ", ") of nkOrder: - s.add("\norder by ") - rs(n, s, indent, "", "", ", ") + s.addKeyw("order by") + rs(n, s, "", "", ", ") + of nkJoin: + var joinType = n.sons[0].strVal + if joinType == "": + joinType = "join" + else: + joinType &= " " & "join" + s.innerKeyw(joinType): + ra(n.sons[1], s) + s.innerKeyw("on"): + ra(n.sons[2], s) of nkDesc: - ra(n.sons[0], s, indent) - s.add(" desc") + ra(n.sons[0], s) + s.addKeyw("desc") of nkUnion: - s.add(" union") + s.addKeyw("union") of nkIntersect: - s.add(" intersect") + s.addKeyw("intersect") of nkExcept: - s.add(" except") + s.addKeyw("except") of nkColumnList: - rs(n, s, indent) + rs(n, s) of nkValueList: - s.add("values ") - rs(n, s, indent) + s.addKeyw("values") + rs(n, s) of nkWhere: - s.add("\nwhere ") - ra(n.sons[0], s, indent) + s.newLine() + s.addKeyw("where") + s.inner: + ra(n.sons[0], s) of nkCreateTable, nkCreateTableIfNotExists: - s.add("create table ") + s.addKeyw("create table") if n.kind == nkCreateTableIfNotExists: - s.add("if not exists ") - ra(n.sons[0], s, indent) + s.addKeyw("if not exists") + ra(n.sons[0], s) s.add('(') for i in 1..n.len-1: - if i > 1: s.add(", ") - ra(n.sons[i], s, indent) + if i > 1: s.add(",") + ra(n.sons[i], s) s.add(");") of nkCreateType, nkCreateTypeIfNotExists: - s.add("create type ") + s.addKeyw("create type") if n.kind == nkCreateTypeIfNotExists: - s.add("if not exists ") - ra(n.sons[0], s, indent) - s.add(" as ") - ra(n.sons[1], s, indent) + s.addKeyw("if not exists") + ra(n.sons[0], s) + s.addKeyw("as") + ra(n.sons[1], s) s.add(';') of nkCreateIndex, nkCreateIndexIfNotExists: - s.add("create index ") + s.addKeyw("create index") if n.kind == nkCreateIndexIfNotExists: - s.add("if not exists ") - ra(n.sons[0], s, indent) - s.add(" on ") - ra(n.sons[1], s, indent) + s.addKeyw("if not exists") + ra(n.sons[0], s) + s.addKeyw("on") + ra(n.sons[1], s) s.add('(') for i in 2..n.len-1: if i > 2: s.add(", ") - ra(n.sons[i], s, indent) + ra(n.sons[i], s) s.add(");") of nkEnumDef: - s.add("enum ") - rs(n, s, indent) + s.addKeyw("enum") + rs(n, s) -# What I want: -# -#select(columns = [T1.all, T2.name], -# fromm = [T1, T2], -# where = T1.name ==. T2.name, -# orderby = [name]): -# -#for row in dbQuery(db, """select x, y, z -# from a, b -# where a.name = b.name"""): -# - -#select x, y, z: -# fromm: Table1, Table2 -# where: x.name == y.name -#db.select(fromm = [t1, t2], where = t1.name == t2.name): -#for x, y, z in db.select(fromm = a, b where = a.name == b.name): -# writeLine x, y, z - -proc renderSQL*(n: SqlNode): string = +proc renderSQL*(n: SqlNode, upperCase=false): string = ## Converts an SQL abstract syntax tree to its string representation. - result = "" - ra(n, result, 0) + var s: SqlWriter + s.buffer = "" + s.upperCase = upperCase + ra(n, s) + return s.buffer proc `$`*(n: SqlNode): string = ## an alias for `renderSQL`. renderSQL(n) - -when not defined(testing) and isMainModule: - echo(renderSQL(parseSQL(newStringStream(""" - CREATE TYPE happiness AS ENUM ('happy', 'very happy', 'ecstatic'); - CREATE TABLE holidays ( - num_weeks int, - happiness happiness - ); - CREATE INDEX table1_attr1 ON table1(attr1); - - SELECT * FROM myTab WHERE col1 = 'happy'; - """), "stdin"))) - -# CREATE TYPE happiness AS ENUM ('happy', 'very happy', 'ecstatic'); -# CREATE TABLE holidays ( -# num_weeks int, -# happiness happiness -# ); -# CREATE INDEX table1_attr1 ON table1(attr1) diff --git a/tests/stdlib/tparsesql.nim b/tests/stdlib/tparsesql.nim new file mode 100644 index 000000000..e73a1d7ee --- /dev/null +++ b/tests/stdlib/tparsesql.nim @@ -0,0 +1,438 @@ +import unittest + +import sequtils +import strutils +import parsesql + +proc fold(str: string): string = + var + lines = str.split("\L") + minCount = 1000 + while lines.len > 0 and lines[0].strip().len == 0: + lines.delete(0, 0) + while lines.len > 0 and lines[lines.len-1].strip().len == 0: + lines.delete(lines.len, lines.len) + for line in lines: + var count = 0 + while line[count] == ' ': + inc count + if minCount > count: + minCount = count + for i, line in lines: + lines[i] = line[minCount..^1] + return lines.join("\L") + +proc parseCheck(have: string, need: string) = + var + sql = parseSQL(have) + sqlHave = renderSQL(sql, true).strip() + sqlNeed = need.fold().strip() + var + haveLines = sqlHave.split("\L") + needLines = sqlNeed.split("\L") + for i in 0.. Date: Wed, 13 Dec 2017 23:34:44 +0000 Subject: fix --- lib/pure/parsesql.nim | 41 ++- tests/stdlib/tparsesql.nim | 808 +++++++++++++++++++++------------------------ 2 files changed, 397 insertions(+), 452 deletions(-) (limited to 'tests/stdlib') diff --git a/lib/pure/parsesql.nim b/lib/pure/parsesql.nim index b53f46f82..f266beef7 100644 --- a/lib/pure/parsesql.nim +++ b/lib/pure/parsesql.nim @@ -662,10 +662,12 @@ proc getPrecedence(p: SqlParser): int = elif isOpr(p, "=") or isOpr(p, "<") or isOpr(p, ">") or isOpr(p, ">=") or isOpr(p, "<=") or isOpr(p, "<>") or isOpr(p, "!=") or isKeyw(p, "is") or isKeyw(p, "like"): - result = 3 + result = 4 elif isKeyw(p, "and"): - result = 2 + result = 3 elif isKeyw(p, "or"): + result = 2 + elif isKeyw(p, "between"): result = 1 elif p.tok.kind == tkOperator: # user-defined operator: @@ -1015,6 +1017,8 @@ proc parseUpdate(p: var SqlParser): SqlNode = proc parseDelete(p: var SqlParser): SqlNode = getTok(p) + if isOpr(p, "*"): + getTok(p) result = newNode(nkDelete) eat(p, "from") result.add(primary(p)) @@ -1156,7 +1160,7 @@ proc open(p: var SqlParser, input: Stream, filename: string) = proc parse(p: var SqlParser): SqlNode = ## parses the content of `p`'s input stream and returns the SQL AST. - ## Syntax errors raise an `EInvalidSql` exception. + ## Syntax errors raise an `SqlParseError` exception. result = newNode(nkStmtList) while p.tok.kind != tkEof: parseStmt(p, result) @@ -1173,7 +1177,7 @@ proc close(p: var SqlParser) = proc parseSQL*(input: Stream, filename: string): SqlNode = ## parses the SQL from `input` into an AST and returns the AST. ## `filename` is only used for error messages. - ## Syntax errors raise an `EInvalidSql` exception. + ## Syntax errors raise an `SqlParseError` exception. var p: SqlParser open(p, input, filename) try: @@ -1184,7 +1188,7 @@ proc parseSQL*(input: Stream, filename: string): SqlNode = proc parseSQL*(input: string, filename=""): SqlNode = ## parses the SQL from `input` into an AST and returns the AST. ## `filename` is only used for error messages. - ## Syntax errors raise an `EInvalidSql` exception. + ## Syntax errors raise an `SqlParseError` exception. parseSQL(newStringStream(input), "") @@ -1210,7 +1214,7 @@ proc addKeyw(s: var SqlWriter, thing: string) = s.buffer.add(" ") proc rm(s: var SqlWriter, chars = " \L,") = - while s.buffer[^1] in chars: + while s.buffer.len > 0 and s.buffer[^1] in chars: s.buffer = s.buffer[0..^2] proc newLine(s: var SqlWriter) = @@ -1253,6 +1257,7 @@ proc ra(n: SqlNode, s: var SqlWriter) = else: s.add("\"" & replace(n.strVal, "\"", "\"\"") & "\"") of nkStringLit: + # TODO add e'' as an option? s.add(escape(n.strVal, "'", "'")) of nkBitStringLit: s.add("b'" & n.strVal & "'") @@ -1329,23 +1334,25 @@ proc ra(n: SqlNode, s: var SqlWriter) = assert n.len == 3 s.addKeyw("insert into") ra(n.sons[0], s) + s.add(" ") ra(n.sons[1], s) if n.sons[2].kind == nkDefault: s.addKeyw("default values") else: s.newLine() ra(n.sons[2], s) + s.rm(" ") s.add(';') of nkUpdate: - s.addKeyw("update") - ra(n.sons[0], s) - s.addKeyw("set") - var L = n.len - for i in 1 .. L-2: - if i > 1: s.add(", ") - var it = n.sons[i] - assert it.kind == nkAsgn - ra(it, s) + s.innerKeyw("update"): + ra(n.sons[0], s) + s.innerKeyw("set"): + var L = n.len + for i in 1 .. L-2: + if i > 1: s.add(", ") + var it = n.sons[i] + assert it.kind == nkAsgn + ra(it, s) ra(n.sons[L-1], s) s.add(';') of nkDelete: @@ -1404,8 +1411,8 @@ proc ra(n: SqlNode, s: var SqlWriter) = s.innerKeyw("having"): rs(n, s, "", "", ", ") of nkOrder: - s.addKeyw("order by") - rs(n, s, "", "", ", ") + s.innerKeyw("order by"): + rs(n, s, "", "", ", ") of nkJoin: var joinType = n.sons[0].strVal if joinType == "": diff --git a/tests/stdlib/tparsesql.nim b/tests/stdlib/tparsesql.nim index e73a1d7ee..fe64d3416 100644 --- a/tests/stdlib/tparsesql.nim +++ b/tests/stdlib/tparsesql.nim @@ -1,438 +1,376 @@ -import unittest - -import sequtils -import strutils -import parsesql - -proc fold(str: string): string = - var - lines = str.split("\L") - minCount = 1000 - while lines.len > 0 and lines[0].strip().len == 0: - lines.delete(0, 0) - while lines.len > 0 and lines[lines.len-1].strip().len == 0: - lines.delete(lines.len, lines.len) - for line in lines: - var count = 0 - while line[count] == ' ': - inc count - if minCount > count: - minCount = count - for i, line in lines: - lines[i] = line[minCount..^1] - return lines.join("\L") - -proc parseCheck(have: string, need: string) = - var - sql = parseSQL(have) - sqlHave = renderSQL(sql, true).strip() - sqlNeed = need.fold().strip() - var - haveLines = sqlHave.split("\L") - needLines = sqlNeed.split("\L") - for i in 0.. Date: Thu, 14 Dec 2017 18:31:43 +0000 Subject: fix --- lib/pure/parsesql.nim | 185 ++++++++++----------- tests/stdlib/tparsesql.nim | 407 +++++++++++++-------------------------------- 2 files changed, 196 insertions(+), 396 deletions(-) (limited to 'tests/stdlib') diff --git a/lib/pure/parsesql.nim b/lib/pure/parsesql.nim index f266beef7..ae192ab9a 100644 --- a/lib/pure/parsesql.nim +++ b/lib/pure/parsesql.nim @@ -55,6 +55,13 @@ const ";", ":", ",", "(", ")", "[", "]", "." ] + reservedKeywords = @[ + # statements + "select", "from", "where", "group", "limit", "having", + # functions + "count", + ] + proc open(L: var SqlLexer, input: Stream, filename: string) = lexbase.open(L, input) L.filename = filename @@ -274,16 +281,16 @@ proc getSymbol(c: var SqlLexer, tok: var Token) = c.bufpos = pos tok.kind = tkIdentifier -proc getQuotedIdentifier(c: var SqlLexer, tok: var Token) = +proc getQuotedIdentifier(c: var SqlLexer, tok: var Token, quote='\"') = var pos = c.bufpos + 1 var buf = c.buf tok.kind = tkQuotedIdentifier while true: var ch = buf[pos] - if ch == '\"': - if buf[pos+1] == '\"': + if ch == quote: + if buf[pos+1] == quote: inc(pos, 2) - add(tok.literal, '\"') + add(tok.literal, quote) else: inc(pos) break @@ -442,7 +449,8 @@ proc getTok(c: var SqlLexer, tok: var Token) = add(tok.literal, '.') of '0'..'9': getNumeric(c, tok) of '\'': getString(c, tok, tkStringConstant) - of '"': getQuotedIdentifier(c, tok) + of '"': getQuotedIdentifier(c, tok, '"') + of '`': getQuotedIdentifier(c, tok, '`') of lexbase.EndOfFile: tok.kind = tkEof tok.literal = "[EOF]" @@ -450,7 +458,7 @@ proc getTok(c: var SqlLexer, tok: var Token) = '\128'..'\255': getSymbol(c, tok) of '+', '-', '*', '/', '<', '>', '=', '~', '!', '@', '#', '%', - '^', '&', '|', '`', '?': + '^', '&', '|', '?': getOperator(c, tok) else: add(tok.literal, c.buf[c.bufpos]) @@ -504,6 +512,7 @@ type nkPrefix, nkInfix, nkCall, + nkPrGroup, nkColumnReference, nkReferences, nkDefault, @@ -700,7 +709,8 @@ proc identOrLiteral(p: var SqlParser): SqlNode = getTok(p) of tkParLe: getTok(p) - result = parseExpr(p) + result = newNode(nkPrGroup) + result.add(parseExpr(p)) eat(p, tkParRi) else: sqlError(p, "expression expected") @@ -752,7 +762,7 @@ proc lowestExprAux(p: var SqlParser, v: var SqlNode, limit: int): int = result = opPred while opPred > limit: node = newNode(nkInfix) - opNode = newNode(nkIdent, p.tok.literal) + opNode = newNode(nkIdent, p.tok.literal.toLower()) getTok(p) result = lowestExprAux(p, v2, opPred) node.add(opNode) @@ -1112,7 +1122,8 @@ proc parseSelect(p: var SqlParser): SqlNode = join.add(newNode(nkIdent, "")) getTok(p) else: - join.add(parseExpr(p)) + join.add(newNode(nkIdent, p.tok.literal.toLower())) + getTok(p) eat(p, "join") join.add(parseFromItem(p)) eat(p, "on") @@ -1167,8 +1178,6 @@ proc parse(p: var SqlParser): SqlNode = if p.tok.kind == tkEof: break eat(p, tkSemicolon) - if result.len == 1: - result = result.sons[0] proc close(p: var SqlParser) = ## closes the parser `p`. The associated input stream is closed too. @@ -1198,44 +1207,25 @@ type upperCase: bool buffer: string -proc add(s: var SqlWriter, thing: string) = +proc add(s: var SqlWriter, thing: char) = s.buffer.add(thing) -proc add(s: var SqlWriter, thing: char) = +proc add(s: var SqlWriter, thing: string) = + if s.buffer.len > 0 and s.buffer[^1] notin {' ', '\L', '(', '.'}: + s.buffer.add(" ") s.buffer.add(thing) proc addKeyw(s: var SqlWriter, thing: string) = - if s.buffer.len > 0 and s.buffer[^1] notin " ,\L(": - s.buffer.add(" ") + var keyw = thing if s.upperCase: - s.buffer.add(thing.toUpper()) - else: - s.buffer.add(thing) - s.buffer.add(" ") - -proc rm(s: var SqlWriter, chars = " \L,") = - while s.buffer.len > 0 and s.buffer[^1] in chars: - s.buffer = s.buffer[0..^2] - -proc newLine(s: var SqlWriter) = - s.rm(" \L") - s.buffer.add("\L") - for i in 0.. 0: + for i in 0 .. n.len-1: + if i > 0: s.add(sep) + ra(n.sons[i], s) + +proc addMulti(s: var SqlWriter, n: SqlNode, sep = ',', prefix, suffix: char) = + if n.len > 0: + s.add(prefix) + for i in 0 .. n.len-1: + if i > 0: s.add(sep) + ra(n.sons[i], s) + s.add(suffix) + proc ra(n: SqlNode, s: var SqlWriter) = if n == nil: return case n.kind of nkNone: discard of nkIdent: - if allCharsInSet(n.strVal, {'\33'..'\127'}): + if allCharsInSet(n.strVal, {'\33'..'\127'}) and n.strVal.toLower() notin reservedKeywords: s.add(n.strVal) else: s.add("\"" & replace(n.strVal, "\"", "\"\"") & "\"") of nkStringLit: - # TODO add e'' as an option? s.add(escape(n.strVal, "'", "'")) of nkBitStringLit: s.add("b'" & n.strVal & "'") @@ -1277,33 +1280,33 @@ proc ra(n: SqlNode, s: var SqlWriter) = s.addKeyw("null") of nkDot: ra(n.sons[0], s) - s.add(".") + s.add('.') ra(n.sons[1], s) of nkDotDot: ra(n.sons[0], s) s.add(". .") ra(n.sons[1], s) of nkPrefix: - s.add('(') ra(n.sons[0], s) s.add(' ') ra(n.sons[1], s) - s.add(')') of nkInfix: - s.add('(') ra(n.sons[1], s) s.add(' ') ra(n.sons[0], s) s.add(' ') ra(n.sons[2], s) - s.add(')') of nkCall, nkColumnReference: ra(n.sons[0], s) s.add('(') for i in 1..n.len-1: - if i > 1: s.add(", ") + if i > 1: s.add(',') ra(n.sons[i], s) s.add(')') + of nkPrGroup: + s.add('(') + s.addMulti(n) + s.add(')') of nkReferences: s.addKeyw("references") ra(n.sons[0], s) @@ -1324,55 +1327,43 @@ proc ra(n: SqlNode, s: var SqlWriter) = of nkIdentity: s.addKeyw("identity") of nkColumnDef: - s.add("\n ") rs(n, s, "", "", " ") of nkStmtList: for i in 0..n.len-1: ra(n.sons[i], s) - s.add("\n") + s.add(';') of nkInsert: assert n.len == 3 s.addKeyw("insert into") ra(n.sons[0], s) - s.add(" ") + s.add(' ') ra(n.sons[1], s) if n.sons[2].kind == nkDefault: s.addKeyw("default values") else: - s.newLine() ra(n.sons[2], s) - s.rm(" ") - s.add(';') of nkUpdate: - s.innerKeyw("update"): - ra(n.sons[0], s) - s.innerKeyw("set"): - var L = n.len - for i in 1 .. L-2: - if i > 1: s.add(", ") - var it = n.sons[i] - assert it.kind == nkAsgn - ra(it, s) + s.addKeyw("update") + ra(n.sons[0], s) + s.addKeyw("set") + var L = n.len + for i in 1 .. L-2: + if i > 1: s.add(", ") + var it = n.sons[i] + assert it.kind == nkAsgn + ra(it, s) ra(n.sons[L-1], s) - s.add(';') of nkDelete: s.addKeyw("delete from") ra(n.sons[0], s) ra(n.sons[1], s) - s.add(';') of nkSelect, nkSelectDistinct: s.addKeyw("select") if n.kind == nkSelectDistinct: s.addKeyw("distinct") - s.inner: - for son in n.sons[0].sons: - ra(son, s) - s.add(',') - s.newLine() - s.rm() + s.addMulti(n.sons[0]) for i in 1 .. n.len-1: ra(n.sons[i], s) - s.add(';') of nkSelectColumns: assert(false) of nkSelectPair: @@ -1385,12 +1376,9 @@ proc ra(n: SqlNode, s: var SqlWriter) = ra(n.sons[0], s) else: assert n.sons[0].kind == nkSelect - s.add("(") - s.inner: - ra(n.sons[0], s) - s.rm("; \L") - s.newLine() - s.add(")") + s.add('(') + ra(n.sons[0], s) + s.add(')') if n.sons.len == 2: s.addKeyw("as") ra(n.sons[1], s) @@ -1399,30 +1387,30 @@ proc ra(n: SqlNode, s: var SqlWriter) = s.add(" = ") ra(n.sons[1], s) of nkFrom: - s.innerKeyw("from"): - rs(n, s, "", "", ", ") + s.addKeyw("from") + s.addMulti(n) of nkGroup: - s.innerKeyw("group by"): - rs(n, s, "", "", ", ") + s.addKeyw("group by") + s.addMulti(n) of nkLimit: - s.innerKeyw("limit"): - rs(n, s, "", "", ", ") + s.addKeyw("limit") + s.addMulti(n) of nkHaving: - s.innerKeyw("having"): - rs(n, s, "", "", ", ") + s.addKeyw("having") + s.addMulti(n) of nkOrder: - s.innerKeyw("order by"): - rs(n, s, "", "", ", ") + s.addKeyw("order by") + s.addMulti(n) of nkJoin: var joinType = n.sons[0].strVal if joinType == "": joinType = "join" else: joinType &= " " & "join" - s.innerKeyw(joinType): - ra(n.sons[1], s) - s.innerKeyw("on"): - ra(n.sons[2], s) + s.addKeyw(joinType) + ra(n.sons[1], s) + s.addKeyw("on") + ra(n.sons[2], s) of nkDesc: ra(n.sons[0], s) s.addKeyw("desc") @@ -1438,10 +1426,8 @@ proc ra(n: SqlNode, s: var SqlWriter) = s.addKeyw("values") rs(n, s) of nkWhere: - s.newLine() s.addKeyw("where") - s.inner: - ra(n.sons[0], s) + ra(n.sons[0], s) of nkCreateTable, nkCreateTableIfNotExists: s.addKeyw("create table") if n.kind == nkCreateTableIfNotExists: @@ -1449,7 +1435,7 @@ proc ra(n: SqlNode, s: var SqlWriter) = ra(n.sons[0], s) s.add('(') for i in 1..n.len-1: - if i > 1: s.add(",") + if i > 1: s.add(',') ra(n.sons[i], s) s.add(");") of nkCreateType, nkCreateTypeIfNotExists: @@ -1459,7 +1445,6 @@ proc ra(n: SqlNode, s: var SqlWriter) = ra(n.sons[0], s) s.addKeyw("as") ra(n.sons[1], s) - s.add(';') of nkCreateIndex, nkCreateIndexIfNotExists: s.addKeyw("create index") if n.kind == nkCreateIndexIfNotExists: diff --git a/tests/stdlib/tparsesql.nim b/tests/stdlib/tparsesql.nim index fe64d3416..3dc949ea1 100644 --- a/tests/stdlib/tparsesql.nim +++ b/tests/stdlib/tparsesql.nim @@ -1,346 +1,149 @@ discard """ file: "tparsesql.nim" - output: '''select - foo -from - table; -select - foo -from - table; -select - foo -from - table -limit - 10; -select - foo, - bar, - baz -from - table -limit - 10; -select - foo as bar -from - table; -select - foo as foo_prime, - bar as bar_prime, - baz as baz_prime -from - table; -select - * -from - table; -select - * -from - table -where - ((a = b) and (c = d)); -select - * -from - table -where - (not b); -select - * -from - table -where - (a and (not b)); -select - * -from - table -where - (((a = b) and (c = d)) or ((n is null) and (((not b) + 1) = 3))); -select - * -from - table -having - ((a = b) and (c = d)); -select - a, - b -from - table -group by - a; -select - a, - b -from - table -group by - 1, 2; -select - t.a -from - t as t; -select - a, - b -from - ( - select - * - from - t - ); -select - a, - b -from - ( - select - * - from - t - ) as foo; -select - a, - b -from - ( - select - * - from - ( - select - * - from - ( - select - * - from - ( - select - * - from - inner as inner1 - ) as inner2 - ) as inner3 - ) as inner4 - ) as inner5; -select - a, - b -from - ( - select - * - from - a - ), ( - select - * - from - b - ), ( - select - * - from - c - ); -select - * -from - Products -where - (Price BETWEEN (10 AND 20)); -select - id -from - a -join - b -on - (a.id == b.id); -select - id -from - a -join - ( - select - id - from - c - ) as b -on - (a.id == b.id); -select - id -from - a -INNER join - b -on - (a.id == b.id); -select - id -from - a -OUTER join - b -on - (a.id == b.id); -select - id -from - a -CROSS join - b -on - (a.id == b.id); -create type happiness as enum ('happy', 'very happy', 'ecstatic'); -create table holidays( - num_weeks int, - happiness happiness); -create index table1_attr1 on table1(attr1); -select - * -from - myTab -where - (col1 = 'happy'); - -insert into Customers (CustomerName, ContactName, Address, City, PostalCode, Country) -values ('Cardinal', 'Tom B. Erichsen', 'Skagen 21', 'Stavanger', '4006', 'Norway'); -insert into TableName default values; - -update - Customers -set - ContactName = 'Alfred Schmidt', City = 'Frankfurt' -where - (CustomerID = 1); -delete from table_name; -delete from table_name; -select - * -from - Customers; -select - * -from - Customers -where - ((((CustomerName LIKE 'L%') OR (CustomerName LIKE 'R%')) OR (CustomerName LIKE 'W%')) AND (Country = 'USA')) -order by - CustomerName; - -''' """ import parsesql -echo $parseSQL "SELECT foo FROM table;" -echo $parseSQL "SELECT foo FROM table" -echo $parseSQL "SELECT foo FROM table limit 10" -echo $parseSQL "SELECT foo, bar, baz FROM table limit 10" -echo $parseSQL "SELECT foo AS bar FROM table" -echo $parseSQL "SELECT foo AS foo_prime, bar AS bar_prime, baz AS baz_prime FROM table" -echo $parseSQL "SELECT * FROM table" +doAssert $parseSQL("SELECT foo FROM table;") == "select foo from table;" +doAssert $parseSQL(""" +SELECT + CustomerName, + ContactName, + Address, + City, + PostalCode, + Country, + CustomerName, + ContactName, + Address, + City, + PostalCode, + Country +FROM table;""") == "select CustomerName, ContactName, Address, City, PostalCode, Country, CustomerName, ContactName, Address, City, PostalCode, Country from table;" + +doAssert $parseSQL("SELECT foo FROM table limit 10") == "select foo from table limit 10;" +doAssert $parseSQL("SELECT foo, bar, baz FROM table limit 10") == "select foo, bar, baz from table limit 10;" +doAssert $parseSQL("SELECT foo AS bar FROM table") == "select foo as bar from table;" +doAssert $parseSQL("SELECT foo AS foo_prime, bar AS bar_prime, baz AS baz_prime FROM table") == "select foo as foo_prime, bar as bar_prime, baz as baz_prime from table;" +doAssert $parseSQL("SELECT * FROM table") == "select * from table;" + + #TODO add count(*) -#echo $parseSQL "SELECT COUNT(*) FROM table" -echo $parseSQL """ +#doAssert $parseSQL("SELECT COUNT(*) FROM table" + +doAssert $parseSQL(""" SELECT * FROM table WHERE a = b and c = d -""" -echo $parseSQL """ +""") == "select * from table where a = b and c = d;" + +doAssert $parseSQL(""" SELECT * FROM table WHERE not b -""" -echo $parseSQL """ +""") == "select * from table where not b;" + +doAssert $parseSQL(""" SELECT * FROM table WHERE a and not b -""" -echo $parseSQL """ +""") == "select * from table where a and not b;" + +doAssert $parseSQL(""" SELECT * FROM table WHERE a = b and c = d or n is null and not b + 1 = 3 -""" -echo $parseSQL """ +""") == "select * from table where a = b and c = d or n is null and not b + 1 = 3;" + +doAssert $parseSQL(""" +SELECT * FROM table +WHERE (a = b and c = d) or (n is null and not b + 1 = 3) +""") == "select * from table where(a = b and c = d) or (n is null and not b + 1 = 3);" + +doAssert $parseSQL(""" SELECT * FROM table HAVING a = b and c = d -""" -echo $parseSQL """ +""") == "select * from table having a = b and c = d;" + +doAssert $parseSQL(""" SELECT a, b FROM table GROUP BY a -""" -echo $parseSQL """ +""") == "select a, b from table group by a;" + +doAssert $parseSQL(""" SELECT a, b FROM table GROUP BY 1, 2 -""" -echo $parseSQL "SELECT t.a FROM t as t" -echo $parseSQL """ +""") == "select a, b from table group by 1, 2;" + +doAssert $parseSQL("SELECT t.a FROM t as t") == "select t.a from t as t;" + +doAssert $parseSQL(""" SELECT a, b FROM ( SELECT * FROM t ) -""" -echo $parseSQL """ +""") == "select a, b from(select * from t);" + +doAssert $parseSQL(""" SELECT a, b FROM ( SELECT * FROM t ) as foo -""" -echo $parseSQL """ +""") == "select a, b from(select * from t) as foo;" + +doAssert $parseSQL(""" SELECT a, b FROM ( SELECT * FROM ( SELECT * FROM ( SELECT * FROM ( - SELECT * FROM inner as inner1 + SELECT * FROM innerTable as inner1 ) as inner2 ) as inner3 ) as inner4 ) as inner5 -""" -echo $parseSQL """ +""") == "select a, b from(select * from(select * from(select * from(select * from innerTable as inner1) as inner2) as inner3) as inner4) as inner5;" + +doAssert $parseSQL(""" SELECT a, b FROM (SELECT * FROM a), (SELECT * FROM b), (SELECT * FROM c) -""" -echo $parseSQL """ +""") == "select a, b from(select * from a),(select * from b),(select * from c);" + +doAssert $parseSQL(""" SELECT * FROM Products WHERE Price BETWEEN 10 AND 20; -""" -echo $parseSQL """ +""") == "select * from Products where Price between 10 and 20;" + +doAssert $parseSQL(""" SELECT id FROM a JOIN b ON a.id == b.id -""" -echo $parseSQL """ +""") == "select id from a join b on a.id == b.id;" + +doAssert $parseSQL(""" SELECT id FROM a JOIN (SELECT id from c) as b ON a.id == b.id -""" -echo $parseSQL """ +""") == "select id from a join(select id from c) as b on a.id == b.id;" + +doAssert $parseSQL(""" SELECT id FROM a INNER JOIN b ON a.id == b.id -""" -echo $parseSQL """ +""") == "select id from a inner join b on a.id == b.id;" + +doAssert $parseSQL(""" SELECT id FROM a OUTER JOIN b ON a.id == b.id -""" -echo $parseSQL """ +""") == "select id from a outer join b on a.id == b.id;" + +doAssert $parseSQL(""" SELECT id FROM a CROSS JOIN b ON a.id == b.id -""" -echo $parseSQL """ +""") == "select id from a cross join b on a.id == b.id;" + +doAssert $parseSQL(""" CREATE TYPE happiness AS ENUM ('happy', 'very happy', 'ecstatic'); CREATE TABLE holidays ( num_weeks int, @@ -348,29 +151,41 @@ CREATE TABLE holidays ( ); CREATE INDEX table1_attr1 ON table1(attr1); SELECT * FROM myTab WHERE col1 = 'happy'; -""" -echo $parseSQL """ +""") == "create type happiness as enum ('happy' , 'very happy' , 'ecstatic' ); create table holidays(num_weeks int , happiness happiness );; create index table1_attr1 on table1(attr1 );; select * from myTab where col1 = 'happy';" + +doAssert $parseSQL(""" INSERT INTO Customers (CustomerName, ContactName, Address, City, PostalCode, Country) VALUES ('Cardinal', 'Tom B. Erichsen', 'Skagen 21', 'Stavanger', '4006', 'Norway'); -""" -echo $parseSQL """ +""") == "insert into Customers (CustomerName , ContactName , Address , City , PostalCode , Country ) values ('Cardinal' , 'Tom B. Erichsen' , 'Skagen 21' , 'Stavanger' , '4006' , 'Norway' );" + +doAssert $parseSQL(""" INSERT INTO TableName DEFAULT VALUES -""" -echo $parseSQL """ +""") == "insert into TableName default values;" + +doAssert $parseSQL(""" UPDATE Customers SET ContactName = 'Alfred Schmidt', City= 'Frankfurt' WHERE CustomerID = 1; -""" -echo $parseSQL "DELETE FROM table_name;" -echo $parseSQL "DELETE * FROM table_name;" -echo $parseSQL """ +""") == "update Customers set ContactName = 'Alfred Schmidt' , City = 'Frankfurt' where CustomerID = 1;" + +doAssert $parseSQL("DELETE FROM table_name;") == "delete from table_name;" + +doAssert $parseSQL("DELETE * FROM table_name;") == "delete from table_name;" + +doAssert $parseSQL(""" --Select all: SELECT * FROM Customers; -""" -echo $parseSQL """ +""") == "select * from Customers;" + +doAssert $parseSQL(""" SELECT * FROM Customers WHERE (CustomerName LIKE 'L%' OR CustomerName LIKE 'R%' /*OR CustomerName LIKE 'S%' OR CustomerName LIKE 'T%'*/ OR CustomerName LIKE 'W%') AND Country='USA' ORDER BY CustomerName; -""" +""") == "select * from Customers where(CustomerName like 'L%' or CustomerName like 'R%' or CustomerName like 'W%') and Country = 'USA' order by CustomerName;" + +# parse keywords as identifires +doAssert $parseSQL(""" +SELECT `SELECT`, `FROM` as `GROUP` FROM `WHERE`; +""") == """select "SELECT", "FROM" as "GROUP" from "WHERE";""" -- cgit 1.4.1-2-gfad0 From a879973081e2c29d64e9fb9d8e539aa980533b10 Mon Sep 17 00:00:00 2001 From: GULPF Date: Mon, 18 Dec 2017 23:11:28 +0100 Subject: Better times module (#6552) * First work on better timezones * Update tests to new api. Removed tests for checking that `isDst` was included when formatting, since `isDst` no longer affects utc offset (the entire utc offset is stored directly in `utcOffset` instead). * Deprecate getLocaltime & getGmTime * Add `now()` as a shorthand for GetTIme().inZone(Local) * Add FedericoCeratto's timezone tests (#6548) * Run more tests in all timezones * Make month enum start at 1 instead of 0 * Deprecate getDayOfWeekJulian * Fix issues with gc safety * Rename TimeInfo => DateTime * Fixes #6465 * Improve isLeapYear * FIx handling negative adjTime * Cleanup: - deprecated toSeconds and fromSeconds, added fromUnix and toUnix instead (that returns int64 instead of float) - added missing doc comments - removed some unnecessary JS specific implementations * Fix misstake in JS `-` for Time * Update usage of TimeEffect * Removed unecessary use of `difftime` * JS fix for local tz * Fix subtraction of months * Fix `days` field in `toTimeInterval` * Style and docs * Fix getDayOfYear for real this time... * Fix handling of adding/subtracting time across dst transitions * Fix some bad usage of the times module in the stdlib * Revert to use proper time resoultion for seeding in random.nim * Move deprecated procs to bottom of file * Always use `epochTime` in `randomize` * Remove TimeInterval normalization * Fixes #6905 * Fix getDayOfWeek for year < 1 * Export toEpochDay/fromEpochDay and change year/month/monthday order * Add asserts for checking that the monthday is valid * Fix some remaining ambiguous references to `Time` * Fix ambiguous reference to Time --- lib/posix/posix.nim | 3 +- lib/posix/posix_linux_amd64.nim | 6 +- lib/posix/posix_other.nim | 36 +- lib/pure/cookies.nim | 6 +- lib/pure/ioselects/ioselectors_epoll.nim | 7 +- lib/pure/ioselects/ioselectors_kqueue.nim | 4 +- lib/pure/oids.nim | 2 +- lib/pure/os.nim | 35 +- lib/pure/osproc.nim | 16 +- lib/pure/random.nim | 8 +- lib/pure/times.nim | 1531 +++++++++++++++-------------- tests/js/ttimes.nim | 53 +- tests/stdlib/ttimes.nim | 366 ++++--- 13 files changed, 1106 insertions(+), 967 deletions(-) (limited to 'tests/stdlib') diff --git a/lib/posix/posix.nim b/lib/posix/posix.nim index b635c0b0b..fba35868c 100644 --- a/lib/posix/posix.nim +++ b/lib/posix/posix.nim @@ -609,11 +609,12 @@ proc clock_nanosleep*(a1: ClockId, a2: cint, a3: var Timespec, proc clock_settime*(a1: ClockId, a2: var Timespec): cint {. importc, header: "".} +proc `==`*(a, b: Time): bool {.borrow.} +proc `-`*(a, b: Time): Time {.borrow.} proc ctime*(a1: var Time): cstring {.importc, header: "".} proc ctime_r*(a1: var Time, a2: cstring): cstring {.importc, header: "".} proc difftime*(a1, a2: Time): cdouble {.importc, header: "".} proc getdate*(a1: cstring): ptr Tm {.importc, header: "".} - proc gmtime*(a1: var Time): ptr Tm {.importc, header: "".} proc gmtime_r*(a1: var Time, a2: var Tm): ptr Tm {.importc, header: "".} proc localtime*(a1: var Time): ptr Tm {.importc, header: "".} diff --git a/lib/posix/posix_linux_amd64.nim b/lib/posix/posix_linux_amd64.nim index c44128b16..9e6211b63 100644 --- a/lib/posix/posix_linux_amd64.nim +++ b/lib/posix/posix_linux_amd64.nim @@ -12,8 +12,6 @@ # To be included from posix.nim! -from times import Time - const hasSpawnH = not defined(haiku) # should exist for every Posix system nowadays hasAioH = defined(linux) @@ -40,13 +38,15 @@ type const SIG_HOLD* = cast[SigHandler](2) type + Time* {.importc: "time_t", header: "".} = distinct clong + Timespec* {.importc: "struct timespec", header: "", final, pure.} = object ## struct timespec tv_sec*: Time ## Seconds. tv_nsec*: clong ## Nanoseconds. Dirent* {.importc: "struct dirent", - header: "", final, pure.} = object ## dirent_t struct + header: "", final, pure.} = object ## dirent_t struct d_ino*: Ino d_off*: Off d_reclen*: cushort diff --git a/lib/posix/posix_other.nim b/lib/posix/posix_other.nim index 7321889a8..e552bf807 100644 --- a/lib/posix/posix_other.nim +++ b/lib/posix/posix_other.nim @@ -9,8 +9,6 @@ {.deadCodeElim:on.} -from times import Time - const hasSpawnH = not defined(haiku) # should exist for every Posix system nowadays hasAioH = defined(linux) @@ -36,6 +34,8 @@ type {.deprecated: [TSocketHandle: SocketHandle].} type + Time* {.importc: "time_t", header: "".} = distinct int + Timespec* {.importc: "struct timespec", header: "", final, pure.} = object ## struct timespec tv_sec*: Time ## Seconds. @@ -209,24 +209,24 @@ type st_gid*: Gid ## Group ID of file. st_rdev*: Dev ## Device ID (if file is character or block special). st_size*: Off ## For regular files, the file size in bytes. - ## For symbolic links, the length in bytes of the - ## pathname contained in the symbolic link. - ## For a shared memory object, the length in bytes. - ## For a typed memory object, the length in bytes. - ## For other file types, the use of this field is - ## unspecified. + ## For symbolic links, the length in bytes of the + ## pathname contained in the symbolic link. + ## For a shared memory object, the length in bytes. + ## For a typed memory object, the length in bytes. + ## For other file types, the use of this field is + ## unspecified. when defined(macosx) or defined(android): - st_atime*: Time ## Time of last access. - st_mtime*: Time ## Time of last data modification. - st_ctime*: Time ## Time of last status change. + st_atime*: Time ## Time of last access. + st_mtime*: Time ## Time of last data modification. + st_ctime*: Time ## Time of last status change. else: - st_atim*: Timespec ## Time of last access. - st_mtim*: Timespec ## Time of last data modification. - st_ctim*: Timespec ## Time of last status change. - st_blksize*: Blksize ## A file system-specific preferred I/O block size - ## for this object. In some file system types, this - ## may vary from file to file. - st_blocks*: Blkcnt ## Number of blocks allocated for this object. + st_atim*: Timespec ## Time of last access. + st_mtim*: Timespec ## Time of last data modification. + st_ctim*: Timespec ## Time of last status change. + st_blksize*: Blksize ## A file system-specific preferred I/O block size + ## for this object. In some file system types, this + ## may vary from file to file. + st_blocks*: Blkcnt ## Number of blocks allocated for this object. Statvfs* {.importc: "struct statvfs", header: "", diff --git a/lib/pure/cookies.nim b/lib/pure/cookies.nim index 07b37c7d4..8f16717ac 100644 --- a/lib/pure/cookies.nim +++ b/lib/pure/cookies.nim @@ -51,7 +51,7 @@ proc setCookie*(key, value: string, domain = "", path = "", if secure: result.add("; Secure") if httpOnly: result.add("; HttpOnly") -proc setCookie*(key, value: string, expires: TimeInfo, +proc setCookie*(key, value: string, expires: DateTime, domain = "", path = "", noName = false, secure = false, httpOnly = false): string = ## Creates a command in the format of @@ -63,9 +63,9 @@ proc setCookie*(key, value: string, expires: TimeInfo, noname, secure, httpOnly) when isMainModule: - var tim = Time(int(getTime()) + 76 * (60 * 60 * 24)) + var tim = fromUnix(getTime().toUnix + 76 * (60 * 60 * 24)) - let cookie = setCookie("test", "value", tim.getGMTime()) + let cookie = setCookie("test", "value", tim.utc) when not defined(testing): echo cookie let start = "Set-Cookie: test=value; Expires=" diff --git a/lib/pure/ioselects/ioselectors_epoll.nim b/lib/pure/ioselects/ioselectors_epoll.nim index 35cdace09..8827f239f 100644 --- a/lib/pure/ioselects/ioselectors_epoll.nim +++ b/lib/pure/ioselects/ioselectors_epoll.nim @@ -277,15 +277,16 @@ proc registerTimer*[T](s: Selector[T], timeout: int, oneshot: bool, var events = {Event.Timer} var epv = EpollEvent(events: EPOLLIN or EPOLLRDHUP) epv.data.u64 = fdi.uint + if oneshot: - new_ts.it_interval.tv_sec = 0.Time + new_ts.it_interval.tv_sec = posix.Time(0) new_ts.it_interval.tv_nsec = 0 - new_ts.it_value.tv_sec = (timeout div 1_000).Time + new_ts.it_value.tv_sec = posix.Time(timeout div 1_000) new_ts.it_value.tv_nsec = (timeout %% 1_000) * 1_000_000 incl(events, Event.Oneshot) epv.events = epv.events or EPOLLONESHOT else: - new_ts.it_interval.tv_sec = (timeout div 1000).Time + new_ts.it_interval.tv_sec = posix.Time(timeout div 1000) new_ts.it_interval.tv_nsec = (timeout %% 1_000) * 1_000_000 new_ts.it_value.tv_sec = new_ts.it_interval.tv_sec new_ts.it_value.tv_nsec = new_ts.it_interval.tv_nsec diff --git a/lib/pure/ioselects/ioselectors_kqueue.nim b/lib/pure/ioselects/ioselectors_kqueue.nim index 3e2ec64a8..af5aa15df 100644 --- a/lib/pure/ioselects/ioselectors_kqueue.nim +++ b/lib/pure/ioselects/ioselectors_kqueue.nim @@ -452,10 +452,10 @@ proc selectInto*[T](s: Selector[T], timeout: int, if timeout != -1: if timeout >= 1000: - tv.tv_sec = (timeout div 1_000).Time + tv.tv_sec = posix.Time(timeout div 1_000) tv.tv_nsec = (timeout %% 1_000) * 1_000_000 else: - tv.tv_sec = 0.Time + tv.tv_sec = posix.Time(0) tv.tv_nsec = timeout * 1_000_000 else: ptv = nil diff --git a/lib/pure/oids.nim b/lib/pure/oids.nim index 60b53dbe0..427a68964 100644 --- a/lib/pure/oids.nim +++ b/lib/pure/oids.nim @@ -88,7 +88,7 @@ proc generatedTime*(oid: Oid): Time = var tmp: int32 var dummy = oid.time bigEndian32(addr(tmp), addr(dummy)) - result = Time(tmp) + result = fromUnix(tmp) when not defined(testing) and isMainModule: let xo = genOid() diff --git a/lib/pure/os.nim b/lib/pure/os.nim index a59134007..87f6def29 100644 --- a/lib/pure/os.nim +++ b/lib/pure/os.nim @@ -173,33 +173,33 @@ proc findExe*(exe: string, followSymlinks: bool = true; return x result = "" -proc getLastModificationTime*(file: string): Time {.rtl, extern: "nos$1".} = +proc getLastModificationTime*(file: string): times.Time {.rtl, extern: "nos$1".} = ## Returns the `file`'s last modification time. when defined(posix): var res: Stat if stat(file, res) < 0'i32: raiseOSError(osLastError()) - return res.st_mtime + return fromUnix(res.st_mtime.int64) else: var f: WIN32_FIND_DATA var h = findFirstFile(file, f) if h == -1'i32: raiseOSError(osLastError()) - result = winTimeToUnixTime(rdFileTime(f.ftLastWriteTime)) + result = fromUnix(winTimeToUnixTime(rdFileTime(f.ftLastWriteTime)).int64) findClose(h) -proc getLastAccessTime*(file: string): Time {.rtl, extern: "nos$1".} = +proc getLastAccessTime*(file: string): times.Time {.rtl, extern: "nos$1".} = ## Returns the `file`'s last read or write access time. when defined(posix): var res: Stat if stat(file, res) < 0'i32: raiseOSError(osLastError()) - return res.st_atime + return fromUnix(res.st_atime.int64) else: var f: WIN32_FIND_DATA var h = findFirstFile(file, f) if h == -1'i32: raiseOSError(osLastError()) - result = winTimeToUnixTime(rdFileTime(f.ftLastAccessTime)) + result = fromUnix(winTimeToUnixTime(rdFileTime(f.ftLastAccessTime)).int64) findClose(h) -proc getCreationTime*(file: string): Time {.rtl, extern: "nos$1".} = +proc getCreationTime*(file: string): times.Time {.rtl, extern: "nos$1".} = ## Returns the `file`'s creation time. ## ## **Note:** Under POSIX OS's, the returned time may actually be the time at @@ -208,12 +208,12 @@ proc getCreationTime*(file: string): Time {.rtl, extern: "nos$1".} = when defined(posix): var res: Stat if stat(file, res) < 0'i32: raiseOSError(osLastError()) - return res.st_ctime + return fromUnix(res.st_ctime.int64) else: var f: WIN32_FIND_DATA var h = findFirstFile(file, f) if h == -1'i32: raiseOSError(osLastError()) - result = winTimeToUnixTime(rdFileTime(f.ftCreationTime)) + result = fromUnix(winTimeToUnixTime(rdFileTime(f.ftCreationTime)).int64) findClose(h) proc fileNewer*(a, b: string): bool {.rtl, extern: "nos$1".} = @@ -1443,7 +1443,7 @@ proc sleep*(milsecs: int) {.rtl, extern: "nos$1", tags: [TimeEffect].} = winlean.sleep(int32(milsecs)) else: var a, b: Timespec - a.tv_sec = Time(milsecs div 1000) + a.tv_sec = posix.Time(milsecs div 1000) a.tv_nsec = (milsecs mod 1000) * 1000 * 1000 discard posix.nanosleep(a, b) @@ -1481,16 +1481,17 @@ type size*: BiggestInt # Size of file. permissions*: set[FilePermission] # File permissions linkCount*: BiggestInt # Number of hard links the file object has. - lastAccessTime*: Time # Time file was last accessed. - lastWriteTime*: Time # Time file was last modified/written to. - creationTime*: Time # Time file was created. Not supported on all systems! + lastAccessTime*: times.Time # Time file was last accessed. + lastWriteTime*: times.Time # Time file was last modified/written to. + creationTime*: times.Time # Time file was created. Not supported on all systems! template rawToFormalFileInfo(rawInfo, path, formalInfo): untyped = ## Transforms the native file info structure into the one nim uses. ## 'rawInfo' is either a 'TBY_HANDLE_FILE_INFORMATION' structure on Windows, ## or a 'Stat' structure on posix when defined(Windows): - template toTime(e: FILETIME): untyped {.gensym.} = winTimeToUnixTime(rdFileTime(e)) # local templates default to bind semantics + template toTime(e: FILETIME): untyped {.gensym.} = + fromUnix(winTimeToUnixTime(rdFileTime(e)).int64) # local templates default to bind semantics template merge(a, b): untyped = a or (b shl 32) formalInfo.id.device = rawInfo.dwVolumeSerialNumber formalInfo.id.file = merge(rawInfo.nFileIndexLow, rawInfo.nFileIndexHigh) @@ -1522,9 +1523,9 @@ template rawToFormalFileInfo(rawInfo, path, formalInfo): untyped = formalInfo.id = (rawInfo.st_dev, rawInfo.st_ino) formalInfo.size = rawInfo.st_size formalInfo.linkCount = rawInfo.st_Nlink.BiggestInt - formalInfo.lastAccessTime = rawInfo.st_atime - formalInfo.lastWriteTime = rawInfo.st_mtime - formalInfo.creationTime = rawInfo.st_ctime + formalInfo.lastAccessTime = fromUnix(rawInfo.st_atime.int64) + formalInfo.lastWriteTime = fromUnix(rawInfo.st_mtime.int64) + formalInfo.creationTime = fromUnix(rawInfo.st_ctime.int64) result.permissions = {} checkAndIncludeMode(S_IRUSR, fpUserRead) diff --git a/lib/pure/osproc.nim b/lib/pure/osproc.nim index f0542ea98..1625845d1 100644 --- a/lib/pure/osproc.nim +++ b/lib/pure/osproc.nim @@ -1060,10 +1060,10 @@ elif not defined(useNimRtl): var tmspec: Timespec if timeout >= 1000: - tmspec.tv_sec = (timeout div 1_000).Time + tmspec.tv_sec = posix.Time(timeout div 1_000) tmspec.tv_nsec = (timeout %% 1_000) * 1_000_000 else: - tmspec.tv_sec = 0.Time + tmspec.tv_sec = posix.Time(0) tmspec.tv_nsec = (timeout * 1_000_000) try: @@ -1109,20 +1109,20 @@ elif not defined(useNimRtl): var b: Timespec b.tv_sec = e.tv_sec b.tv_nsec = e.tv_nsec - e.tv_sec = (e.tv_sec - s.tv_sec).Time + e.tv_sec = e.tv_sec - s.tv_sec if e.tv_nsec >= s.tv_nsec: e.tv_nsec -= s.tv_nsec else: - if e.tv_sec == 0.Time: + if e.tv_sec == posix.Time(0): raise newException(ValueError, "System time was modified") else: diff = s.tv_nsec - e.tv_nsec e.tv_nsec = 1_000_000_000 - diff - t.tv_sec = (t.tv_sec - e.tv_sec).Time + t.tv_sec = t.tv_sec - e.tv_sec if t.tv_nsec >= e.tv_nsec: t.tv_nsec -= e.tv_nsec else: - t.tv_sec = (int(t.tv_sec) - 1).Time + t.tv_sec = t.tv_sec - posix.Time(1) diff = e.tv_nsec - t.tv_nsec t.tv_nsec = 1_000_000_000 - diff s.tv_sec = b.tv_sec @@ -1154,10 +1154,10 @@ elif not defined(useNimRtl): raiseOSError(osLastError()) if timeout >= 1000: - tmspec.tv_sec = (timeout div 1_000).Time + tmspec.tv_sec = posix.Time(timeout div 1_000) tmspec.tv_nsec = (timeout %% 1_000) * 1_000_000 else: - tmspec.tv_sec = 0.Time + tmspec.tv_sec = posix.Time(0) tmspec.tv_nsec = (timeout * 1_000_000) try: diff --git a/lib/pure/random.nim b/lib/pure/random.nim index 7edd93c08..de419b9fb 100644 --- a/lib/pure/random.nim +++ b/lib/pure/random.nim @@ -190,12 +190,8 @@ when not defined(nimscript): proc randomize*() {.benign.} = ## Initializes the random number generator with a "random" ## number, i.e. a tickcount. Note: Does not work for NimScript. - when defined(JS): - proc getMil(t: Time): int {.importcpp: "getTime", nodecl.} - randomize(getMil times.getTime()) - else: - let time = int64(times.epochTime() * 1_000_000_000) - randomize(time) + let time = int64(times.epochTime() * 1_000_000_000) + randomize(time) {.pop.} diff --git a/lib/pure/times.nim b/lib/pure/times.nim index c1d6c3e53..dcc817b7b 100644 --- a/lib/pure/times.nim +++ b/lib/pure/times.nim @@ -10,27 +10,26 @@ ## This module contains routines and types for dealing with time. ## This module is available for the `JavaScript target -## `_. +## `_. The proleptic Gregorian calendar is the only calendar supported. ## ## Examples: ## ## .. code-block:: nim ## ## import times, os -## var -## t = cpuTime() +## let time = cpuTime() ## ## sleep(100) # replace this with something to be timed -## echo "Time taken: ",cpuTime() - t +## echo "Time taken: ",cpuTime() - time ## -## echo "My formatted time: ", format(getLocalTime(getTime()), "d MMMM yyyy HH:mm") +## echo "My formatted time: ", format(now(), "d MMMM yyyy HH:mm") ## echo "Using predefined formats: ", getClockStr(), " ", getDateStr() ## ## echo "epochTime() float value: ", epochTime() ## echo "getTime() float value: ", toSeconds(getTime()) ## echo "cpuTime() float value: ", cpuTime() -## echo "An hour from now : ", getLocalTime(getTime()) + 1.hours -## echo "An hour from (UTC) now: ", getGmTime(getTime()) + initInterval(0,0,0,1) +## echo "An hour from now : ", now() + 1.hours +## echo "An hour from (UTC) now: ", getTime().utc + initInterval(0,0,0,1) {.push debugger:off.} # the user does not want to trace a part # of the standard library! @@ -40,132 +39,85 @@ import include "system/inclrtl" -type - Month* = enum ## represents a month - mJan, mFeb, mMar, mApr, mMay, mJun, mJul, mAug, mSep, mOct, mNov, mDec - WeekDay* = enum ## represents a weekday - dMon, dTue, dWed, dThu, dFri, dSat, dSun - -when defined(posix) and not defined(JS): - when defined(linux) and defined(amd64): - type - TimeImpl {.importc: "time_t", header: "".} = clong - Time* = distinct TimeImpl ## distinct type that represents a time - ## measured as number of seconds since the epoch - - Timeval {.importc: "struct timeval", - header: "".} = object ## struct timeval - tv_sec: clong ## Seconds. - tv_usec: clong ## Microseconds. - else: - type - TimeImpl {.importc: "time_t", header: "".} = int - Time* = distinct TimeImpl ## distinct type that represents a time - ## measured as number of seconds since the epoch - - Timeval {.importc: "struct timeval", - header: "".} = object ## struct timeval - tv_sec: int ## Seconds. - tv_usec: int ## Microseconds. +when defined(posix): + import posix - # we cannot import posix.nim here, because posix.nim depends on times.nim. - # Ok, we could, but I don't want circular dependencies. - # And gettimeofday() is not defined in the posix module anyway. Sigh. + type CTime = posix.Time proc posix_gettimeofday(tp: var Timeval, unused: pointer = nil) {. importc: "gettimeofday", header: "".} when not defined(freebsd) and not defined(netbsd) and not defined(openbsd): var timezone {.importc, header: "".}: int - proc tzset(): void {.importc, header: "".} tzset() elif defined(windows): import winlean # newest version of Visual C++ defines time_t to be of 64 bits - type TimeImpl {.importc: "time_t", header: "".} = int64 + type CTime {.importc: "time_t", header: "".} = distinct int64 # visual c's c runtime exposes these under a different name - var - timezone {.importc: "_timezone", header: "".}: int - - type - Time* = distinct TimeImpl + var timezone {.importc: "_timezone", header: "".}: int +type + Month* = enum ## Represents a month. Note that the enum starts at ``1``, so ``ord(month)`` will give + ## the month number in the range ``[1..12]``. + mJan = 1, mFeb, mMar, mApr, mMay, mJun, mJul, mAug, mSep, mOct, mNov, mDec -elif defined(JS): - type - TimeBase = float - Time* = distinct TimeBase - - proc getDay(t: Time): int {.tags: [], raises: [], benign, importcpp.} - proc getFullYear(t: Time): int {.tags: [], raises: [], benign, importcpp.} - proc getHours(t: Time): int {.tags: [], raises: [], benign, importcpp.} - proc getMilliseconds(t: Time): int {.tags: [], raises: [], benign, importcpp.} - proc getMinutes(t: Time): int {.tags: [], raises: [], benign, importcpp.} - proc getMonth(t: Time): int {.tags: [], raises: [], benign, importcpp.} - proc getSeconds(t: Time): int {.tags: [], raises: [], benign, importcpp.} - proc getTime(t: Time): int {.tags: [], raises: [], noSideEffect, benign, importcpp.} - proc getTimezoneOffset(t: Time): int {.tags: [], raises: [], benign, importcpp.} - proc getDate(t: Time): int {.tags: [], raises: [], benign, importcpp.} - proc getUTCDate(t: Time): int {.tags: [], raises: [], benign, importcpp.} - proc getUTCFullYear(t: Time): int {.tags: [], raises: [], benign, importcpp.} - proc getUTCHours(t: Time): int {.tags: [], raises: [], benign, importcpp.} - proc getUTCMilliseconds(t: Time): int {.tags: [], raises: [], benign, importcpp.} - proc getUTCMinutes(t: Time): int {.tags: [], raises: [], benign, importcpp.} - proc getUTCMonth(t: Time): int {.tags: [], raises: [], benign, importcpp.} - proc getUTCSeconds(t: Time): int {.tags: [], raises: [], benign, importcpp.} - proc getUTCDay(t: Time): int {.tags: [], raises: [], benign, importcpp.} - proc getYear(t: Time): int {.tags: [], raises: [], benign, importcpp.} - proc parse(t: Time; s: cstring): Time {.tags: [], raises: [], benign, importcpp.} - proc setDate(t: Time; x: int) {.tags: [], raises: [], benign, importcpp.} - proc setFullYear(t: Time; x: int) {.tags: [], raises: [], benign, importcpp.} - proc setHours(t: Time; x: int) {.tags: [], raises: [], benign, importcpp.} - proc setMilliseconds(t: Time; x: int) {.tags: [], raises: [], benign, importcpp.} - proc setMinutes(t: Time; x: int) {.tags: [], raises: [], benign, importcpp.} - proc setMonth(t: Time; x: int) {.tags: [], raises: [], benign, importcpp.} - proc setSeconds(t: Time; x: int) {.tags: [], raises: [], benign, importcpp.} - proc setTime(t: Time; x: int) {.tags: [], raises: [], benign, importcpp.} - proc setUTCDate(t: Time; x: int) {.tags: [], raises: [], benign, importcpp.} - proc setUTCFullYear(t: Time; x: int) {.tags: [], raises: [], benign, importcpp.} - proc setUTCHours(t: Time; x: int) {.tags: [], raises: [], benign, importcpp.} - proc setUTCMilliseconds(t: Time; x: int) {.tags: [], raises: [], benign, importcpp.} - proc setUTCMinutes(t: Time; x: int) {.tags: [], raises: [], benign, importcpp.} - proc setUTCMonth(t: Time; x: int) {.tags: [], raises: [], benign, importcpp.} - proc setUTCSeconds(t: Time; x: int) {.tags: [], raises: [], benign, importcpp.} - proc setYear(t: Time; x: int) {.tags: [], raises: [], benign, importcpp.} - proc toGMTString(t: Time): cstring {.tags: [], raises: [], benign, importcpp.} - proc toLocaleString(t: Time): cstring {.tags: [], raises: [], benign, importcpp.} + WeekDay* = enum ## Represents a weekday. + dMon, dTue, dWed, dThu, dFri, dSat, dSun -type - TimeInfo* = object of RootObj ## represents a time in different parts - second*: range[0..61] ## The number of seconds after the minute, + MonthdayRange* = range[1..31] + HourRange* = range[0..23] + MinuteRange* = range[0..59] + SecondRange* = range[0..60] + YeardayRange* = range[0..365] + + TimeImpl = int64 + + Time* = distinct TimeImpl ## Represents a point in time. + ## This is currently implemented as a ``int64`` representing + ## seconds since ``1970-01-01T00:00:00Z``, but don't + ## rely on this knowledge because it might change + ## in the future to allow for higher precision. + ## Use the procs ``toUnix`` and ``fromUnix`` to + ## work with unix timestamps instead. + + DateTime* = object of RootObj ## Represents a time in different parts. + ## Although this type can represent leap + ## seconds, they are generally not supported + ## in this module. They are not ignored, + ## but the ``DateTime``'s returned by + ## procedures in this module will never have + ## a leap second. + second*: SecondRange ## The number of seconds after the minute, ## normally in the range 0 to 59, but can - ## be up to 61 to allow for leap seconds. - minute*: range[0..59] ## The number of minutes after the hour, + ## be up to 60 to allow for a leap second. + minute*: MinuteRange ## The number of minutes after the hour, ## in the range 0 to 59. - hour*: range[0..23] ## The number of hours past midnight, + hour*: HourRange ## The number of hours past midnight, ## in the range 0 to 23. - monthday*: range[1..31] ## The day of the month, in the range 1 to 31. + monthday*: MonthdayRange ## The day of the month, in the range 1 to 31. month*: Month ## The current month. - year*: int ## The current year. + year*: int ## The current year, using astronomical year numbering + ## (meaning that before year 1 is year 0, then year -1 and so on). weekday*: WeekDay ## The current day of the week. - yearday*: range[0..365] ## The number of days since January 1, + yearday*: YeardayRange ## The number of days since January 1, ## in the range 0 to 365. - ## Always 0 if the target is JS. - isDST*: bool ## Determines whether DST is in effect. - ## Semantically, this adds another negative hour - ## offset to the time in addition to the timezone. - timezone*: int ## The offset of the (non-DST) timezone in seconds - ## west of UTC. Note that the sign of this number - ## is the opposite of the one in a formatted - ## timezone string like ``+01:00`` (which would be - ## parsed into the timezone ``-3600``). - - ## I make some assumptions about the data in here. Either - ## everything should be positive or everything negative. Zero is - ## fine too. Mixed signs will lead to unexpected results. - TimeInterval* = object ## a time interval + isDst*: bool ## Determines whether DST is in effect. + ## Always false for the JavaScript backend. + timezone*: Timezone ## The timezone represented as an implementation of ``Timezone``. + utcOffset*: int ## The offset in seconds west of UTC, including any offset due to DST. + ## Note that the sign of this number is the opposite + ## of the one in a formatted offset string like ``+01:00`` + ## (which would be parsed into the UTC offset ``-3600``). + + TimeInterval* = object ## Represents a duration of time. Can be used to add and subtract + ## from a ``DateTime`` or ``Time``. + ## Note that a ``TimeInterval`` doesn't represent a fixed duration of time, + ## since the duration of some units depend on the context (e.g a year + ## can be either 365 or 366 days long). The non-fixed time units are years, + ## months and days. milliseconds*: int ## The number of milliseconds seconds*: int ## The number of seconds minutes*: int ## The number of minutes @@ -174,92 +126,394 @@ type months*: int ## The number of months years*: int ## The number of years + Timezone* = object ## Timezone interface for supporting ``DateTime``'s of arbritary timezones. + ## The ``times`` module only supplies implementations for the systems local time and UTC. + ## The members ``zoneInfoFromUtc`` and ``zoneInfoFromTz`` should not be accessed directly + ## and are only exported so that ``Timezone`` can be implemented by other modules. + zoneInfoFromUtc*: proc (time: Time): ZonedTime {.nimcall, tags: [], raises: [], benign .} + zoneInfoFromTz*: proc (adjTime: Time): ZonedTime {.nimcall, tags: [], raises: [], benign .} + name*: string ## The name of the timezone, f.ex 'Europe/Stockholm' or 'Etc/UTC'. Used for checking equality. + ## Se also: https://en.wikipedia.org/wiki/List_of_tz_database_time_zones + ZonedTime* = object ## Represents a zooned instant in time that is not associated with any calendar. + ## This type is only used for implementing timezones. + adjTime*: Time ## Time adjusted to a timezone. + utcOffset*: int + isDst*: bool + {.deprecated: [TMonth: Month, TWeekDay: WeekDay, TTime: Time, - TTimeInterval: TimeInterval, TTimeInfo: TimeInfo].} + TTimeInterval: TimeInterval, TTimeInfo: DateTime, TimeInfo: DateTime].} -proc getTime*(): Time {.tags: [TimeEffect], benign.} - ## gets the current calendar time as a UNIX epoch value (number of seconds - ## elapsed since 1970) with integer precission. Use epochTime for higher - ## resolution. -proc getLocalTime*(t: Time): TimeInfo {.tags: [TimeEffect], raises: [], benign.} - ## converts the calendar time `t` to broken-time representation, - ## expressed relative to the user's specified time zone. -proc getGMTime*(t: Time): TimeInfo {.tags: [TimeEffect], raises: [], benign.} - ## converts the calendar time `t` to broken-down time representation, - ## expressed in Coordinated Universal Time (UTC). +const + secondsInMin = 60 + secondsInHour = 60*60 + secondsInDay = 60*60*24 + minutesInHour = 60 -proc timeInfoToTime*(timeInfo: TimeInfo): Time - {.tags: [TimeEffect], benign, deprecated.} - ## converts a broken-down time structure to - ## calendar time representation. The function ignores the specified - ## contents of the structure members `weekday` and `yearday` and recomputes - ## them from the other information in the broken-down time structure. - ## - ## **Warning:** This procedure is deprecated since version 0.14.0. - ## Use ``toTime`` instead. +proc fromUnix*(unix: int64): Time {.benign, tags: [], raises: [], noSideEffect.} = + ## Convert a unix timestamp (seconds since ``1970-01-01T00:00:00Z``) to a ``Time``. + Time(unix) -proc toTime*(timeInfo: TimeInfo): Time {.tags: [TimeEffect], benign.} - ## converts a broken-down time structure to - ## calendar time representation. The function ignores the specified - ## contents of the structure members `weekday` and `yearday` and recomputes - ## them from the other information in the broken-down time structure. +proc toUnix*(t: Time): int64 {.benign, tags: [], raises: [], noSideEffect.} = + ## Convert ``t`` to a unix timestamp (seconds since ``1970-01-01T00:00:00Z``). + t.int64 -proc fromSeconds*(since1970: float): Time {.tags: [], raises: [], benign.} - ## Takes a float which contains the number of seconds since the unix epoch and - ## returns a time object. +proc isLeapYear*(year: int): bool = + ## Returns true if ``year`` is a leap year. + year mod 4 == 0 and (year mod 100 != 0 or year mod 400 == 0) -proc fromSeconds*(since1970: int64): Time {.tags: [], raises: [], benign.} = - ## Takes an int which contains the number of seconds since the unix epoch and - ## returns a time object. - fromSeconds(float(since1970)) +proc getDaysInMonth*(month: Month, year: int): int = + ## Get the number of days in a ``month`` of a ``year``. + # http://www.dispersiondesign.com/articles/time/number_of_days_in_a_month + case month + of mFeb: result = if isLeapYear(year): 29 else: 28 + of mApr, mJun, mSep, mNov: result = 30 + else: result = 31 -proc toSeconds*(time: Time): float {.tags: [], raises: [], benign.} - ## Returns the time in seconds since the unix epoch. +proc getDaysInYear*(year: int): int = + ## Get the number of days in a ``year`` + result = 365 + (if isLeapYear(year): 1 else: 0) + +proc assertValidDate(monthday: MonthdayRange, month: Month, year: int) {.inline.} = + assert monthday <= getDaysInMonth(month, year), + $year & "-" & $ord(month) & "-" & $monthday & " is not a valid date" + +proc toEpochDay*(monthday: MonthdayRange, month: Month, year: int): int64 = + ## Get the epoch day from a year/month/day date. + ## The epoch day is the number of days since 1970/01/01 (it might be negative). + assertValidDate monthday, month, year + # Based on http://howardhinnant.github.io/date_algorithms.html + var (y, m, d) = (year, ord(month), monthday.int) + if m <= 2: + y.dec + + let era = (if y >= 0: y else: y-399) div 400 + let yoe = y - era * 400 + let doy = (153 * (m + (if m > 2: -3 else: 9)) + 2) div 5 + d-1 + let doe = yoe * 365 + yoe div 4 - yoe div 100 + doy + return era * 146097 + doe - 719468 + +proc fromEpochDay*(epochday: int64): tuple[monthday: MonthdayRange, month: Month, year: int] = + ## Get the year/month/day date from a epoch day. + ## The epoch day is the number of days since 1970/01/01 (it might be negative). + # Based on http://howardhinnant.github.io/date_algorithms.html + var z = epochday + z.inc 719468 + let era = (if z >= 0: z else: z - 146096) div 146097 + let doe = z - era * 146097 + let yoe = (doe - doe div 1460 + doe div 36524 - doe div 146096) div 365 + let y = yoe + era * 400; + let doy = doe - (365 * yoe + yoe div 4 - yoe div 100) + let mp = (5 * doy + 2) div 153 + let d = doy - (153 * mp + 2) div 5 + 1 + let m = mp + (if mp < 10: 3 else: -9) + return (d.MonthdayRange, m.Month, (y + ord(m <= 2)).int) + +proc getDayOfYear*(monthday: MonthdayRange, month: Month, year: int): YeardayRange {.tags: [], raises: [], benign .} = + ## Returns the day of the year. + ## Equivalent with ``initDateTime(day, month, year).yearday``. + assertValidDate monthday, month, year + const daysUntilMonth: array[Month, int] = [0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334] + const daysUntilMonthLeap: array[Month, int] = [0, 31, 60, 91, 121, 152, 182, 213, 244, 274, 305, 335] + + if isLeapYear(year): + result = daysUntilMonthLeap[month] + monthday - 1 + else: + result = daysUntilMonth[month] + monthday - 1 + +proc getDayOfWeek*(monthday: MonthdayRange, month: Month, year: int): WeekDay {.tags: [], raises: [], benign .} = + ## Returns the day of the week enum from day, month and year. + ## Equivalent with ``initDateTime(day, month, year).weekday``. + assertValidDate monthday, month, year + # 1970-01-01 is a Thursday, we adjust to the previous Monday + let days = toEpochday(monthday, month, year) - 3 + let weeks = (if days >= 0: days else: days - 6) div 7 + let wd = days - weeks * 7 + # The value of d is 0 for a Sunday, 1 for a Monday, 2 for a Tuesday, etc. + # so we must correct for the WeekDay type. + result = if wd == 0: dSun else: WeekDay(wd - 1) + +# Forward declarations +proc utcZoneInfoFromUtc(time: Time): ZonedTime {.tags: [], raises: [], benign .} +proc utcZoneInfoFromTz(adjTime: Time): ZonedTime {.tags: [], raises: [], benign .} +proc localZoneInfoFromUtc(time: Time): ZonedTime {.tags: [], raises: [], benign .} +proc localZoneInfoFromTz(adjTime: Time): ZonedTime {.tags: [], raises: [], benign .} proc `-`*(a, b: Time): int64 {. - rtl, extern: "ntDiffTime", tags: [], raises: [], noSideEffect, benign.} - ## computes the difference of two calendar times. Result is in seconds. + rtl, extern: "ntDiffTime", tags: [], raises: [], noSideEffect, benign, deprecated.} = + ## Computes the difference of two calendar times. Result is in seconds. + ## This is deprecated because it will need to change when sub second time resolution is implemented. + ## Use ``a.toUnix - b.toUnix`` instead. ## ## .. code-block:: nim ## let a = fromSeconds(1_000_000_000) ## let b = fromSeconds(1_500_000_000) ## echo initInterval(seconds=int(b - a)) ## # (milliseconds: 0, seconds: 20, minutes: 53, hours: 0, days: 5787, months: 0, years: 0) + a.toUnix - b.toUnix proc `<`*(a, b: Time): bool {. - rtl, extern: "ntLtTime", tags: [], raises: [], noSideEffect.} = - ## returns true iff ``a < b``, that is iff a happened before b. - when defined(js): - result = TimeBase(a) < TimeBase(b) - else: - result = a - b < 0 + rtl, extern: "ntLtTime", tags: [], raises: [], noSideEffect, borrow.} + ## Returns true iff ``a < b``, that is iff a happened before b. proc `<=` * (a, b: Time): bool {. - rtl, extern: "ntLeTime", tags: [], raises: [], noSideEffect.}= - ## returns true iff ``a <= b``. - when defined(js): - result = TimeBase(a) <= TimeBase(b) - else: - result = a - b <= 0 + rtl, extern: "ntLeTime", tags: [], raises: [], noSideEffect, borrow.} + ## Returns true iff ``a <= b``. proc `==`*(a, b: Time): bool {. - rtl, extern: "ntEqTime", tags: [], raises: [], noSideEffect.} = - ## returns true if ``a == b``, that is if both times represent the same value - when defined(js): - result = TimeBase(a) == TimeBase(b) + rtl, extern: "ntEqTime", tags: [], raises: [], noSideEffect, borrow.} + ## Returns true if ``a == b``, that is if both times represent the same point in time. + +proc toTime*(dt: DateTime): Time {.tags: [], raises: [], benign.} = + ## Converts a broken-down time structure to + ## calendar time representation. + let epochDay = toEpochday(dt.monthday, dt.month, dt.year) + result = Time(epochDay * secondsInDay) + result.inc dt.hour * secondsInHour + result.inc dt.minute * 60 + result.inc dt.second + # The code above ignores the UTC offset of `timeInfo`, + # so we need to compensate for that here. + result.inc dt.utcOffset + +proc initDateTime(zt: ZonedTime, zone: Timezone): DateTime = + let adjTime = zt.adjTime.int64 + let epochday = (if adjTime >= 0: adjTime else: adjTime - (secondsInDay - 1)) div secondsInDay + var rem = zt.adjTime.int64 - epochday * secondsInDay + let hour = rem div secondsInHour + rem = rem - hour * secondsInHour + let minute = rem div secondsInMin + rem = rem - minute * secondsInMin + let second = rem + + let (d, m, y) = fromEpochday(epochday) + + DateTime( + year: y, + month: m, + monthday: d, + hour: hour, + minute: minute, + second: second, + weekday: getDayOfWeek(d, m, y), + yearday: getDayOfYear(d, m, y), + isDst: zt.isDst, + timezone: zone, + utcOffset: zt.utcOffset + ) + +proc inZone*(time: Time, zone: Timezone): DateTime {.tags: [], raises: [], benign.} = + ## Break down ``time`` into a ``DateTime`` using ``zone`` as the timezone. + let zoneInfo = zone.zoneInfoFromUtc(time) + result = initDateTime(zoneInfo, zone) + +proc inZone*(dt: DateTime, zone: Timezone): DateTime {.tags: [], raises: [], benign.} = + ## Convert ``dt`` into a ``DateTime`` using ``zone`` as the timezone. + dt.toTime.inZone(zone) + +proc `$`*(zone: Timezone): string = + ## Returns the name of the timezone. + zone.name + +proc `==`*(zone1, zone2: Timezone): bool = + ## Two ``Timezone``'s are considered equal if their name is equal. + zone1.name == zone2.name + +proc toAdjTime(dt: DateTime): Time = + let epochDay = toEpochday(dt.monthday, dt.month, dt.year) + result = Time(epochDay * secondsInDay) + result.inc dt.hour * secondsInHour + result.inc dt.minute * secondsInMin + result.inc dt.second + +when defined(JS): + type JsDate = object + proc newDate(year, month, date, hours, minutes, seconds, milliseconds: int): JsDate {.tags: [], raises: [], importc: "new Date".} + proc newDate(): JsDate {.importc: "new Date".} + proc newDate(value: float): JsDate {.importc: "new Date".} + proc getTimezoneOffset(js: JsDate): int {.tags: [], raises: [], benign, importcpp.} + proc getDay(js: JsDate): int {.tags: [], raises: [], benign, importcpp.} + proc getFullYear(js: JsDate): int {.tags: [], raises: [], benign, importcpp.} + proc getHours(js: JsDate): int {.tags: [], raises: [], benign, importcpp.} + proc getMilliseconds(js: JsDate): int {.tags: [], raises: [], benign, importcpp.} + proc getMinutes(js: JsDate): int {.tags: [], raises: [], benign, importcpp.} + proc getMonth(js: JsDate): int {.tags: [], raises: [], benign, importcpp.} + proc getSeconds(js: JsDate): int {.tags: [], raises: [], benign, importcpp.} + proc getTime(js: JsDate): int {.tags: [], raises: [], noSideEffect, benign, importcpp.} + proc getDate(js: JsDate): int {.tags: [], raises: [], benign, importcpp.} + proc getUTCDate(js: JsDate): int {.tags: [], raises: [], benign, importcpp.} + proc getUTCFullYear(js: JsDate): int {.tags: [], raises: [], benign, importcpp.} + proc getUTCHours(js: JsDate): int {.tags: [], raises: [], benign, importcpp.} + proc getUTCMilliseconds(js: JsDate): int {.tags: [], raises: [], benign, importcpp.} + proc getUTCMinutes(js: JsDate): int {.tags: [], raises: [], benign, importcpp.} + proc getUTCMonth(js: JsDate): int {.tags: [], raises: [], benign, importcpp.} + proc getUTCSeconds(js: JsDate): int {.tags: [], raises: [], benign, importcpp.} + proc getUTCDay(js: JsDate): int {.tags: [], raises: [], benign, importcpp.} + proc getYear(js: JsDate): int {.tags: [], raises: [], benign, importcpp.} + proc setFullYear(js: JsDate, year: int): void {.tags: [], raises: [], benign, importcpp.} + + proc localZoneInfoFromUtc(time: Time): ZonedTime = + let jsDate = newDate(time.float * 1000) + let offset = jsDate.getTimezoneOffset() * secondsInMin + result.adjTime = Time(time.int64 - offset) + result.utcOffset = offset + result.isDst = false + + proc localZoneInfoFromTz(adjTime: Time): ZonedTime = + let utcDate = newDate(adjTime.float * 1000) + let localDate = newDate(utcDate.getUTCFullYear(), utcDate.getUTCMonth(), utcDate.getUTCDate(), + utcDate.getUTCHours(), utcDate.getUTCMinutes(), utcDate.getUTCSeconds(), 0) + + # This is as dumb as it looks - JS doesn't support years in the range 0-99 in the constructor + # because they are assumed to be 19xx... + # Because JS doesn't support timezone history, it doesn't really matter in practice. + if utcDate.getUTCFullYear() in 0 .. 99: + localDate.setFullYear(utcDate.getUTCFullYear()) + + result.adjTime = adjTime + result.utcOffset = localDate.getTimezoneOffset() * secondsInMin + result.isDst = false + +else: + when defined(freebsd) or defined(netbsd) or defined(openbsd) or + defined(macosx): + type + StructTm {.importc: "struct tm".} = object + second {.importc: "tm_sec".}, + minute {.importc: "tm_min".}, + hour {.importc: "tm_hour".}, + monthday {.importc: "tm_mday".}, + month {.importc: "tm_mon".}, + year {.importc: "tm_year".}, + weekday {.importc: "tm_wday".}, + yearday {.importc: "tm_yday".}, + isdst {.importc: "tm_isdst".}: cint + gmtoff {.importc: "tm_gmtoff".}: clong else: - result = a - b == 0 + type + StructTm {.importc: "struct tm".} = object + second {.importc: "tm_sec".}, + minute {.importc: "tm_min".}, + hour {.importc: "tm_hour".}, + monthday {.importc: "tm_mday".}, + month {.importc: "tm_mon".}, + year {.importc: "tm_year".}, + weekday {.importc: "tm_wday".}, + yearday {.importc: "tm_yday".}, + isdst {.importc: "tm_isdst".}: cint + when defined(linux) and defined(amd64): + gmtoff {.importc: "tm_gmtoff".}: clong + zone {.importc: "tm_zone".}: cstring + type + StructTmPtr = ptr StructTm + + proc localtime(timer: ptr CTime): StructTmPtr {. importc: "localtime", header: "", tags: [].} + + proc toAdjTime(tm: StructTm): Time = + let epochDay = toEpochday(tm.monthday, (tm.month + 1).Month, tm.year.int + 1900) + result = Time(epochDay * secondsInDay) + result.inc tm.hour * secondsInHour + result.inc tm.minute * 60 + result.inc tm.second + + proc getStructTm(time: Time | int64): StructTm = + let timei64 = time.int64 + var a = + if timei64 < low(CTime): + CTime(low(CTime)) + elif timei64 > high(CTime): + CTime(high(CTime)) + else: + CTime(timei64) + result = localtime(addr(a))[] + + proc localZoneInfoFromUtc(time: Time): ZonedTime = + let tm = getStructTm(time) + let adjTime = tm.toAdjTime + result.adjTime = adjTime + result.utcOffset = (time.toUnix - adjTime.toUnix).int + result.isDst = tm.isdst > 0 + + proc localZoneInfoFromTz(adjTime: Time): ZonedTime = + var adjTimei64 = adjTime.int64 + let past = adjTimei64 - secondsInDay + var tm = getStructTm(past) + let pastOffset = past - tm.toAdjTime.int64 + + let future = adjTimei64 + secondsInDay + tm = getStructTm(future) + let futureOffset = future - tm.toAdjTime.int64 + + var utcOffset: int + if pastOffset == futureOffset: + utcOffset = pastOffset.int + else: + if pastOffset > futureOffset: + adjTimei64 -= secondsInHour + + adjTimei64 += pastOffset + utcOffset = (adjTimei64 - getStructTm(adjTimei64).toAdjTime.int64).int + + # This extra roundtrip is needed to normalize any impossible datetimes + # as a result of offset changes (normally due to dst) + let utcTime = adjTime.int64 + utcOffset + tm = getStructTm(utcTime) + result.adjTime = tm.toAdjTime + result.utcOffset = (utcTime - result.adjTime.int64).int + result.isDst = tm.isdst > 0 + +proc utcZoneInfoFromUtc(time: Time): ZonedTime = + result.adjTime = time + result.utcOffset = 0 + result.isDst = false + +proc utcZoneInfoFromTz(adjTime: Time): ZonedTime = + utcZoneInfoFromUtc(adjTime) # adjTime == time since we are in UTC + +proc utc*(): TimeZone = + ## Get the ``Timezone`` implementation for the UTC timezone. + ## + ## .. code-block:: nim + ## doAssert now().utc.timezone == utc() + ## doAssert utc().name == "Etc/UTC" + Timezone(zoneInfoFromUtc: utcZoneInfoFromUtc, zoneInfoFromTz: utcZoneInfoFromTz, name: "Etc/UTC") -proc getTimezone*(): int {.tags: [TimeEffect], raises: [], benign.} - ## returns the offset of the local (non-DST) timezone in seconds west of UTC. +proc local*(): TimeZone = + ## Get the ``Timezone`` implementation for the local timezone. + ## + ## .. code-block:: nim + ## doAssert now().timezone == local() + ## doAssert local().name == "LOCAL" + Timezone(zoneInfoFromUtc: localZoneInfoFromUtc, zoneInfoFromTz: localZoneInfoFromTz, name: "LOCAL") + +proc utc*(dt: DateTime): DateTime = + ## Shorthand for ``dt.inZone(utc())``. + dt.inZone(utc()) + +proc local*(dt: DateTime): DateTime = + ## Shorthand for ``dt.inZone(local())``. + dt.inZone(local()) + +proc utc*(t: Time): DateTime = + ## Shorthand for ``t.inZone(utc())``. + t.inZone(utc()) + +proc local*(t: Time): DateTime = + ## Shorthand for ``t.inZone(local())``. + t.inZone(local()) -proc getStartMilsecs*(): int {.deprecated, tags: [TimeEffect], benign.} - ## get the milliseconds from the start of the program. **Deprecated since - ## version 0.8.10.** Use ``epochTime`` or ``cpuTime`` instead. +proc getTime*(): Time {.tags: [TimeEffect], benign.} + ## Gets the current time as a ``Time`` with second resolution. Use epochTime for higher + ## resolution. + +proc now*(): DateTime {.tags: [TimeEffect], benign.} = + ## Get the current time as a ``DateTime`` in the local timezone. + ## + ## Shorthand for ``getTime().local``. + getTime().local proc initInterval*(milliseconds, seconds, minutes, hours, days, months, years: int = 0): TimeInterval = - ## creates a new ``TimeInterval``. + ## Creates a new ``TimeInterval``. ## ## You can also use the convenience procedures called ``milliseconds``, ## ``seconds``, ``minutes``, ``hours``, ``days``, ``months``, and ``years``. @@ -269,46 +523,33 @@ proc initInterval*(milliseconds, seconds, minutes, hours, days, months, ## .. code-block:: nim ## ## let day = initInterval(hours=24) - ## let tomorrow = getTime() + day - ## echo(tomorrow) - var carryO = 0 - result.milliseconds = `mod`(milliseconds, 1000) - carryO = `div`(milliseconds, 1000) - result.seconds = `mod`(carryO + seconds, 60) - carryO = `div`(carryO + seconds, 60) - result.minutes = `mod`(carryO + minutes, 60) - carryO = `div`(carryO + minutes, 60) - result.hours = `mod`(carryO + hours, 24) - carryO = `div`(carryO + hours, 24) - result.days = carryO + days - - result.months = `mod`(months, 12) - carryO = `div`(months, 12) - result.years = carryO + years + ## let dt = initDateTime(01, mJan, 2000, 12, 00, 00, utc()) + ## doAssert $(dt + day) == "2000-01-02T12-00-00+00:00" + result.milliseconds = milliseconds + result.seconds = seconds + result.minutes = minutes + result.hours = hours + result.days = days + result.months = months + result.years = years proc `+`*(ti1, ti2: TimeInterval): TimeInterval = ## Adds two ``TimeInterval`` objects together. - var carryO = 0 - result.milliseconds = `mod`(ti1.milliseconds + ti2.milliseconds, 1000) - carryO = `div`(ti1.milliseconds + ti2.milliseconds, 1000) - result.seconds = `mod`(carryO + ti1.seconds + ti2.seconds, 60) - carryO = `div`(carryO + ti1.seconds + ti2.seconds, 60) - result.minutes = `mod`(carryO + ti1.minutes + ti2.minutes, 60) - carryO = `div`(carryO + ti1.minutes + ti2.minutes, 60) - result.hours = `mod`(carryO + ti1.hours + ti2.hours, 24) - carryO = `div`(carryO + ti1.hours + ti2.hours, 24) - result.days = carryO + ti1.days + ti2.days - - result.months = `mod`(ti1.months + ti2.months, 12) - carryO = `div`(ti1.months + ti2.months, 12) - result.years = carryO + ti1.years + ti2.years + result.milliseconds = ti1.milliseconds + ti2.milliseconds + result.seconds = ti1.seconds + ti2.seconds + result.minutes = ti1.minutes + ti2.minutes + result.hours = ti1.hours + ti2.hours + result.days = ti1.days + ti2.days + result.months = ti1.months + ti2.months + result.years = ti1.years + ti2.years proc `-`*(ti: TimeInterval): TimeInterval = ## Reverses a time interval + ## ## .. code-block:: nim ## ## let day = -initInterval(hours=24) - ## echo day # -> (milliseconds: 0, seconds: 0, minutes: 0, hours: 0, days: -1, months: 0, years: 0) + ## echo day # -> (milliseconds: 0, seconds: 0, minutes: 0, hours: -24, days: 0, months: 0, years: 0) result = TimeInterval( milliseconds: -ti.milliseconds, seconds: -ti.seconds, @@ -325,123 +566,100 @@ proc `-`*(ti1, ti2: TimeInterval): TimeInterval = ## Time components are compared one-by-one, see output: ## ## .. code-block:: nim - ## let a = fromSeconds(1_000_000_000) - ## let b = fromSeconds(1_500_000_000) + ## let a = fromUnix(1_000_000_000) + ## let b = fromUnix(1_500_000_000) ## echo b.toTimeInterval - a.toTimeInterval - ## # (milliseconds: 0, seconds: -40, minutes: -6, hours: 1, days: -2, months: -2, years: 16) + ## # (milliseconds: 0, seconds: -40, minutes: -6, hours: 1, days: 5, months: -2, years: 16) result = ti1 + (-ti2) -proc isLeapYear*(year: int): bool = - ## returns true if ``year`` is a leap year - - if year mod 400 == 0: - return true - elif year mod 100 == 0: - return false - elif year mod 4 == 0: - return true - else: - return false - -proc getDaysInMonth*(month: Month, year: int): int = - ## Get the number of days in a ``month`` of a ``year`` +proc evaluateInterval(dt: DateTime, interval: TimeInterval): tuple[adjDiff, absDiff: int64] = + ## Evaluates how many seconds the interval is worth + ## in the context of ``dt``. + ## The result in split into an adjusted diff and an absolute diff. - # http://www.dispersiondesign.com/articles/time/number_of_days_in_a_month - case month - of mFeb: result = if isLeapYear(year): 29 else: 28 - of mApr, mJun, mSep, mNov: result = 30 - else: result = 31 - -proc getDaysInYear*(year: int): int = - ## Get the number of days in a ``year`` - result = 365 + (if isLeapYear(year): 1 else: 0) - -proc toSeconds(a: TimeInfo, interval: TimeInterval): float = - ## Calculates how many seconds the interval is worth by adding up - ## all the fields - - var anew = a + var anew = dt var newinterv = interval - result = 0 newinterv.months += interval.years * 12 var curMonth = anew.month - if newinterv.months < 0: # subtracting + # Subtracting + if newinterv.months < 0: for mth in countDown(-1 * newinterv.months, 1): - result -= float(getDaysInMonth(curMonth, anew.year) * 24 * 60 * 60) if curMonth == mJan: curMonth = mDec anew.year.dec() else: curMonth.dec() - else: # adding + result.adjDiff -= getDaysInMonth(curMonth, anew.year) * secondsInDay + # Adding + else: for mth in 1 .. newinterv.months: - result += float(getDaysInMonth(curMonth, anew.year) * 24 * 60 * 60) + result.adjDiff += getDaysInMonth(curMonth, anew.year) * secondsInDay if curMonth == mDec: curMonth = mJan anew.year.inc() else: curMonth.inc() - result += float(newinterv.days * 24 * 60 * 60) - result += float(newinterv.hours * 60 * 60) - result += float(newinterv.minutes * 60) - result += float(newinterv.seconds) - result += newinterv.milliseconds / 1000 - -proc `+`*(a: TimeInfo, interval: TimeInterval): TimeInfo = - ## adds ``interval`` time from TimeInfo ``a``. + result.adjDiff += newinterv.days * secondsInDay + result.absDiff += newinterv.hours * secondsInHour + result.absDiff += newinterv.minutes * secondsInMin + result.absDiff += newinterv.seconds + result.absDiff += newinterv.milliseconds div 1000 + +proc `+`*(dt: DateTime, interval: TimeInterval): DateTime = + ## Adds ``interval`` to ``dt``. Components from ``interval`` are added + ## in the order of their size, i.e first the ``years`` component, then the ``months`` + ## component and so on. The returned ``DateTime`` will have the same timezone as the input. + ## + ## Note that when adding months, monthday overflow is allowed. This means that if the resulting + ## month doesn't have enough days it, the month will be incremented and the monthday will be + ## set to the number of days overflowed. So adding one month to `31 October` will result in `31 November`, + ## which will overflow and result in `1 December`. ## - ## **Note:** This has been only briefly tested and it may not be - ## very accurate. - let t = toSeconds(toTime(a)) - let secs = toSeconds(a, interval) - if a.timezone == 0: - result = getGMTime(fromSeconds(t + secs)) - else: - result = getLocalTime(fromSeconds(t + secs)) - -proc `-`*(a: TimeInfo, interval: TimeInterval): TimeInfo = - ## subtracts ``interval`` time from TimeInfo ``a``. - ## - ## **Note:** This has been only briefly tested, it is inaccurate especially - ## when you subtract so much that you reach the Julian calendar. - let - t = toSeconds(toTime(a)) - secs = toSeconds(a, -interval) - if a.timezone == 0: - result = getGMTime(fromSeconds(t + secs)) + ## .. code-block:: nim + ## let dt = initDateTime(30, mMar, 2017, 00, 00, 00, utc()) + ## doAssert $(dt + 1.months) == "2017-04-30T00:00:00+00:00" + ## # This is correct and happens due to monthday overflow. + ## doAssert $(dt - 1.months) == "2017-03-02T00:00:00+00:00" + let (adjDiff, absDiff) = evaluateInterval(dt, interval) + + if adjDiff.int64 != 0: + let zInfo = dt.timezone.zoneInfoFromTz(Time(dt.toAdjTime.int64 + adjDiff)) + + if absDiff != 0: + let time = Time(zInfo.adjTime.int64 + zInfo.utcOffset + absDiff) + result = initDateTime(dt.timezone.zoneInfoFromUtc(time), dt.timezone) + else: + result = initDateTime(zInfo, dt.timezone) else: - result = getLocalTime(fromSeconds(t + secs)) - -proc miliseconds*(t: TimeInterval): int {.deprecated.} = t.milliseconds + result = initDateTime(dt.timezone.zoneInfoFromUtc(Time(dt.toTime.int64 + absDiff)), dt.timezone) -proc `miliseconds=`*(t: var TimeInterval, milliseconds: int) {.deprecated.} = - ## An alias for a misspelled field in ``TimeInterval``. - ## - ## **Warning:** This should not be used! It will be removed in the next - ## version. - t.milliseconds = milliseconds +proc `-`*(dt: DateTime, interval: TimeInterval): DateTime = + ## Subtract ``interval`` from ``dt``. Components from ``interval`` are subtracted + ## in the order of their size, i.e first the ``years`` component, then the ``months`` + ## component and so on. The returned ``DateTime`` will have the same timezone as the input. + dt + (-interval) proc getDateStr*(): string {.rtl, extern: "nt$1", tags: [TimeEffect].} = - ## gets the current date as a string of the format ``YYYY-MM-DD``. - var ti = getLocalTime(getTime()) - result = $ti.year & '-' & intToStr(ord(ti.month)+1, 2) & + ## Gets the current date as a string of the format ``YYYY-MM-DD``. + var ti = now() + result = $ti.year & '-' & intToStr(ord(ti.month), 2) & '-' & intToStr(ti.monthday, 2) proc getClockStr*(): string {.rtl, extern: "nt$1", tags: [TimeEffect].} = - ## gets the current clock time as a string of the format ``HH:MM:SS``. - var ti = getLocalTime(getTime()) + ## Gets the current clock time as a string of the format ``HH:MM:SS``. + var ti = now() result = intToStr(ti.hour, 2) & ':' & intToStr(ti.minute, 2) & ':' & intToStr(ti.second, 2) proc `$`*(day: WeekDay): string = - ## stingify operator for ``WeekDay``. + ## Stringify operator for ``WeekDay``. const lookup: array[WeekDay, string] = ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"] return lookup[day] proc `$`*(m: Month): string = - ## stingify operator for ``Month``. + ## Stringify operator for ``Month``. const lookup: array[Month, string] = ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"] @@ -450,74 +668,68 @@ proc `$`*(m: Month): string = proc milliseconds*(ms: int): TimeInterval {.inline.} = ## TimeInterval of `ms` milliseconds ## - ## Note: not all time functions have millisecond resolution - initInterval(`mod`(ms,1000), `div`(ms,1000)) + ## Note: not all time procedures have millisecond resolution + initInterval(milliseconds = ms) proc seconds*(s: int): TimeInterval {.inline.} = ## TimeInterval of `s` seconds ## ## ``echo getTime() + 5.second`` - initInterval(0,`mod`(s,60), `div`(s,60)) + initInterval(seconds = s) proc minutes*(m: int): TimeInterval {.inline.} = ## TimeInterval of `m` minutes ## ## ``echo getTime() + 5.minutes`` - initInterval(0,0,`mod`(m,60), `div`(m,60)) + initInterval(minutes = m) proc hours*(h: int): TimeInterval {.inline.} = ## TimeInterval of `h` hours ## ## ``echo getTime() + 2.hours`` - initInterval(0,0,0,`mod`(h,24),`div`(h,24)) + initInterval(hours = h) proc days*(d: int): TimeInterval {.inline.} = ## TimeInterval of `d` days ## ## ``echo getTime() + 2.days`` - initInterval(0,0,0,0,d) + initInterval(days = d) proc months*(m: int): TimeInterval {.inline.} = ## TimeInterval of `m` months ## ## ``echo getTime() + 2.months`` - initInterval(0,0,0,0,0,`mod`(m,12),`div`(m,12)) + initInterval(months = m) proc years*(y: int): TimeInterval {.inline.} = ## TimeInterval of `y` years ## ## ``echo getTime() + 2.years`` - initInterval(0,0,0,0,0,0,y) + initInterval(years = y) -proc `+=`*(t: var Time, ti: TimeInterval) = - ## modifies `t` by adding the interval `ti` - t = toTime(getLocalTime(t) + ti) +proc `+=`*(time: var Time, interval: TimeInterval) = + ## Modifies `time` by adding `interval`. + time = toTime(time.local + interval) -proc `+`*(t: Time, ti: TimeInterval): Time = - ## adds the interval `ti` to Time `t` - ## by converting to localTime, adding the interval, and converting back +proc `+`*(time: Time, interval: TimeInterval): Time = + ## Adds `interval` to `time` + ## by converting to a ``DateTime`` in the local timezone, + ## adding the interval, and converting back to ``Time``. ## ## ``echo getTime() + 1.day`` - result = toTime(getLocalTime(t) + ti) + result = toTime(time.local + interval) -proc `-=`*(t: var Time, ti: TimeInterval) = - ## modifies `t` by subtracting the interval `ti` - t = toTime(getLocalTime(t) - ti) +proc `-=`*(time: var Time, interval: TimeInterval) = + ## Modifies `time` by subtracting `interval`. + time = toTime(time.local - interval) -proc `-`*(t: Time, ti: TimeInterval): Time = - ## subtracts the interval `ti` from Time `t` +proc `-`*(time: Time, interval: TimeInterval): Time = + ## Subtracts `interval` from Time `time`. ## ## ``echo getTime() - 1.day`` - result = toTime(getLocalTime(t) - ti) + result = toTime(time.local - interval) -const - secondsInMin = 60 - secondsInHour = 60*60 - secondsInDay = 60*60*24 - minutesInHour = 60 - epochStartYear = 1970 - -proc formatToken(info: TimeInfo, token: string, buf: var string) = +proc formatToken(dt: DateTime, token: string, buf: var string) = ## Helper of the format proc to parse individual tokens. ## ## Pass the found token in the user input string, and the buffer where the @@ -525,96 +737,96 @@ proc formatToken(info: TimeInfo, token: string, buf: var string) = ## formatting tokens require modifying the previous characters. case token of "d": - buf.add($info.monthday) + buf.add($dt.monthday) of "dd": - if info.monthday < 10: + if dt.monthday < 10: buf.add("0") - buf.add($info.monthday) + buf.add($dt.monthday) of "ddd": - buf.add(($info.weekday)[0 .. 2]) + buf.add(($dt.weekday)[0 .. 2]) of "dddd": - buf.add($info.weekday) + buf.add($dt.weekday) of "h": - buf.add($(if info.hour > 12: info.hour - 12 else: info.hour)) + buf.add($(if dt.hour > 12: dt.hour - 12 else: dt.hour)) of "hh": - let amerHour = if info.hour > 12: info.hour - 12 else: info.hour + let amerHour = if dt.hour > 12: dt.hour - 12 else: dt.hour if amerHour < 10: buf.add('0') buf.add($amerHour) of "H": - buf.add($info.hour) + buf.add($dt.hour) of "HH": - if info.hour < 10: + if dt.hour < 10: buf.add('0') - buf.add($info.hour) + buf.add($dt.hour) of "m": - buf.add($info.minute) + buf.add($dt.minute) of "mm": - if info.minute < 10: + if dt.minute < 10: buf.add('0') - buf.add($info.minute) + buf.add($dt.minute) of "M": - buf.add($(int(info.month)+1)) + buf.add($ord(dt.month)) of "MM": - if info.month < mOct: + if dt.month < mOct: buf.add('0') - buf.add($(int(info.month)+1)) + buf.add($ord(dt.month)) of "MMM": - buf.add(($info.month)[0..2]) + buf.add(($dt.month)[0..2]) of "MMMM": - buf.add($info.month) + buf.add($dt.month) of "s": - buf.add($info.second) + buf.add($dt.second) of "ss": - if info.second < 10: + if dt.second < 10: buf.add('0') - buf.add($info.second) + buf.add($dt.second) of "t": - if info.hour >= 12: + if dt.hour >= 12: buf.add('P') else: buf.add('A') of "tt": - if info.hour >= 12: + if dt.hour >= 12: buf.add("PM") else: buf.add("AM") of "y": - var fr = ($info.year).len()-1 + var fr = ($dt.year).len()-1 if fr < 0: fr = 0 - buf.add(($info.year)[fr .. ($info.year).len()-1]) + buf.add(($dt.year)[fr .. ($dt.year).len()-1]) of "yy": - var fr = ($info.year).len()-2 + var fr = ($dt.year).len()-2 if fr < 0: fr = 0 - var fyear = ($info.year)[fr .. ($info.year).len()-1] + var fyear = ($dt.year)[fr .. ($dt.year).len()-1] if fyear.len != 2: fyear = repeat('0', 2-fyear.len()) & fyear buf.add(fyear) of "yyy": - var fr = ($info.year).len()-3 + var fr = ($dt.year).len()-3 if fr < 0: fr = 0 - var fyear = ($info.year)[fr .. ($info.year).len()-1] + var fyear = ($dt.year)[fr .. ($dt.year).len()-1] if fyear.len != 3: fyear = repeat('0', 3-fyear.len()) & fyear buf.add(fyear) of "yyyy": - var fr = ($info.year).len()-4 + var fr = ($dt.year).len()-4 if fr < 0: fr = 0 - var fyear = ($info.year)[fr .. ($info.year).len()-1] + var fyear = ($dt.year)[fr .. ($dt.year).len()-1] if fyear.len != 4: fyear = repeat('0', 4-fyear.len()) & fyear buf.add(fyear) of "yyyyy": - var fr = ($info.year).len()-5 + var fr = ($dt.year).len()-5 if fr < 0: fr = 0 - var fyear = ($info.year)[fr .. ($info.year).len()-1] + var fyear = ($dt.year)[fr .. ($dt.year).len()-1] if fyear.len != 5: fyear = repeat('0', 5-fyear.len()) & fyear buf.add(fyear) of "z": let - nonDstTz = info.timezone - int(info.isDst) * secondsInHour + nonDstTz = dt.utcOffset hours = abs(nonDstTz) div secondsInHour if nonDstTz <= 0: buf.add('+') else: buf.add('-') buf.add($hours) of "zz": let - nonDstTz = info.timezone - int(info.isDst) * secondsInHour + nonDstTz = dt.utcOffset hours = abs(nonDstTz) div secondsInHour if nonDstTz <= 0: buf.add('+') else: buf.add('-') @@ -622,7 +834,7 @@ proc formatToken(info: TimeInfo, token: string, buf: var string) = buf.add($hours) of "zzz": let - nonDstTz = info.timezone - int(info.isDst) * secondsInHour + nonDstTz = dt.utcOffset hours = abs(nonDstTz) div secondsInHour minutes = (abs(nonDstTz) div secondsInMin) mod minutesInHour if nonDstTz <= 0: buf.add('+') @@ -638,8 +850,8 @@ proc formatToken(info: TimeInfo, token: string, buf: var string) = raise newException(ValueError, "Invalid format string: " & token) -proc format*(info: TimeInfo, f: string): string = - ## This function formats `info` as specified by `f`. The following format +proc format*(dt: DateTime, f: string): string {.tags: [].}= + ## This procedure formats `dt` as specified by `f`. The following format ## specifiers are available: ## ## ========== ================================================================================= ================================================ @@ -683,7 +895,7 @@ proc format*(info: TimeInfo, f: string): string = while true: case f[i] of ' ', '-', '/', ':', '\'', '\0', '(', ')', '[', ']', ',': - formatToken(info, currentF, result) + formatToken(dt, currentF, result) currentF = "" if f[i] == '\0': break @@ -700,187 +912,187 @@ proc format*(info: TimeInfo, f: string): string = if currentF.len < 1 or currentF[high(currentF)] == f[i]: currentF.add(f[i]) else: - formatToken(info, currentF, result) + formatToken(dt, currentF, result) dec(i) # Move position back to re-process the character separately. currentF = "" inc(i) -proc `$`*(timeInfo: TimeInfo): string {.tags: [], raises: [], benign.} = - ## converts a `TimeInfo` object to a string representation. +proc `$`*(dt: DateTime): string {.tags: [], raises: [], benign.} = + ## Converts a `DateTime` object to a string representation. ## It uses the format ``yyyy-MM-dd'T'HH-mm-sszzz``. try: - result = format(timeInfo, "yyyy-MM-dd'T'HH:mm:sszzz") # todo: optimize this + result = format(dt, "yyyy-MM-dd'T'HH:mm:sszzz") # todo: optimize this except ValueError: assert false # cannot happen because format string is valid -proc `$`*(time: Time): string {.tags: [TimeEffect], raises: [], benign.} = +proc `$`*(time: Time): string {.tags: [], raises: [], benign.} = ## converts a `Time` value to a string representation. It will use the local ## time zone and use the format ``yyyy-MM-dd'T'HH-mm-sszzz``. - $getLocalTime(time) + $time.local {.pop.} -proc parseToken(info: var TimeInfo; token, value: string; j: var int) = +proc parseToken(dt: var DateTime; token, value: string; j: var int) = ## 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 + dt.monthday = sv j += pd of "dd": - info.monthday = value[j..j+1].parseInt() + dt.monthday = value[j..j+1].parseInt() j += 2 of "ddd": case value[j..j+2].toLowerAscii() - 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 + of "sun": dt.weekday = dSun + of "mon": dt.weekday = dMon + of "tue": dt.weekday = dTue + of "wed": dt.weekday = dWed + of "thu": dt.weekday = dThu + of "fri": dt.weekday = dFri + of "sat": dt.weekday = dSat else: raise newException(ValueError, "Couldn't parse day of week (ddd), got: " & value[j..j+2]) j += 3 of "dddd": if value.len >= j+6 and value[j..j+5].cmpIgnoreCase("sunday") == 0: - info.weekday = dSun + dt.weekday = dSun j += 6 elif value.len >= j+6 and value[j..j+5].cmpIgnoreCase("monday") == 0: - info.weekday = dMon + dt.weekday = dMon j += 6 elif value.len >= j+7 and value[j..j+6].cmpIgnoreCase("tuesday") == 0: - info.weekday = dTue + dt.weekday = dTue j += 7 elif value.len >= j+9 and value[j..j+8].cmpIgnoreCase("wednesday") == 0: - info.weekday = dWed + dt.weekday = dWed j += 9 elif value.len >= j+8 and value[j..j+7].cmpIgnoreCase("thursday") == 0: - info.weekday = dThu + dt.weekday = dThu j += 8 elif value.len >= j+6 and value[j..j+5].cmpIgnoreCase("friday") == 0: - info.weekday = dFri + dt.weekday = dFri j += 6 elif value.len >= j+8 and value[j..j+7].cmpIgnoreCase("saturday") == 0: - info.weekday = dSat + dt.weekday = dSat j += 8 else: raise newException(ValueError, "Couldn't parse day of week (dddd), got: " & value) of "h", "H": var pd = parseInt(value[j..j+1], sv) - info.hour = sv + dt.hour = sv j += pd of "hh", "HH": - info.hour = value[j..j+1].parseInt() + dt.hour = value[j..j+1].parseInt() j += 2 of "m": var pd = parseInt(value[j..j+1], sv) - info.minute = sv + dt.minute = sv j += pd of "mm": - info.minute = value[j..j+1].parseInt() + dt.minute = value[j..j+1].parseInt() j += 2 of "M": var pd = parseInt(value[j..j+1], sv) - info.month = Month(sv-1) + dt.month = sv.Month j += pd of "MM": var month = value[j..j+1].parseInt() j += 2 - info.month = Month(month-1) + dt.month = month.Month of "MMM": case value[j..j+2].toLowerAscii(): - 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 + of "jan": dt.month = mJan + of "feb": dt.month = mFeb + of "mar": dt.month = mMar + of "apr": dt.month = mApr + of "may": dt.month = mMay + of "jun": dt.month = mJun + of "jul": dt.month = mJul + of "aug": dt.month = mAug + of "sep": dt.month = mSep + of "oct": dt.month = mOct + of "nov": dt.month = mNov + of "dec": dt.month = mDec else: raise newException(ValueError, "Couldn't parse month (MMM), got: " & value) j += 3 of "MMMM": if value.len >= j+7 and value[j..j+6].cmpIgnoreCase("january") == 0: - info.month = mJan + dt.month = mJan j += 7 elif value.len >= j+8 and value[j..j+7].cmpIgnoreCase("february") == 0: - info.month = mFeb + dt.month = mFeb j += 8 elif value.len >= j+5 and value[j..j+4].cmpIgnoreCase("march") == 0: - info.month = mMar + dt.month = mMar j += 5 elif value.len >= j+5 and value[j..j+4].cmpIgnoreCase("april") == 0: - info.month = mApr + dt.month = mApr j += 5 elif value.len >= j+3 and value[j..j+2].cmpIgnoreCase("may") == 0: - info.month = mMay + dt.month = mMay j += 3 elif value.len >= j+4 and value[j..j+3].cmpIgnoreCase("june") == 0: - info.month = mJun + dt.month = mJun j += 4 elif value.len >= j+4 and value[j..j+3].cmpIgnoreCase("july") == 0: - info.month = mJul + dt.month = mJul j += 4 elif value.len >= j+6 and value[j..j+5].cmpIgnoreCase("august") == 0: - info.month = mAug + dt.month = mAug j += 6 elif value.len >= j+9 and value[j..j+8].cmpIgnoreCase("september") == 0: - info.month = mSep + dt.month = mSep j += 9 elif value.len >= j+7 and value[j..j+6].cmpIgnoreCase("october") == 0: - info.month = mOct + dt.month = mOct j += 7 elif value.len >= j+8 and value[j..j+7].cmpIgnoreCase("november") == 0: - info.month = mNov + dt.month = mNov j += 8 elif value.len >= j+8 and value[j..j+7].cmpIgnoreCase("december") == 0: - info.month = mDec + dt.month = mDec j += 8 else: raise newException(ValueError, "Couldn't parse month (MMMM), got: " & value) of "s": var pd = parseInt(value[j..j+1], sv) - info.second = sv + dt.second = sv j += pd of "ss": - info.second = value[j..j+1].parseInt() + dt.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 + if value[j] == 'P' and dt.hour > 0 and dt.hour < 12: + dt.hour += 12 j += 1 of "tt": - if value[j..j+1] == "PM" and info.hour > 0 and info.hour < 12: - info.hour += 12 + if value[j..j+1] == "PM" and dt.hour > 0 and dt.hour < 12: + dt.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 + var thisCen = now().year div 100 + dt.year = thisCen*100 + year j += 2 of "yyyy": - info.year = value[j..j+3].parseInt() + dt.year = value[j..j+3].parseInt() j += 4 of "z": - info.isDST = false + dt.isDst = false if value[j] == '+': - info.timezone = 0 - parseInt($value[j+1]) * secondsInHour + dt.utcOffset = 0 - parseInt($value[j+1]) * secondsInHour elif value[j] == '-': - info.timezone = parseInt($value[j+1]) * secondsInHour + dt.utcOffset = parseInt($value[j+1]) * secondsInHour elif value[j] == 'Z': - info.timezone = 0 + dt.utcOffset = 0 j += 1 return else: @@ -888,13 +1100,13 @@ proc parseToken(info: var TimeInfo; token, value: string; j: var int) = "Couldn't parse timezone offset (z), got: " & value[j]) j += 2 of "zz": - info.isDST = false + dt.isDst = false if value[j] == '+': - info.timezone = 0 - value[j+1..j+2].parseInt() * secondsInHour + dt.utcOffset = 0 - value[j+1..j+2].parseInt() * secondsInHour elif value[j] == '-': - info.timezone = value[j+1..j+2].parseInt() * secondsInHour + dt.utcOffset = value[j+1..j+2].parseInt() * secondsInHour elif value[j] == 'Z': - info.timezone = 0 + dt.utcOffset = 0 j += 1 return else: @@ -902,31 +1114,33 @@ proc parseToken(info: var TimeInfo; token, value: string; j: var int) = "Couldn't parse timezone offset (zz), got: " & value[j]) j += 3 of "zzz": - info.isDST = false + dt.isDst = false var factor = 0 if value[j] == '+': factor = -1 elif value[j] == '-': factor = 1 elif value[j] == 'Z': - info.timezone = 0 + dt.utcOffset = 0 j += 1 return else: raise newException(ValueError, "Couldn't parse timezone offset (zzz), got: " & value[j]) - info.timezone = factor * value[j+1..j+2].parseInt() * secondsInHour + dt.utcOffset = factor * value[j+1..j+2].parseInt() * secondsInHour j += 4 - info.timezone += factor * value[j..j+1].parseInt() * 60 + dt.utcOffset += factor * value[j..j+1].parseInt() * 60 j += 2 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 as listed below. The function defaults information not provided - ## in the format string from the running program (timezone, month, year, etc). - ## Daylight saving time is only set if no timezone is given and the given date - ## lies within the DST period of the current locale. +proc parse*(value, layout: string, zone: Timezone = local()): DateTime = + ## This procedure parses a date/time string using the standard format + ## identifiers as listed below. The procedure defaults information not provided + ## in the format string from the running program (month, year, etc). + ## + ## The return value will always be in the `zone` timezone. If no UTC offset was + ## parsed, then the input will be assumed to be specified in the `zone` timezone + ## already, so no timezone conversion will be done in that case. ## ## ========== ================================================================================= ================================================ ## Specifier Description Example @@ -965,17 +1179,17 @@ proc parse*(value, layout: string): TimeInfo = 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 - info.isDST = true # using this is flag for checking whether a timezone has \ + var dt = now() + dt.hour = 0 + dt.minute = 0 + dt.second = 0 + dt.isDst = true # using this is flag for checking whether a timezone has \ # been read (because DST is always false when a tz is parsed) while true: case layout[i] of ' ', '-', '/', ':', '\'', '\0', '(', ')', '[', ']', ',': if token.len > 0: - parseToken(info, token, value, j) + parseToken(dt, token, value, j) # Reset token token = "" # Break if at end of line @@ -997,26 +1211,15 @@ proc parse*(value, layout: string): TimeInfo = token.add(layout[i]) inc(i) else: - parseToken(info, token, value, j) + parseToken(dt, token, value, j) token = "" - if info.isDST: - # means that no timezone has been parsed. In this case, we need to check - # whether the date is within DST of the local time. - let tmp = getLocalTime(toTime(info)) - # correctly set isDST so that the following step works on the correct time - info.isDST = tmp.isDST - - # Correct weekday and yearday; transform timestamp to local time. - # There currently is no way of returning this with the original (parsed) - # timezone while also setting weekday and yearday (we are depending on stdlib - # to provide this calculation). - return getLocalTime(toTime(info)) - -# Leap year calculations are adapted from: -# http://www.codeproject.com/Articles/7358/Ultra-fast-Algorithms-for-Working-with-Leap-Years -# The dayOfTheWeek procs are adapated from: -# http://stason.org/TULARC/society/calendars/2-5-What-day-of-the-week-was-2-August-1953.html + if dt.isDst: + # No timezone parsed - assume timezone is `zone` + result = initDateTime(zone.zoneInfoFromTz(dt.toAdjTime), zone) + else: + # Otherwise convert to `zone` + result = dt.toTime.inZone(zone) proc countLeapYears*(yearSpan: int): int = ## Returns the number of leap years spanned by a given number of years. @@ -1042,75 +1245,7 @@ proc countYearsAndDays*(daySpan: int): tuple[years: int, days: int] = result.years = days div 365 result.days = days mod 365 -proc getDayOfWeek*(day, month, year: int): WeekDay = - ## Returns the day of the week enum from day, month and year. - # Day & month start from one. - let - a = (14 - month) div 12 - y = year - a - m = month + (12*a) - 2 - d = (day + y + (y div 4) - (y div 100) + (y div 400) + (31*m) div 12) mod 7 - # The value of d is 0 for a Sunday, 1 for a Monday, 2 for a Tuesday, etc. - # so we must correct for the WeekDay type. - if d == 0: return dSun - result = (d-1).WeekDay - -proc getDayOfWeekJulian*(day, month, year: int): WeekDay = - ## Returns the day of the week enum from day, month and year, - ## according to the Julian calendar. - # Day & month start from one. - let - a = (14 - month) div 12 - y = year - a - m = month + (12*a) - 2 - d = (5 + day + y + (y div 4) + (31*m) div 12) mod 7 - result = d.WeekDay - -proc timeToTimeInfo*(t: Time): TimeInfo {.deprecated.} = - ## Converts a Time to TimeInfo. - ## - ## **Warning:** This procedure is deprecated since version 0.14.0. - ## Use ``getLocalTime`` or ``getGMTime`` instead. - let - secs = t.toSeconds().int - daysSinceEpoch = secs div secondsInDay - (yearsSinceEpoch, daysRemaining) = countYearsAndDays(daysSinceEpoch) - daySeconds = secs mod secondsInDay - - y = yearsSinceEpoch + epochStartYear - - var - mon = mJan - days = daysRemaining - daysInMonth = getDaysInMonth(mon, y) - - # calculate month and day remainder - while days > daysInMonth and mon <= mDec: - days -= daysInMonth - mon.inc - daysInMonth = getDaysInMonth(mon, y) - - let - yd = daysRemaining - m = mon # month is zero indexed enum - md = days - # NB: month is zero indexed but dayOfWeek expects 1 indexed. - wd = getDayOfWeek(days, mon.int + 1, y).Weekday - h = daySeconds div secondsInHour + 1 - mi = (daySeconds mod secondsInHour) div secondsInMin - s = daySeconds mod secondsInMin - result = TimeInfo(year: y, yearday: yd, month: m, monthday: md, weekday: wd, hour: h, minute: mi, second: s) - -proc timeToTimeInterval*(t: Time): TimeInterval {.deprecated.} = - ## Converts a Time to a TimeInterval. - ## - ## **Warning:** This procedure is deprecated since version 0.14.0. - ## Use ``toTimeInterval`` instead. - # Milliseconds not available from Time - var tInfo = t.getLocalTime() - initInterval(0, tInfo.second, tInfo.minute, tInfo.hour, tInfo.weekday.ord, tInfo.month.ord, tInfo.year) - -proc toTimeInterval*(t: Time): TimeInterval = +proc toTimeInterval*(time: Time): TimeInterval = ## Converts a Time to a TimeInterval. ## ## To be used when diffing times. @@ -1121,10 +1256,25 @@ proc toTimeInterval*(t: Time): TimeInterval = ## echo a, " ", b # real dates ## echo a.toTimeInterval # meaningless value, don't use it by itself ## echo b.toTimeInterval - a.toTimeInterval - ## # (milliseconds: 0, seconds: -40, minutes: -6, hours: 1, days: -2, months: -2, years: 16) + ## # (milliseconds: 0, seconds: -40, minutes: -6, hours: 1, days: 5, months: -2, years: 16) # Milliseconds not available from Time - var tInfo = t.getLocalTime() - initInterval(0, tInfo.second, tInfo.minute, tInfo.hour, tInfo.weekday.ord, tInfo.month.ord, tInfo.year) + var dt = time.local + initInterval(0, dt.second, dt.minute, dt.hour, dt.monthday, dt.month.ord - 1, dt.year) + +proc initDateTime*(monthday: MonthdayRange, month: Month, year: int, + hour: HourRange, minute: MinuteRange, second: SecondRange, zone: Timezone = local()): DateTime = + ## Create a new ``DateTime`` in the specified timezone. + assertValidDate monthday, month, year + doAssert monthday <= getDaysInMonth(month, year), "Invalid date: " & $month & " " & $monthday & ", " & $year + let dt = DateTime( + monthday: monthday, + year: year, + month: month, + hour: hour, + minute: minute, + second: second + ) + result = initDateTime(zone.zoneInfoFromTz(dt.toAdjTime), zone) when not defined(JS): proc epochTime*(): float {.rtl, extern: "nt$1", tags: [TimeEffect].} @@ -1145,160 +1295,39 @@ when not defined(JS): ## doWork() ## echo "CPU time [s] ", cpuTime() - t0 -when not defined(JS): - # C wrapper: - when defined(freebsd) or defined(netbsd) or defined(openbsd) or - defined(macosx): - type - StructTM {.importc: "struct tm".} = object - second {.importc: "tm_sec".}, - minute {.importc: "tm_min".}, - hour {.importc: "tm_hour".}, - monthday {.importc: "tm_mday".}, - month {.importc: "tm_mon".}, - year {.importc: "tm_year".}, - weekday {.importc: "tm_wday".}, - yearday {.importc: "tm_yday".}, - isdst {.importc: "tm_isdst".}: cint - gmtoff {.importc: "tm_gmtoff".}: clong - else: - type - StructTM {.importc: "struct tm".} = object - second {.importc: "tm_sec".}, - minute {.importc: "tm_min".}, - hour {.importc: "tm_hour".}, - monthday {.importc: "tm_mday".}, - month {.importc: "tm_mon".}, - year {.importc: "tm_year".}, - weekday {.importc: "tm_wday".}, - yearday {.importc: "tm_yday".}, - isdst {.importc: "tm_isdst".}: cint - when defined(linux) and defined(amd64): - gmtoff {.importc: "tm_gmtoff".}: clong - zone {.importc: "tm_zone".}: cstring +when defined(JS): + proc getTime(): Time = + (newDate().getTime() div 1000).Time + + proc epochTime*(): float {.tags: [TimeEffect].} = + newDate().getTime() / 1000 + +else: type - TimeInfoPtr = ptr StructTM Clock {.importc: "clock_t".} = distinct int - when not defined(windows): - # This is not ANSI C, but common enough - proc timegm(t: StructTM): Time {. - importc: "timegm", header: "", tags: [].} - - proc localtime(timer: ptr Time): TimeInfoPtr {. - importc: "localtime", header: "", tags: [].} - proc gmtime(timer: ptr Time): TimeInfoPtr {. - importc: "gmtime", header: "", tags: [].} - proc timec(timer: ptr Time): Time {. + proc timec(timer: ptr CTime): CTime {. importc: "time", header: "", tags: [].} - proc mktime(t: StructTM): Time {. - importc: "mktime", header: "", tags: [].} + proc getClock(): Clock {.importc: "clock", header: "", tags: [TimeEffect].} - proc difftime(a, b: Time): float {.importc: "difftime", header: "", - tags: [].} var clocksPerSec {.importc: "CLOCKS_PER_SEC", nodecl.}: int - # our own procs on top of that: - proc tmToTimeInfo(tm: StructTM, local: bool): TimeInfo = - const - weekDays: array[0..6, WeekDay] = [ - dSun, dMon, dTue, dWed, dThu, dFri, dSat] - TimeInfo(second: int(tm.second), - minute: int(tm.minute), - hour: int(tm.hour), - monthday: int(tm.monthday), - month: Month(tm.month), - year: tm.year + 1900'i32, - weekday: weekDays[int(tm.weekday)], - yearday: int(tm.yearday), - isDST: tm.isdst > 0, - timezone: if local: getTimezone() else: 0 - ) - - - proc timeInfoToTM(t: TimeInfo): StructTM = - const - weekDays: array[WeekDay, int8] = [1'i8,2'i8,3'i8,4'i8,5'i8,6'i8,0'i8] - result.second = t.second - result.minute = t.minute - result.hour = t.hour - result.monthday = t.monthday - result.month = cint(t.month) - result.year = cint(t.year - 1900) - result.weekday = weekDays[t.weekday] - result.yearday = t.yearday - result.isdst = if t.isDST: 1 else: 0 - - when not defined(useNimRtl): - proc `-` (a, b: Time): int64 = - return toBiggestInt(difftime(a, b)) - - proc getStartMilsecs(): int = - #echo "clocks per sec: ", clocksPerSec, "clock: ", int(getClock()) - #return getClock() div (clocksPerSec div 1000) - when defined(macosx): - result = toInt(toFloat(int(getClock())) / (toFloat(clocksPerSec) / 1000.0)) - else: - result = int(getClock()) div (clocksPerSec div 1000) - when false: - var a: Timeval - posix_gettimeofday(a) - result = a.tv_sec * 1000'i64 + a.tv_usec div 1000'i64 - #echo "result: ", result - - proc getTime(): Time = return timec(nil) - proc getLocalTime(t: Time): TimeInfo = - var a = t - let lt = localtime(addr(a)) - assert(not lt.isNil) - result = tmToTimeInfo(lt[], true) - # copying is needed anyway to provide reentrancity; thus - # the conversion is not expensive - - proc getGMTime(t: Time): TimeInfo = - var a = t - result = tmToTimeInfo(gmtime(addr(a))[], false) - # copying is needed anyway to provide reentrancity; thus - # the conversion is not expensive - - proc toTime(timeInfo: TimeInfo): Time = - var cTimeInfo = timeInfo # for C++ we have to make a copy - # because the header of mktime is broken in my version of libc - - result = mktime(timeInfoToTM(cTimeInfo)) - # mktime is defined to interpret the input as local time. As timeInfoToTM - # does ignore the timezone, we need to adjust this here. - result = Time(TimeImpl(result) - getTimezone() + timeInfo.timezone) - - proc timeInfoToTime(timeInfo: TimeInfo): Time = toTime(timeInfo) + proc getTime(): Time = + timec(nil).Time const epochDiff = 116444736000000000'i64 rateDiff = 10000000'i64 # 100 nsecs - proc unixTimeToWinTime*(t: Time): int64 = + proc unixTimeToWinTime*(time: CTime): int64 = ## converts a UNIX `Time` (``time_t``) to a Windows file time - result = int64(t) * rateDiff + epochDiff + result = int64(time) * rateDiff + epochDiff - proc winTimeToUnixTime*(t: int64): Time = + proc winTimeToUnixTime*(time: int64): CTime = ## converts a Windows time to a UNIX `Time` (``time_t``) - result = Time((t - epochDiff) div rateDiff) - - proc getTimezone(): int = - when defined(freebsd) or defined(netbsd) or defined(openbsd): - var a = timec(nil) - let lt = localtime(addr(a)) - # BSD stores in `gmtoff` offset east of UTC in seconds, - # but posix systems using west of UTC in seconds - return -(lt.gmtoff) - else: - return timezone - - proc fromSeconds(since1970: float): Time = Time(since1970) - - proc toSeconds(time: Time): float = float(time) + result = CTime((time - epochDiff) div rateDiff) when not defined(useNimRtl): proc epochTime(): float = @@ -1319,83 +1348,125 @@ when not defined(JS): proc cpuTime(): float = result = toFloat(int(getClock())) / toFloat(clocksPerSec) -elif defined(JS): - proc newDate(): Time {.importc: "new Date".} - proc internGetTime(): Time {.importc: "new Date", tags: [].} +# Deprecated procs - proc newDate(value: float): Time {.importc: "new Date".} - proc newDate(value: cstring): Time {.importc: "new Date".} - proc getTime(): Time = - # Warning: This is something different in JS. - return newDate() +proc fromSeconds*(since1970: float): Time {.tags: [], raises: [], benign, deprecated.} = + ## Takes a float which contains the number of seconds since the unix epoch and + ## returns a time object. + Time(since1970) - const - weekDays: array[0..6, WeekDay] = [ - dSun, dMon, dTue, dWed, dThu, dFri, dSat] - - proc getLocalTime(t: Time): TimeInfo = - result.second = t.getSeconds() - result.minute = t.getMinutes() - result.hour = t.getHours() - result.monthday = t.getDate() - result.month = Month(t.getMonth()) - result.year = t.getFullYear() - result.weekday = weekDays[t.getDay()] - result.timezone = getTimezone() - - result.yearday = result.monthday - 1 - for month in mJan.. daysInMonth and mon <= mDec: + days -= daysInMonth + mon.inc + daysInMonth = getDaysInMonth(mon, y) + + let + yd = daysRemaining + m = mon # month is zero indexed enum + md = days + # NB: month is zero indexed but dayOfWeek expects 1 indexed. + wd = getDayOfWeek(days, mon, y).Weekday + h = daySeconds div secondsInHour + 1 + mi = (daySeconds mod secondsInHour) div secondsInMin + s = daySeconds mod secondsInMin + result = DateTime(year: y, yearday: yd, month: m, monthday: md, weekday: wd, hour: h, minute: mi, second: s) + +proc getDayOfWeek*(day, month, year: int): WeekDay {.tags: [], raises: [], benign, deprecated.} = + getDayOfWeek(day, month.Month, year) + +proc getDayOfWeekJulian*(day, month, year: int): WeekDay {.deprecated.} = + ## Returns the day of the week enum from day, month and year, + ## according to the Julian calendar. + # Day & month start from one. + let + a = (14 - month) div 12 + y = year - a + m = month + (12*a) - 2 + d = (5 + day + y + (y div 4) + (31*m) div 12) mod 7 + result = d.WeekDay \ No newline at end of file diff --git a/tests/js/ttimes.nim b/tests/js/ttimes.nim index 2868c6d0f..63a4bb04f 100644 --- a/tests/js/ttimes.nim +++ b/tests/js/ttimes.nim @@ -1,4 +1,3 @@ -# test times module with js discard """ action: run """ @@ -9,21 +8,37 @@ import times # Tue 19 Jan 03:14:07 GMT 2038 block yeardayTest: - # check if yearday attribute is properly set on TimeInfo creation - doAssert fromSeconds(2147483647).getGMTime().yearday == 18 - -block localTimezoneTest: - # check if timezone is properly set during Time to TimeInfo conversion - doAssert fromSeconds(2147483647).getLocalTime().timezone == getTimezone() - -block timestampPersistenceTest: - # check if timestamp persists during TimeInfo to Time conversion - const - timeString = "2017-03-21T12:34:56+03:00" - timeStringGmt = "2017-03-21T09:34:56+00:00" - timeStringGmt2 = "2017-03-21T08:34:56+00:00" - fmt = "yyyy-MM-dd'T'HH:mm:sszzz" - # XXX Check which one is the right solution here: - - let x = $timeString.parse(fmt).toTime().getGMTime() - doAssert x == timeStringGmt or x == timeStringGmt2 + doAssert fromUnix(2147483647).utc.yearday == 18 + +block localTime: + var local = now() + let utc = local.utc + doAssert local.toTime == utc.toTime + +let a = fromUnix(1_000_000_000) +let b = fromUnix(1_500_000_000) +doAssert b - a == 500_000_000 + +# Because we can't change the timezone JS uses, we define a simple static timezone for testing. + +proc staticZoneInfoFromUtc(time: Time): ZonedTime = + result.utcOffset = -7200 + result.isDst = false + result.adjTime = (time.toUnix + 7200).Time + +proc staticZoneInfoFromTz(adjTime: Time): ZonedTIme = + result.utcOffset = -7200 + result.isDst = false + result.adjTime = adjTime + +let utcPlus2 = Timezone(zoneInfoFromUtc: staticZoneInfoFromUtc, zoneInfoFromTz: staticZoneInfoFromTz, name: "") + +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.inZone(utcPlus2) == $dt + +doAssert $initDateTime(01, mJan, 1911, 12, 00, 00, utc()) == "1911-01-01T12:00:00+00:00" +# See #6752 +# doAssert $initDateTime(01, mJan, 1900, 12, 00, 00, utc()) == "0023-01-01T12:00:00+00:00" \ No newline at end of file diff --git a/tests/stdlib/ttimes.nim b/tests/stdlib/ttimes.nim index 84e00f8de..a6ac186cc 100644 --- a/tests/stdlib/ttimes.nim +++ b/tests/stdlib/ttimes.nim @@ -1,16 +1,17 @@ # test the new time module discard """ file: "ttimes.nim" - action: "run" + output: '''[Suite] ttimes +''' """ import - times, strutils + times, os, strutils, unittest # $ date --date='@2147483647' # Tue 19 Jan 03:14:07 GMT 2038 -proc checkFormat(t: TimeInfo, format, expected: string) = +proc checkFormat(t: DateTime, format, expected: string) = let actual = t.format(format) if actual != expected: echo "Formatting failure!" @@ -18,7 +19,7 @@ proc checkFormat(t: TimeInfo, format, expected: string) = echo "actual : ", actual doAssert false -let t = getGMTime(fromSeconds(2147483647)) +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" & @@ -27,107 +28,41 @@ t.checkFormat("d dd ddd dddd h hh H HH m mm M MM MMM MMMM s" & t.checkFormat("yyyyMMddhhmmss", "20380119031407") -let t2 = getGMTime(fromSeconds(160070789)) # 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") -var t4 = getGMTime(fromSeconds(876124714)) # Mon 6 Oct 08:58:34 BST 1997 +var t4 = fromUnix(876124714).utc # Mon 6 Oct 08:58:34 BST 1997 t4.checkFormat("M MM MMM MMMM", "10 10 Oct October") # Interval tests (t4 - initInterval(years = 2)).checkFormat("yyyy", "1995") (t4 - initInterval(years = 7, minutes = 34, seconds = 24)).checkFormat("yyyy mm ss", "1990 24 10") -proc parseTest(s, f, sExpected: string, ydExpected: int) = - let - parsed = s.parse(f) - parsedStr = $getGMTime(toTime(parsed)) - if parsedStr != sExpected: - echo "Parsing failure!" - echo "expected: ", sExpected - echo "actual : ", parsedStr - doAssert false - doAssert(parsed.yearday == ydExpected) -proc parseTestTimeOnly(s, f, sExpected: string) = - doAssert(sExpected in $s.parse(f)) - -# because setting a specific timezone for testing is platform-specific, we use -# explicit timezone offsets in all tests. - -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) -# 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) -# 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) -# 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 -# 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) -# 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 -# 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) -# 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 -# 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) -# 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) -# 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) -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) -# 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 - # checking dayOfWeek matches known days -doAssert getDayOfWeek(21, 9, 1900) == dFri -doAssert getDayOfWeek(1, 1, 1970) == dThu -doAssert getDayOfWeek(21, 9, 1970) == dMon -doAssert getDayOfWeek(1, 1, 2000) == dSat -doAssert getDayOfWeek(1, 1, 2021) == dFri -# Julian tests -doAssert getDayOfWeekJulian(21, 9, 1900) == dFri -doAssert getDayOfWeekJulian(21, 9, 1970) == dMon -doAssert getDayOfWeekJulian(1, 1, 2000) == dSat -doAssert getDayOfWeekJulian(1, 1, 2021) == dFri - -# toSeconds tests with GM timezone -let t4L = getGMTime(fromSeconds(876124714)) -doAssert toSeconds(toTime(t4L)) == 876124714 -doAssert toSeconds(toTime(t4L)) + t4L.timezone.float == toSeconds(toTime(t4)) +doAssert getDayOfWeek(01, mJan, 0000) == dSat +doAssert getDayOfWeek(01, mJan, -0023) == dSat +doAssert getDayOfWeek(21, mSep, 1900) == dFri +doAssert getDayOfWeek(01, mJan, 1970) == dThu +doAssert getDayOfWeek(21, mSep, 1970) == dMon +doAssert getDayOfWeek(01, mJan, 2000) == dSat +doAssert getDayOfWeek(01, mJan, 2021) == dFri + +# toUnix tests with GM timezone +let t4L = fromUnix(876124714).utc +doAssert toUnix(toTime(t4L)) == 876124714 +doAssert toUnix(toTime(t4L)) + t4L.utcOffset == toUnix(toTime(t4)) # adding intervals var - a1L = toSeconds(toTime(t4L + initInterval(hours = 1))) + t4L.timezone.float - a1G = toSeconds(toTime(t4)) + 60.0 * 60.0 + a1L = toUnix(toTime(t4L + initInterval(hours = 1))) + t4L.utcOffset + a1G = toUnix(toTime(t4)) + 60 * 60 doAssert a1L == a1G # subtracting intervals -a1L = toSeconds(toTime(t4L - initInterval(hours = 1))) + t4L.timezone.float -a1G = toSeconds(toTime(t4)) - (60.0 * 60.0) +a1L = toUnix(toTime(t4L - initInterval(hours = 1))) + t4L.utcOffset +a1G = toUnix(toTime(t4)) - (60 * 60) doAssert a1L == a1G # add/subtract TimeIntervals and Time/TimeInfo @@ -143,45 +78,16 @@ doAssert ti1 == getTime() ti1 += 1.days doAssert ti1 == getTime() + 1.days -# overflow of TimeIntervals on initalisation -doAssert initInterval(milliseconds = 25000) == initInterval(seconds = 25) -doAssert initInterval(seconds = 65) == initInterval(seconds = 5, minutes = 1) -doAssert initInterval(hours = 25) == initInterval(hours = 1, days = 1) -doAssert initInterval(months = 13) == initInterval(months = 1, years = 1) - # Bug with adding a day to a Time let day = 24.hours let tomorrow = getTime() + day doAssert tomorrow - getTime() == 60*60*24 -doAssert milliseconds(1000 * 60) == minutes(1) -doAssert milliseconds(1000 * 60 * 60) == hours(1) -doAssert milliseconds(1000 * 60 * 60 * 24) == days(1) -doAssert seconds(60 * 60) == hours(1) -doAssert seconds(60 * 60 * 24) == days(1) -doAssert seconds(60 * 60 + 65) == (hours(1) + minutes(1) + seconds(5)) - -# 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 -# are testing that be relying on the fact that tranforming a TimeInfo to a Time -# and back again will correctly set the DST value. With the incorrect parse -# behavior this will introduce a one hour offset from the named time and the -# parsed time if the DST value differs between the current time and the date we -# are parsing. -# -# Unfortunately these tests depend on the locale of the system in which they -# are run. They will not be meaningful when run in a locale without DST. They -# also assume that Jan. 1 and Jun. 1 will have differing isDST values. -let dstT1 = parse("2016-01-01 00:00:00", "yyyy-MM-dd HH:mm:ss") -let dstT2 = parse("2016-06-01 00:00:00", "yyyy-MM-dd HH:mm:ss") -doAssert dstT1 == getLocalTime(toTime(dstT1)) -doAssert dstT2 == getLocalTime(toTime(dstT2)) - # Comparison between Time objects should be detected by compiler # as 'noSideEffect'. proc cmpTimeNoSideEffect(t1: Time, t2: Time): bool {.noSideEffect.} = result = t1 == t2 -doAssert cmpTimeNoSideEffect(0.fromSeconds, 0.fromSeconds) +doAssert cmpTimeNoSideEffect(0.fromUnix, 0.fromUnix) # Additionally `==` generic for seq[T] has explicit 'noSideEffect' pragma # so we can check above condition by comparing seq[Time] sequences let seqA: seq[Time] = @[] @@ -195,49 +101,197 @@ for tz in [ (-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 = TimeInfo(monthday: 1, timezone: tz[0]) + 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] -block formatDst: - var ti = TimeInfo(monthday: 1, isDst: true) - - # BST - ti.timezone = 0 - doAssert ti.format("z") == "+1" - doAssert ti.format("zz") == "+01" - doAssert ti.format("zzz") == "+01:00" - - # EDT - ti.timezone = 5 * 60 * 60 - doAssert ti.format("z") == "-4" - doAssert ti.format("zz") == "-04" - doAssert ti.format("zzz") == "-04:00" - -block dstTest: - let nonDst = TimeInfo(year: 2015, month: mJan, monthday: 01, yearday: 0, - weekday: dThu, hour: 00, minute: 00, second: 00, isDST: false, timezone: 0) - var dst = nonDst - dst.isDst = true - # note that both isDST == true and isDST == false are valid here because - # DST is in effect on January 1st in some southern parts of Australia. - # FIXME: Fails in UTC - # doAssert nonDst.toTime() - dst.toTime() == 3600 - doAssert nonDst.format("z") == "+0" - doAssert dst.format("z") == "+1" - - # parsing will set isDST in relation to the local time. We take a date in - # January and one in July to maximize the probability to hit one date with DST - # and one without on the local machine. However, this is not guaranteed. - 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) == fromSeconds(1451962800) - doAssert toTime(parsedJul) == fromSeconds(1467342000) - block countLeapYears: # 1920, 2004 and 2020 are leap years, and should be counted starting at the following year doAssert countLeapYears(1920) + 1 == countLeapYears(1921) doAssert countLeapYears(2004) + 1 == countLeapYears(2005) - doAssert countLeapYears(2020) + 1 == countLeapYears(2021) \ No newline at end of file + doAssert countLeapYears(2020) + 1 == countLeapYears(2021) + +block timezoneConversion: + var l = now() + let u = l.utc + l = u.local + + doAssert l.timezone == local() + doAssert u.timezone == utc() + +template parseTest(s, f, sExpected: string, ydExpected: int) = + let + parsed = s.parse(f, utc()) + parsedStr = $parsed + check parsedStr == sExpected + if parsed.yearday != ydExpected: + echo s + echo parsed.repr + echo parsed.yearday, " exp: ", ydExpected + check(parsed.yearday == ydExpected) + +template parseTestTimeOnly(s, f, sExpected: string) = + check sExpected in $s.parse(f, utc()) + +# because setting a specific timezone for testing is platform-specific, we use +# 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) + # 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) + # 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) + # 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 + # 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) + # 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 + # 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) + # 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 + # 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) + # 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) + # 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) + 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) + # 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 + # are testing that be relying on the fact that tranforming a TimeInfo to a Time + # and back again will correctly set the DST value. With the incorrect parse + # behavior this will introduce a one hour offset from the named time and the + # parsed time if the DST value differs between the current time and the date we + # are parsing. + let dstT1 = parse("2016-01-01 00:00:00", "yyyy-MM-dd HH:mm:ss") + let dstT2 = parse("2016-06-01 00:00:00", "yyyy-MM-dd HH:mm:ss") + check dstT1 == toTime(dstT1).local + check dstT2 == toTime(dstT2).local + + block dstTest: + # parsing will set isDST in relation to the local time. We take a date in + # January and one in July to maximize the probability to hit one date with DST + # and one without on the local machine. However, this is not guaranteed. + 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) == fromUnix(1451962800) + doAssert toTime(parsedJul) == fromUnix(1467342000) + +suite "ttimes": + + # Generate tests for multiple timezone files where available + # Set the TZ env var for each test + when defined(Linux) or defined(macosx): + const tz_dir = "/usr/share/zoneinfo" + const f = "yyyy-MM-dd HH:mm zzz" + + let orig_tz = getEnv("TZ") + var tz_cnt = 0 + for tz_fn in walkFiles(tz_dir & "/*"): + if symlinkExists(tz_fn) or tz_fn.endsWith(".tab") or + tz_fn.endsWith(".list"): + continue + + test "test for " & tz_fn: + tz_cnt.inc + putEnv("TZ", tz_fn) + runTimezoneTests() + + test "enough timezone files tested": + check tz_cnt > 10 + + test "dst handling": + putEnv("TZ", "Europe/Stockholm") + # In case of an impossible time, the time is moved to after the impossible time period + check initDateTime(26, mMar, 2017, 02, 30, 00).format(f) == "2017-03-26 03:30 +02:00" + # In case of an ambiguous time, the earlier time is choosen + check initDateTime(29, mOct, 2017, 02, 00, 00).format(f) == "2017-10-29 02:00 +02:00" + # These are just dates on either side of the dst switch + check initDateTime(29, mOct, 2017, 01, 00, 00).format(f) == "2017-10-29 01:00 +02:00" + check initDateTime(29, mOct, 2017, 01, 00, 00).isDst + check initDateTime(29, mOct, 2017, 03, 01, 00).format(f) == "2017-10-29 03:01 +01:00" + check (not initDateTime(29, mOct, 2017, 03, 01, 00).isDst) + + check initDateTime(21, mOct, 2017, 01, 00, 00).format(f) == "2017-10-21 01:00 +02:00" + + test "issue #6520": + putEnv("TZ", "Europe/Stockholm") + var local = fromUnix(1469275200).local + var utc = fromUnix(1469275200).utc + + let claimedOffset = local.utcOffset + local.utcOffset = 0 + check claimedOffset == utc.toTime - local.toTime + + test "issue #5704": + putEnv("TZ", "Asia/Seoul") + let diff = parse("19700101-000000", "yyyyMMdd-hhmmss").toTime - parse("19000101-000000", "yyyyMMdd-hhmmss").toTime + check diff == 2208986872 + + test "issue #6465": + putEnv("TZ", "Europe/Stockholm") + let dt = parse("2017-03-25 12:00", "yyyy-MM-dd hh:mm") + check $(dt + 1.days) == "2017-03-26T12:00:00+02:00" + + test "datetime before epoch": + check $fromUnix(-2147483648).utc == "1901-12-13T20:45:52+00:00" + + test "adding/subtracting time across dst": + putenv("TZ", "Europe/Stockholm") + + let dt1 = initDateTime(26, mMar, 2017, 03, 00, 00) + check $(dt1 - 1.seconds) == "2017-03-26T01:59:59+01:00" + + var dt2 = initDateTime(29, mOct, 2017, 02, 59, 59) + check $(dt2 + 1.seconds) == "2017-10-29T02:00:00+01:00" + + putEnv("TZ", orig_tz) + + else: + # not on Linux or macosx: run one parseTest only + test "parseTest": + runTimezoneTests() + + test "isLeapYear": + check isLeapYear(2016) + check (not isLeapYear(2015)) + check isLeapYear(2000) + check (not isLeapYear(1900)) + + test "subtract months": + var dt = initDateTime(1, mFeb, 2017, 00, 00, 00, utc()) + check $(dt - 1.months) == "2017-01-01T00:00:00+00:00" + dt = initDateTime(15, mMar, 2017, 00, 00, 00, utc()) + check $(dt - 1.months) == "2017-02-15T00:00:00+00:00" + dt = initDateTime(31, mMar, 2017, 00, 00, 00, utc()) + # This happens due to monthday overflow. It's consistent with Phobos. + check $(dt - 1.months) == "2017-03-03T00:00:00+00:00" \ No newline at end of file -- cgit 1.4.1-2-gfad0 From a89b81eb9679d1351aceae766e5eaf1bf8faeffe Mon Sep 17 00:00:00 2001 From: skilchen Date: Thu, 21 Dec 2017 16:32:26 +0100 Subject: fixes #6353 (#6951) --- lib/pure/math.nim | 17 +++++++++++++---- tests/stdlib/tfrexp1.nim | 44 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 57 insertions(+), 4 deletions(-) create mode 100644 tests/stdlib/tfrexp1.nim (limited to 'tests/stdlib') diff --git a/lib/pure/math.nim b/lib/pure/math.nim index a9dabfa48..cbd04a145 100644 --- a/lib/pure/math.nim +++ b/lib/pure/math.nim @@ -351,15 +351,19 @@ proc round*[T: float32|float64](x: T, places: int = 0): T = result = round0(x*mult)/mult when not defined(JS): - proc frexp*(x: float32, exponent: var int): float32 {. + proc c_frexp*(x: float32, exponent: var int32): float32 {. importc: "frexp", header: "".} - proc frexp*(x: float64, exponent: var int): float64 {. + proc c_frexp*(x: float64, exponent: var int32): float64 {. importc: "frexp", header: "".} + proc frexp*[T, U](x: T, exponent: var U): T = ## Split a number into mantissa and exponent. ## `frexp` calculates the mantissa m (a float greater than or equal to 0.5 ## and less than 1) and the integer value n such that `x` (the original ## float value) equals m * 2**n. frexp stores n in `exponent` and returns ## m. + var exp: int32 + result = c_frexp(x, exp) + exponent = exp else: proc frexp*[T: float32|float64](x: T, exponent: var int): T = if x == 0.0: @@ -368,9 +372,14 @@ else: elif x < 0.0: result = -frexp(-x, exponent) else: - var ex = floor(log2(x)) - exponent = round(ex) + var ex = trunc(log2(x)) + exponent = int(ex) result = x / pow(2.0, ex) + if abs(result) >= 1: + inc(exponent) + result = result / 2 + if exponent == 1024 and result == 0.0: + result = 0.99999999999999988898 proc splitDecimal*[T: float32|float64](x: T): tuple[intpart: T, floatpart: T] = ## Breaks `x` into an integral and a fractional part. diff --git a/tests/stdlib/tfrexp1.nim b/tests/stdlib/tfrexp1.nim new file mode 100644 index 000000000..c6bb2b38c --- /dev/null +++ b/tests/stdlib/tfrexp1.nim @@ -0,0 +1,44 @@ +discard """ + targets: "js c c++" + output: '''ok''' +""" + +import math +import strformat + +const manualTest = false + +proc frexp_test(lo, hi, step: float64) = + var exp: int + var frac: float64 + + var eps = 1e-15.float64 + + var x:float64 = lo + while x <= hi: + frac = frexp(x.float, exp) + let rslt = pow(2.0, float(exp)) * frac + + doAssert(abs(rslt - x) < eps) + + when manualTest: + echo fmt("x: {x:10.3f} exp: {exp:4d} frac: {frac:24.20f} check: {$(abs(rslt - x) < eps):-5s} {rslt: 9.3f}") + x += step + +when manualTest: + var exp: int + var frac: float64 + + for flval in [1.7976931348623157e+308, -1.7976931348623157e+308, # max, min float64 + 3.4028234663852886e+38, -3.4028234663852886e+38, # max, min float32 + 4.9406564584124654e-324, -4.9406564584124654e-324, # smallest/largest positive/negative float64 + 1.4012984643248171e-45, -1.4012984643248171e-45, # smallest/largest positive/negative float32 + 2.2250738585072014e-308, 1.1754943508222875e-38]: # smallest normal float64/float32 + frac = frexp(flval, exp) + echo fmt("{flval:25.16e}, {exp: 6d}, {frac: .20f} {frac * pow(2.0, float(exp)): .20e}") + + frexp_test(-1000.0, 1000.0, 0.0125) +else: + frexp_test(-1000000.0, 1000000.0, 0.125) + +echo "ok" -- cgit 1.4.1-2-gfad0 From 6bd3a2826fb559d4e88cd2fa5ba89be995553700 Mon Sep 17 00:00:00 2001 From: Mathias Stearn Date: Sun, 24 Dec 2017 09:23:17 -0500 Subject: cmp(x, y: string) now uses memcmp rather than strcmp (#6869) (#6968) --- lib/system.nim | 5 ++++- lib/system/sysstr.nim | 8 ++++---- tests/stdlib/tstring.nim | 20 ++++++++++++++++++++ 3 files changed, 28 insertions(+), 5 deletions(-) (limited to 'tests/stdlib') diff --git a/lib/system.nim b/lib/system.nim index 83e87683a..85643891b 100644 --- a/lib/system.nim +++ b/lib/system.nim @@ -2916,7 +2916,10 @@ when not defined(JS): #and not defined(nimscript): elif x > y: result = 1 else: result = 0 else: - result = int(c_strcmp(x, y)) + let minlen = min(x.len, y.len) + result = int(c_memcmp(x.cstring, y.cstring, minlen.csize)) + if result == 0: + result = x.len - y.len when defined(nimscript): proc readFile*(filename: string): string {.tags: [ReadIOEffect], benign.} diff --git a/lib/system/sysstr.nim b/lib/system/sysstr.nim index 56b8ade97..4c5f3d9a1 100644 --- a/lib/system/sysstr.nim +++ b/lib/system/sysstr.nim @@ -24,10 +24,10 @@ proc cmpStrings(a, b: NimString): int {.inline, compilerProc.} = if a == b: return 0 if a == nil: return -1 if b == nil: return 1 - when defined(nimNoArrayToCstringConversion): - return c_strcmp(addr a.data, addr b.data) - else: - return c_strcmp(a.data, b.data) + let minlen = min(a.len, b.len) + result = c_memcmp(addr a.data, addr b.data, minlen.csize) + if result == 0: + result = a.len - b.len proc eqStrings(a, b: NimString): bool {.inline, compilerProc.} = if a == b: return true diff --git a/tests/stdlib/tstring.nim b/tests/stdlib/tstring.nim index 904bc462a..660746150 100644 --- a/tests/stdlib/tstring.nim +++ b/tests/stdlib/tstring.nim @@ -56,4 +56,24 @@ proc test_string_slice() = echo("OK") +proc test_string_cmp() = + let world = "hello\0world" + let earth = "hello\0earth" + let short = "hello\0" + let hello = "hello" + let goodbye = "goodbye" + + doAssert world == world + doAssert world != earth + doAssert world != short + doAssert world != hello + doAssert world != goodbye + + doAssert cmp(world, world) == 0 + doAssert cmp(world, earth) > 0 + doAssert cmp(world, short) > 0 + doAssert cmp(world, hello) > 0 + doAssert cmp(world, goodbye) > 0 + test_string_slice() +test_string_cmp() -- cgit 1.4.1-2-gfad0 From 8bcaadc9e4c10aad17339f9cea69418dd5d2bdf8 Mon Sep 17 00:00:00 2001 From: Araq Date: Wed, 3 Jan 2018 13:31:03 +0100 Subject: memfiles: enable test; refs #6361 --- tests/stdlib/tmemfiles2.nim | 2 -- 1 file changed, 2 deletions(-) (limited to 'tests/stdlib') diff --git a/tests/stdlib/tmemfiles2.nim b/tests/stdlib/tmemfiles2.nim index 665e92e8a..7ea94cffc 100644 --- a/tests/stdlib/tmemfiles2.nim +++ b/tests/stdlib/tmemfiles2.nim @@ -1,10 +1,8 @@ discard """ file: "tmemfiles2.nim" - disabled: true output: '''Full read size: 20 Half read size: 10 Data: Hello''' """ -# doesn't work on windows. fmReadWrite doesn't create a file. import memfiles, os var mm, mm_full, mm_half: MemFile -- cgit 1.4.1-2-gfad0