summary refs log tree commit diff stats
path: root/lib/pure/strformat.nim
diff options
context:
space:
mode:
Diffstat (limited to 'lib/pure/strformat.nim')
-rw-r--r--lib/pure/strformat.nim383
1 files changed, 236 insertions, 147 deletions
diff --git a/lib/pure/strformat.nim b/lib/pure/strformat.nim
index 180cbcbec..36404cdf7 100644
--- a/lib/pure/strformat.nim
+++ b/lib/pure/strformat.nim
@@ -11,12 +11,58 @@
 String `interpolation`:idx: / `format`:idx: inspired by
 Python's ``f``-strings.
 
-Examples:
+``fmt`` vs. ``&``
+=================
+
+You can use either ``fmt`` or the unary ``&`` operator for formatting. The
+difference between them is subtle but important.
+
+The ``fmt"{expr}"`` syntax is more aesthetically pleasing, but it hides a small
+gotcha. The string is a
+`generalized raw string literal <manual.html#lexical-analysis-generalized-raw-string-literals>`_.
+This has some surprising effects:
+
+.. code-block:: nim
+
+    import strformat
+    let msg = "hello"
+    doAssert fmt"{msg}\n" == "hello\\n"
+
+Because the literal is a raw string literal, the ``\n`` is not interpreted as
+an escape sequence.
+
+There are multiple ways to get around this, including the use of the ``&``
+operator:
+
+.. code-block:: nim
+
+    import strformat
+    let msg = "hello"
+
+    doAssert &"{msg}\n" == "hello\n"
+
+    doAssert fmt"{msg}{'\n'}" == "hello\n"
+    doAssert fmt("{msg}\n") == "hello\n"
+    doAssert "{msg}\n".fmt == "hello\n"
+
+The choice of style is up to you.
+
+Formatting strings
+==================
 
 .. code-block:: nim
 
-    doAssert fmt"""{"abc":>4}""" == " abc"
-    doAssert fmt"""{"abc":<4}""" == "abc "
+    import strformat
+
+    doAssert &"""{"abc":>4}""" == " abc"
+    doAssert &"""{"abc":<4}""" == "abc "
+
+Formatting floats
+=================
+
+.. code-block:: nim
+
+    import strformat
 
     doAssert fmt"{-12345:08}" == "-0012345"
     doAssert fmt"{-1:3}" == " -1"
@@ -35,7 +81,10 @@ Examples:
     doAssert fmt"{123.456:13e}" == " 1.234560e+02"
 
 
-An expression like ``fmt"{key} is {value:arg} {{z}}"`` is transformed into:
+Implementation details
+======================
+
+An expression like ``&"{key} is {value:arg} {{z}}"`` is transformed into:
 
 .. code-block:: nim
   var temp = newStringOfCap(educatedCapGuess)
@@ -48,13 +97,13 @@ An expression like ``fmt"{key} is {value:arg} {{z}}"`` is transformed into:
 Parts of the string that are enclosed in the curly braces are interpreted
 as Nim code, to escape an ``{`` or ``}`` double it.
 
-``fmt`` delegates most of the work to an open overloaded set
+``&`` delegates most of the work to an open overloaded set
 of ``format`` procs. The required signature for a type ``T`` that supports
 formatting is usually ``proc format(x: T; result: var string)`` for efficiency
 but can also be ``proc format(x: T): string``. ``add`` and ``$`` procs are
 used as the fallback implementation.
 
-This is the concrete lookup algorithm that ``fmt`` uses:
+This is the concrete lookup algorithm that ``&`` uses:
 
 .. code-block:: nim
 
@@ -69,7 +118,7 @@ This is the concrete lookup algorithm that ``fmt`` uses:
 
 
 The subexpression after the colon
-(``arg`` in ``fmt"{key} is {value:arg} {{z}}"``) is an optional argument
+(``arg`` in ``&"{key} is {value:arg} {{z}}"``) is an optional argument
 passed to ``format``.
 
 If an optional argument is present the following lookup algorithm is used:
@@ -86,8 +135,8 @@ For strings and numeric types the optional argument is a so-called
 "standard format specifier".
 
 
-Standard format specifier
-=========================
+Standard format specifier for strings, integers and floats
+==========================================================
 
 
 The general form of a standard format specifier is::
@@ -221,138 +270,15 @@ template callFormat(res, arg) {.dirty.} =
 template callFormatOption(res, arg, option) {.dirty.} =
   when compiles(format(arg, option, res)):
     format(arg, option, res)
-  else:
+  elif compiles(format(arg, option)):
     res.add format(arg, option)
+  else:
+    format($arg, option, res)
 
-macro fmt*(pattern: string): untyped =
-  ## For a specification of the ``fmt`` macro, see the module level documentation.
-  runnableExamples:
-    template check(actual, expected: string) =
-      doAssert actual == expected
-
-    from strutils import toUpperAscii, repeat
-
-    # Basic tests
-    let s = "string"
-    check fmt"{0} {s}", "0 string"
-    check fmt"{s[0..2].toUpperAscii}", "STR"
-    check fmt"{-10:04}", "-010"
-    check fmt"{-10:<04}", "-010"
-    check fmt"{-10:>04}", "-010"
-    check fmt"0x{10:02X}", "0x0A"
-
-    check fmt"{10:#04X}", "0x0A"
-
-    check fmt"""{"test":#>5}""", "#test"
-    check fmt"""{"test":>5}""", " test"
-
-    check fmt"""{"test":#^7}""", "#test##"
-
-    check fmt"""{"test": <5}""", "test "
-    check fmt"""{"test":<5}""", "test "
-    check fmt"{1f:.3f}", "1.000"
-    check fmt"Hello, {s}!", "Hello, string!"
-
-    # Tests for identifers without parenthesis
-    check fmt"{s} works{s}", "string worksstring"
-    check fmt"{s:>7}", " string"
-    doAssert(not compiles(fmt"{s_works}")) # parsed as identifier `s_works`
-
-    # Misc general tests
-    check fmt"{{}}", "{}"
-    check fmt"{0}%", "0%"
-    check fmt"{0}%asdf", "0%asdf"
-    check fmt("\n{\"\\n\"}\n"), "\n\n\n"
-    check fmt"""{"abc"}s""", "abcs"
-
-    # String tests
-    check fmt"""{"abc"}""", "abc"
-    check fmt"""{"abc":>4}""", " abc"
-    check fmt"""{"abc":<4}""", "abc "
-    check fmt"""{"":>4}""", "    "
-    check fmt"""{"":<4}""", "    "
-
-    # Int tests
-    check fmt"{12345}", "12345"
-    check fmt"{ - 12345}", "-12345"
-    check fmt"{12345:6}", " 12345"
-    check fmt"{12345:>6}", " 12345"
-    check fmt"{12345:4}", "12345"
-    check fmt"{12345:08}", "00012345"
-    check fmt"{-12345:08}", "-0012345"
-    check fmt"{0:0}", "0"
-    check fmt"{0:02}", "00"
-    check fmt"{-1:3}", " -1"
-    check fmt"{-1:03}", "-01"
-    check fmt"{10}", "10"
-    check fmt"{16:#X}", "0x10"
-    check fmt"{16:^#7X}", " 0x10  "
-    check fmt"{16:^+#7X}", " +0x10 "
-
-    # Hex tests
-    check fmt"{0:x}", "0"
-    check fmt"{-0:x}", "0"
-    check fmt"{255:x}", "ff"
-    check fmt"{255:X}", "FF"
-    check fmt"{-255:x}", "-ff"
-    check fmt"{-255:X}", "-FF"
-    check fmt"{255:x} uNaffeCteD CaSe", "ff uNaffeCteD CaSe"
-    check fmt"{255:X} uNaffeCteD CaSe", "FF uNaffeCteD CaSe"
-    check fmt"{255:4x}", "  ff"
-    check fmt"{255:04x}", "00ff"
-    check fmt"{-255:4x}", " -ff"
-    check fmt"{-255:04x}", "-0ff"
-
-    # Float tests
-    check fmt"{123.456}", "123.456"
-    check fmt"{-123.456}", "-123.456"
-    check fmt"{123.456:.3f}", "123.456"
-    check fmt"{123.456:+.3f}", "+123.456"
-    check fmt"{-123.456:+.3f}", "-123.456"
-    check fmt"{-123.456:.3f}", "-123.456"
-    check fmt"{123.456:1g}", "123.456"
-    check fmt"{123.456:.1f}", "123.5"
-    check fmt"{123.456:.0f}", "123."
-    #check fmt"{123.456:.0f}", "123."
-    check fmt"{123.456:>9.3f}", "  123.456"
-    check fmt"{123.456:9.3f}", "  123.456"
-    check fmt"{123.456:>9.4f}", " 123.4560"
-    check fmt"{123.456:>9.0f}", "     123."
-    check fmt"{123.456:<9.4f}", "123.4560 "
-
-    # Float (scientific) tests
-    check fmt"{123.456:e}", "1.234560e+02"
-    check fmt"{123.456:>13e}", " 1.234560e+02"
-    check fmt"{123.456:<13e}", "1.234560e+02 "
-    check fmt"{123.456:.1e}", "1.2e+02"
-    check fmt"{123.456:.2e}", "1.23e+02"
-    check fmt"{123.456:.3e}", "1.235e+02"
-
-    # Note: times.format adheres to the format protocol. Test that this
-    # works:
-    import times
-
-    var nullTime: TimeInfo
-    check fmt"{nullTime:yyyy-mm-dd}", "0000-00-00"
-
-    # Unicode string tests
-    check fmt"""{"αβγ"}""", "αβγ"
-    check fmt"""{"αβγ":>5}""", "  αβγ"
-    check fmt"""{"αβγ":<5}""", "αβγ  "
-    check fmt"""a{"a"}α{"α"}€{"€"}𐍈{"𐍈"}""", "aaαα€€𐍈𐍈"
-    check fmt"""a{"a":2}α{"α":2}€{"€":2}𐍈{"𐍈":2}""", "aa αα €€ 𐍈𐍈 "
-    # Invalid unicode sequences should be handled as plain strings.
-    # Invalid examples taken from: https://stackoverflow.com/a/3886015/1804173
-    let invalidUtf8 = [
-      "\xc3\x28", "\xa0\xa1",
-      "\xe2\x28\xa1", "\xe2\x82\x28",
-      "\xf0\x28\x8c\xbc", "\xf0\x90\x28\xbc", "\xf0\x28\x8c\x28"
-    ]
-    for s in invalidUtf8:
-      check fmt"{s:>5}", repeat(" ", 5-s.len) & s
-
+macro `&`*(pattern: string): untyped =
+  ## For a specification of the ``&`` macro, see the module level documentation.
   if pattern.kind notin {nnkStrLit..nnkTripleStrLit}:
-    error "fmt only works with string literals", pattern
+    error "string formatting (fmt(), &) only works with string literals", pattern
   let f = pattern.strVal
   var i = 0
   let res = genSym(nskVar, "fmtRes")
@@ -405,6 +331,11 @@ macro fmt*(pattern: string): untyped =
   when defined(debugFmtDsl):
     echo repr result
 
+template fmt*(pattern: string): untyped =
+  ## An alias for ``&``.
+  bind `&`
+  &pattern
+
 proc mkDigit(v: int, typ: char): string {.inline.} =
   assert(v < 26)
   if v < 10:
@@ -444,7 +375,7 @@ type
                       ## ``parseStandardFormatSpecifier`` returned.
 
 proc formatInt(n: SomeNumber; radix: int; spec: StandardFormatSpecifier): string =
-  ## Converts ``n`` to string. If ``n`` is `SomeReal`, it casts to `int64`.
+  ## Converts ``n`` to string. If ``n`` is `SomeFloat`, it casts to `int64`.
   ## Conversion is done using ``radix``. If result's length is lesser than
   ## ``minimumWidth``, it aligns result to the right or left (depending on ``a``)
   ## with ``fill`` char.
@@ -558,7 +489,7 @@ proc parseStandardFormatSpecifier*(s: string; start = 0;
 proc format*(value: SomeInteger; specifier: string; res: var string) =
   ## Standard format implementation for ``SomeInteger``. It makes little
   ## sense to call this directly, but it is required to exist
-  ## by the ``fmt`` macro.
+  ## by the ``&`` macro.
   let spec = parseStandardFormatSpecifier(specifier)
   var radix = 10
   case spec.typ
@@ -572,10 +503,10 @@ proc format*(value: SomeInteger; specifier: string; res: var string) =
       " of 'x', 'X', 'b', 'd', 'o' but got: " & spec.typ)
   res.add formatInt(value, radix, spec)
 
-proc format*(value: SomeReal; specifier: string; res: var string) =
-  ## Standard format implementation for ``SomeReal``. It makes little
+proc format*(value: SomeFloat; specifier: string; res: var string) =
+  ## Standard format implementation for ``SomeFloat``. It makes little
   ## sense to call this directly, but it is required to exist
-  ## by the ``fmt`` macro.
+  ## by the ``&`` macro.
   let spec = parseStandardFormatSpecifier(specifier)
 
   var fmode = ffDefault
@@ -593,8 +524,31 @@ proc format*(value: SomeReal; specifier: string; res: var string) =
       " of 'e', 'E', 'f', 'F', 'g', 'G' but got: " & spec.typ)
 
   var f = formatBiggestFloat(value, fmode, spec.precision)
-  if value >= 0.0 and spec.sign != '-':
-    f = spec.sign & f
+  var sign = false
+  if value >= 0.0:
+    if spec.sign != '-':
+      sign = true
+      if  value == 0.0:
+        if 1.0 / value == Inf:
+          # only insert the sign if value != negZero
+          f.insert($spec.sign, 0)
+      else:
+        f.insert($spec.sign, 0)
+  else:
+    sign = true
+
+  if spec.padWithZero:
+    var sign_str = ""
+    if sign:
+      sign_str = $f[0]
+      f = f[1..^1]
+
+    let toFill = spec.minimumWidth - f.len - ord(sign)
+    if toFill > 0:
+      f = repeat('0', toFill) & f
+    if sign:
+      f = sign_str & f
+
   # the default for numbers is right-alignment:
   let align = if spec.align == '\0': '>' else: spec.align
   let result = alignString(f, spec.minimumWidth,
@@ -607,13 +561,148 @@ proc format*(value: SomeReal; specifier: string; res: var string) =
 proc format*(value: string; specifier: string; res: var string) =
   ## Standard format implementation for ``string``. It makes little
   ## sense to call this directly, but it is required to exist
-  ## by the ``fmt`` macro.
+  ## by the ``&`` macro.
   let spec = parseStandardFormatSpecifier(specifier)
-  var fmode = ffDefault
+  var value = value
   case spec.typ
   of 's', '\0': discard
   else:
     raise newException(ValueError,
       "invalid type in format string for string, expected 's', but got " &
       spec.typ)
+  if spec.precision != -1:
+    if spec.precision < runelen(value):
+      setLen(value, runeOffset(value, spec.precision))
   res.add alignString(value, spec.minimumWidth, spec.align, spec.fill)
+
+when isMainModule:
+  template check(actual, expected: string) =
+    doAssert actual == expected
+
+  from strutils import toUpperAscii, repeat
+
+  # Basic tests
+  let s = "string"
+  check &"{0} {s}", "0 string"
+  check &"{s[0..2].toUpperAscii}", "STR"
+  check &"{-10:04}", "-010"
+  check &"{-10:<04}", "-010"
+  check &"{-10:>04}", "-010"
+  check &"0x{10:02X}", "0x0A"
+
+  check &"{10:#04X}", "0x0A"
+
+  check &"""{"test":#>5}""", "#test"
+  check &"""{"test":>5}""", " test"
+
+  check &"""{"test":#^7}""", "#test##"
+
+  check &"""{"test": <5}""", "test "
+  check &"""{"test":<5}""", "test "
+  check &"{1f:.3f}", "1.000"
+  check &"Hello, {s}!", "Hello, string!"
+
+  # Tests for identifers without parenthesis
+  check &"{s} works{s}", "string worksstring"
+  check &"{s:>7}", " string"
+  doAssert(not compiles(&"{s_works}")) # parsed as identifier `s_works`
+
+  # Misc general tests
+  check &"{{}}", "{}"
+  check &"{0}%", "0%"
+  check &"{0}%asdf", "0%asdf"
+  check &("\n{\"\\n\"}\n"), "\n\n\n"
+  check &"""{"abc"}s""", "abcs"
+
+  # String tests
+  check &"""{"abc"}""", "abc"
+  check &"""{"abc":>4}""", " abc"
+  check &"""{"abc":<4}""", "abc "
+  check &"""{"":>4}""", "    "
+  check &"""{"":<4}""", "    "
+
+  # Int tests
+  check &"{12345}", "12345"
+  check &"{ - 12345}", "-12345"
+  check &"{12345:6}", " 12345"
+  check &"{12345:>6}", " 12345"
+  check &"{12345:4}", "12345"
+  check &"{12345:08}", "00012345"
+  check &"{-12345:08}", "-0012345"
+  check &"{0:0}", "0"
+  check &"{0:02}", "00"
+  check &"{-1:3}", " -1"
+  check &"{-1:03}", "-01"
+  check &"{10}", "10"
+  check &"{16:#X}", "0x10"
+  check &"{16:^#7X}", " 0x10  "
+  check &"{16:^+#7X}", " +0x10 "
+
+  # Hex tests
+  check &"{0:x}", "0"
+  check &"{-0:x}", "0"
+  check &"{255:x}", "ff"
+  check &"{255:X}", "FF"
+  check &"{-255:x}", "-ff"
+  check &"{-255:X}", "-FF"
+  check &"{255:x} uNaffeCteD CaSe", "ff uNaffeCteD CaSe"
+  check &"{255:X} uNaffeCteD CaSe", "FF uNaffeCteD CaSe"
+  check &"{255:4x}", "  ff"
+  check &"{255:04x}", "00ff"
+  check &"{-255:4x}", " -ff"
+  check &"{-255:04x}", "-0ff"
+
+  # Float tests
+  check &"{123.456}", "123.456"
+  check &"{-123.456}", "-123.456"
+  check &"{123.456:.3f}", "123.456"
+  check &"{123.456:+.3f}", "+123.456"
+  check &"{-123.456:+.3f}", "-123.456"
+  check &"{-123.456:.3f}", "-123.456"
+  check &"{123.456:1g}", "123.456"
+  check &"{123.456:.1f}", "123.5"
+  check &"{123.456:.0f}", "123."
+  #check &"{123.456:.0f}", "123."
+  check &"{123.456:>9.3f}", "  123.456"
+  check &"{123.456:9.3f}", "  123.456"
+  check &"{123.456:>9.4f}", " 123.4560"
+  check &"{123.456:>9.0f}", "     123."
+  check &"{123.456:<9.4f}", "123.4560 "
+
+  # Float (scientific) tests
+  check &"{123.456:e}", "1.234560e+02"
+  check &"{123.456:>13e}", " 1.234560e+02"
+  check &"{123.456:<13e}", "1.234560e+02 "
+  check &"{123.456:.1e}", "1.2e+02"
+  check &"{123.456:.2e}", "1.23e+02"
+  check &"{123.456:.3e}", "1.235e+02"
+
+  # Note: times.format adheres to the format protocol. Test that this
+  # works:
+  import times
+
+  var nullTime: DateTime
+  check &"{nullTime:yyyy-mm-dd}", "0000-00-00"
+
+  # Unicode string tests
+  check &"""{"αβγ"}""", "αβγ"
+  check &"""{"αβγ":>5}""", "  αβγ"
+  check &"""{"αβγ":<5}""", "αβγ  "
+  check &"""a{"a"}α{"α"}€{"€"}𐍈{"𐍈"}""", "aaαα€€𐍈𐍈"
+  check &"""a{"a":2}α{"α":2}€{"€":2}𐍈{"𐍈":2}""", "aa αα €€ 𐍈𐍈 "
+  # Invalid unicode sequences should be handled as plain strings.
+  # Invalid examples taken from: https://stackoverflow.com/a/3886015/1804173
+  let invalidUtf8 = [
+    "\xc3\x28", "\xa0\xa1",
+    "\xe2\x28\xa1", "\xe2\x82\x28",
+    "\xf0\x28\x8c\xbc", "\xf0\x90\x28\xbc", "\xf0\x28\x8c\x28"
+  ]
+  for s in invalidUtf8:
+    check &"{s:>5}", repeat(" ", 5-s.len) & s
+
+
+  import json
+
+  doAssert fmt"{'a'} {'b'}" == "a b"
+
+  echo("All tests ok")