strformat.nim 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805
  1. #
  2. #
  3. # Nim's Runtime Library
  4. # (c) Copyright 2017 Nim contributors
  5. #
  6. # See the file "copying.txt", included in this
  7. # distribution, for details about the copyright.
  8. #
  9. ##[
  10. String `interpolation`:idx: / `format`:idx: inspired by
  11. Python's ``f``-strings.
  12. ``fmt`` vs. ``&``
  13. =================
  14. You can use either ``fmt`` or the unary ``&`` operator for formatting. The
  15. difference between them is subtle but important.
  16. The ``fmt"{expr}"`` syntax is more aesthetically pleasing, but it hides a small
  17. gotcha. The string is a
  18. `generalized raw string literal <manual.html#lexical-analysis-generalized-raw-string-literals>`_.
  19. This has some surprising effects:
  20. .. code-block:: nim
  21. import strformat
  22. let msg = "hello"
  23. doAssert fmt"{msg}\n" == "hello\\n"
  24. Because the literal is a raw string literal, the ``\n`` is not interpreted as
  25. an escape sequence.
  26. There are multiple ways to get around this, including the use of the ``&``
  27. operator:
  28. .. code-block:: nim
  29. import strformat
  30. let msg = "hello"
  31. doAssert &"{msg}\n" == "hello\n"
  32. doAssert fmt"{msg}{'\n'}" == "hello\n"
  33. doAssert fmt("{msg}\n") == "hello\n"
  34. doAssert "{msg}\n".fmt == "hello\n"
  35. The choice of style is up to you.
  36. Formatting strings
  37. ==================
  38. .. code-block:: nim
  39. import strformat
  40. doAssert &"""{"abc":>4}""" == " abc"
  41. doAssert &"""{"abc":<4}""" == "abc "
  42. Formatting floats
  43. =================
  44. .. code-block:: nim
  45. import strformat
  46. doAssert fmt"{-12345:08}" == "-0012345"
  47. doAssert fmt"{-1:3}" == " -1"
  48. doAssert fmt"{-1:03}" == "-01"
  49. doAssert fmt"{16:#X}" == "0x10"
  50. doAssert fmt"{123.456}" == "123.456"
  51. doAssert fmt"{123.456:>9.3f}" == " 123.456"
  52. doAssert fmt"{123.456:9.3f}" == " 123.456"
  53. doAssert fmt"{123.456:9.4f}" == " 123.4560"
  54. doAssert fmt"{123.456:>9.0f}" == " 123."
  55. doAssert fmt"{123.456:<9.4f}" == "123.4560 "
  56. doAssert fmt"{123.456:e}" == "1.234560e+02"
  57. doAssert fmt"{123.456:>13e}" == " 1.234560e+02"
  58. doAssert fmt"{123.456:13e}" == " 1.234560e+02"
  59. Debugging strings
  60. =================
  61. ``fmt"{expr=}"`` expands to ``fmt"expr={expr}"`` namely the text of the expression,
  62. an equal sign and the results of evaluated expression.
  63. .. code-block:: nim
  64. import strformat
  65. doAssert fmt"{123.456=}" == "123.456=123.456"
  66. doAssert fmt"{123.456=:>9.3f}" == "123.456= 123.456"
  67. let x = "hello"
  68. doAssert fmt"{x=}" == "x=hello"
  69. doAssert fmt"{x =}" == "x =hello"
  70. let y = 3.1415926
  71. doAssert fmt"{y=:.2f}" == fmt"y={y:.2f}"
  72. doAssert fmt"{y=}" == fmt"y={y}"
  73. doAssert fmt"{y = : <8}" == fmt"y = 3.14159 "
  74. proc hello(a: string, b: float): int = 12
  75. let a = "hello"
  76. let b = 3.1415926
  77. doAssert fmt"{hello(x, y) = }" == "hello(x, y) = 12"
  78. doAssert fmt"{x.hello(y) = }" == "x.hello(y) = 12"
  79. doAssert fmt"{hello x, y = }" == "hello x, y = 12"
  80. Note that it is space sensitive:
  81. .. code-block:: nim
  82. import strformat
  83. let x = "12"
  84. doAssert fmt"{x=}" == "x=12"
  85. doAssert fmt"{x =:}" == "x =12"
  86. doAssert fmt"{x =}" == "x =12"
  87. doAssert fmt"{x= :}" == "x= 12"
  88. doAssert fmt"{x= }" == "x= 12"
  89. doAssert fmt"{x = :}" == "x = 12"
  90. doAssert fmt"{x = }" == "x = 12"
  91. doAssert fmt"{x = :}" == "x = 12"
  92. doAssert fmt"{x = }" == "x = 12"
  93. Implementation details
  94. ======================
  95. An expression like ``&"{key} is {value:arg} {{z}}"`` is transformed into:
  96. .. code-block:: nim
  97. var temp = newStringOfCap(educatedCapGuess)
  98. temp.formatValue key, ""
  99. temp.add " is "
  100. temp.formatValue value, arg
  101. temp.add " {z}"
  102. temp
  103. Parts of the string that are enclosed in the curly braces are interpreted
  104. as Nim code, to escape an ``{`` or ``}`` double it.
  105. ``&`` delegates most of the work to an open overloaded set
  106. of ``formatValue`` procs. The required signature for a type ``T`` that supports
  107. formatting is usually ``proc formatValue(result: var string; x: T; specifier: string)``.
  108. The subexpression after the colon
  109. (``arg`` in ``&"{key} is {value:arg} {{z}}"``) is optional. It will be passed as
  110. the last argument to ``formatValue``. When the colon with the subexpression it is
  111. left out, an empty string will be taken instead.
  112. For strings and numeric types the optional argument is a so-called
  113. "standard format specifier".
  114. Standard format specifier for strings, integers and floats
  115. ==========================================================
  116. The general form of a standard format specifier is::
  117. [[fill]align][sign][#][0][minimumwidth][.precision][type]
  118. The square brackets ``[]`` indicate an optional element.
  119. The optional align flag can be one of the following:
  120. '<'
  121. Forces the field to be left-aligned within the available
  122. space. (This is the default for strings.)
  123. '>'
  124. Forces the field to be right-aligned within the available space.
  125. (This is the default for numbers.)
  126. '^'
  127. Forces the field to be centered within the available space.
  128. Note that unless a minimum field width is defined, the field width
  129. will always be the same size as the data to fill it, so that the alignment
  130. option has no meaning in this case.
  131. The optional 'fill' character defines the character to be used to pad
  132. the field to the minimum width. The fill character, if present, must be
  133. followed by an alignment flag.
  134. The 'sign' option is only valid for numeric types, and can be one of the following:
  135. ================= ====================================================
  136. Sign Meaning
  137. ================= ====================================================
  138. ``+`` Indicates that a sign should be used for both
  139. positive as well as negative numbers.
  140. ``-`` Indicates that a sign should be used only for
  141. negative numbers (this is the default behavior).
  142. (space) Indicates that a leading space should be used on
  143. positive numbers.
  144. ================= ====================================================
  145. If the '#' character is present, integers use the 'alternate form' for formatting.
  146. This means that binary, octal, and hexadecimal output will be prefixed
  147. with '0b', '0o', and '0x', respectively.
  148. 'width' is a decimal integer defining the minimum field width. If not specified,
  149. then the field width will be determined by the content.
  150. If the width field is preceded by a zero ('0') character, this enables
  151. zero-padding.
  152. The 'precision' is a decimal number indicating how many digits should be displayed
  153. after the decimal point in a floating point conversion. For non-numeric types the
  154. field indicates the maximum field size - in other words, how many characters will
  155. be used from the field content. The precision is ignored for integer conversions.
  156. Finally, the 'type' determines how the data should be presented.
  157. The available integer presentation types are:
  158. ================= ====================================================
  159. Type Result
  160. ================= ====================================================
  161. ``b`` Binary. Outputs the number in base 2.
  162. ``d`` Decimal Integer. Outputs the number in base 10.
  163. ``o`` Octal format. Outputs the number in base 8.
  164. ``x`` Hex format. Outputs the number in base 16, using
  165. lower-case letters for the digits above 9.
  166. ``X`` Hex format. Outputs the number in base 16, using
  167. uppercase letters for the digits above 9.
  168. (None) the same as 'd'
  169. ================= ====================================================
  170. The available floating point presentation types are:
  171. ================= ====================================================
  172. Type Result
  173. ================= ====================================================
  174. ``e`` Exponent notation. Prints the number in scientific
  175. notation using the letter 'e' to indicate the
  176. exponent.
  177. ``E`` Exponent notation. Same as 'e' except it converts
  178. the number to uppercase.
  179. ``f`` Fixed point. Displays the number as a fixed-point
  180. number.
  181. ``F`` Fixed point. Same as 'f' except it converts the
  182. number to uppercase.
  183. ``g`` General format. This prints the number as a
  184. fixed-point number, unless the number is too
  185. large, in which case it switches to 'e'
  186. exponent notation.
  187. ``G`` General format. Same as 'g' except switches to 'E'
  188. if the number gets to large.
  189. (None) similar to 'g', except that it prints at least one
  190. digit after the decimal point.
  191. ================= ====================================================
  192. Limitations
  193. ===========
  194. Because of the well defined order how templates and macros are
  195. expanded, strformat cannot expand template arguments:
  196. .. code-block:: nim
  197. template myTemplate(arg: untyped): untyped =
  198. echo "arg is: ", arg
  199. echo &"--- {arg} ---"
  200. let x = "abc"
  201. myTemplate(x)
  202. First the template ``myTemplate`` is expanded, where every identifier
  203. ``arg`` is substituted with its argument. The ``arg`` inside the
  204. format string is not seen by this process, because it is part of a
  205. quoted string literal. It is not an identifier yet. Then the strformat
  206. macro creates the ``arg`` identifier from the string literal. An
  207. identifier that cannot be resolved anymore.
  208. The workaround for this is to bind the template argument to a new local variable.
  209. .. code-block:: nim
  210. template myTemplate(arg: untyped): untyped =
  211. block:
  212. let arg1 {.inject.} = arg
  213. echo "arg is: ", arg1
  214. echo &"--- {arg1} ---"
  215. The use of ``{.inject.}`` here is necessary again because of template
  216. expansion order and hygienic templates. But since we generally want to
  217. keep the hygienicness of ``myTemplate``, and we do not want ``arg1``
  218. to be injected into the context where ``myTemplate`` is expanded,
  219. everything is wrapped in a ``block``.
  220. Future directions
  221. =================
  222. A curly expression with commas in it like ``{x, argA, argB}`` could be
  223. transformed to ``formatValue(result, x, argA, argB)`` in order to support
  224. formatters that do not need to parse a custom language within a custom
  225. language but instead prefer to use Nim's existing syntax. This also
  226. helps in readability since there is only so much you can cram into
  227. single letter DSLs.
  228. ]##
  229. import macros, parseutils, unicode
  230. import strutils except format
  231. proc mkDigit(v: int, typ: char): string {.inline.} =
  232. assert(v < 26)
  233. if v < 10:
  234. result = $chr(ord('0') + v)
  235. else:
  236. result = $chr(ord(if typ == 'x': 'a' else: 'A') + v - 10)
  237. proc alignString*(s: string, minimumWidth: int; align = '\0';
  238. fill = ' '): string =
  239. ## Aligns ``s`` using ``fill`` char.
  240. ## This is only of interest if you want to write a custom ``format`` proc that
  241. ## should support the standard format specifiers.
  242. if minimumWidth == 0:
  243. result = s
  244. else:
  245. let sRuneLen = if s.validateUtf8 == -1: s.runeLen else: s.len
  246. let toFill = minimumWidth - sRuneLen
  247. if toFill <= 0:
  248. result = s
  249. elif align == '<' or align == '\0':
  250. result = s & repeat(fill, toFill)
  251. elif align == '^':
  252. let half = toFill div 2
  253. result = repeat(fill, half) & s & repeat(fill, toFill - half)
  254. else:
  255. result = repeat(fill, toFill) & s
  256. type
  257. StandardFormatSpecifier* = object ## Type that describes "standard format specifiers".
  258. fill*, align*: char ## Desired fill and alignment.
  259. sign*: char ## Desired sign.
  260. alternateForm*: bool ## Whether to prefix binary, octal and hex numbers
  261. ## with ``0b``, ``0o``, ``0x``.
  262. padWithZero*: bool ## Whether to pad with zeros rather than spaces.
  263. minimumWidth*, precision*: int ## Desired minimum width and precision.
  264. typ*: char ## Type like 'f', 'g' or 'd'.
  265. endPosition*: int ## End position in the format specifier after
  266. ## ``parseStandardFormatSpecifier`` returned.
  267. proc formatInt(n: SomeNumber; radix: int;
  268. spec: StandardFormatSpecifier): string =
  269. ## Converts ``n`` to string. If ``n`` is `SomeFloat`, it casts to `int64`.
  270. ## Conversion is done using ``radix``. If result's length is lesser than
  271. ## ``minimumWidth``, it aligns result to the right or left (depending on ``a``)
  272. ## with ``fill`` char.
  273. when n is SomeUnsignedInt:
  274. var v = n.uint64
  275. let negative = false
  276. else:
  277. var v = n.int64
  278. let negative = v.int64 < 0
  279. if negative:
  280. # FIXME: overflow error for low(int64)
  281. v = v * -1
  282. var xx = ""
  283. if spec.alternateForm:
  284. case spec.typ
  285. of 'X': xx = "0x"
  286. of 'x': xx = "0x"
  287. of 'b': xx = "0b"
  288. of 'o': xx = "0o"
  289. else: discard
  290. if v == 0:
  291. result = "0"
  292. else:
  293. result = ""
  294. while v > type(v)(0):
  295. let d = v mod type(v)(radix)
  296. v = v div type(v)(radix)
  297. result.add(mkDigit(d.int, spec.typ))
  298. for idx in 0..<(result.len div 2):
  299. swap result[idx], result[result.len - idx - 1]
  300. if spec.padWithZero:
  301. let sign = negative or spec.sign != '-'
  302. let toFill = spec.minimumWidth - result.len - xx.len - ord(sign)
  303. if toFill > 0:
  304. result = repeat('0', toFill) & result
  305. if negative:
  306. result = "-" & xx & result
  307. elif spec.sign != '-':
  308. result = spec.sign & xx & result
  309. else:
  310. result = xx & result
  311. if spec.align == '<':
  312. for i in result.len..<spec.minimumWidth:
  313. result.add(spec.fill)
  314. else:
  315. let toFill = spec.minimumWidth - result.len
  316. if spec.align == '^':
  317. let half = toFill div 2
  318. result = repeat(spec.fill, half) & result & repeat(spec.fill, toFill - half)
  319. else:
  320. if toFill > 0:
  321. result = repeat(spec.fill, toFill) & result
  322. proc parseStandardFormatSpecifier*(s: string; start = 0;
  323. ignoreUnknownSuffix = false): StandardFormatSpecifier =
  324. ## An exported helper proc that parses the "standard format specifiers",
  325. ## as specified by the grammar::
  326. ##
  327. ## [[fill]align][sign][#][0][minimumwidth][.precision][type]
  328. ##
  329. ## This is only of interest if you want to write a custom ``format`` proc that
  330. ## should support the standard format specifiers. If ``ignoreUnknownSuffix`` is true,
  331. ## an unknown suffix after the ``type`` field is not an error.
  332. const alignChars = {'<', '>', '^'}
  333. result.fill = ' '
  334. result.align = '\0'
  335. result.sign = '-'
  336. var i = start
  337. if i + 1 < s.len and s[i+1] in alignChars:
  338. result.fill = s[i]
  339. result.align = s[i+1]
  340. inc i, 2
  341. elif i < s.len and s[i] in alignChars:
  342. result.align = s[i]
  343. inc i
  344. if i < s.len and s[i] in {'-', '+', ' '}:
  345. result.sign = s[i]
  346. inc i
  347. if i < s.len and s[i] == '#':
  348. result.alternateForm = true
  349. inc i
  350. if i+1 < s.len and s[i] == '0' and s[i+1] in {'0'..'9'}:
  351. result.padWithZero = true
  352. inc i
  353. let parsedLength = parseSaturatedNatural(s, result.minimumWidth, i)
  354. inc i, parsedLength
  355. if i < s.len and s[i] == '.':
  356. inc i
  357. let parsedLengthB = parseSaturatedNatural(s, result.precision, i)
  358. inc i, parsedLengthB
  359. else:
  360. result.precision = -1
  361. if i < s.len and s[i] in {'A'..'Z', 'a'..'z'}:
  362. result.typ = s[i]
  363. inc i
  364. result.endPosition = i
  365. if i != s.len and not ignoreUnknownSuffix:
  366. raise newException(ValueError,
  367. "invalid format string, cannot parse: " & s[i..^1])
  368. proc formatValue*[T: SomeInteger](result: var string; value: T;
  369. specifier: string) =
  370. ## Standard format implementation for ``SomeInteger``. It makes little
  371. ## sense to call this directly, but it is required to exist
  372. ## by the ``&`` macro.
  373. if specifier.len == 0:
  374. result.add $value
  375. return
  376. let spec = parseStandardFormatSpecifier(specifier)
  377. var radix = 10
  378. case spec.typ
  379. of 'x', 'X': radix = 16
  380. of 'd', '\0': discard
  381. of 'b': radix = 2
  382. of 'o': radix = 8
  383. else:
  384. raise newException(ValueError,
  385. "invalid type in format string for number, expected one " &
  386. " of 'x', 'X', 'b', 'd', 'o' but got: " & spec.typ)
  387. result.add formatInt(value, radix, spec)
  388. proc formatValue*(result: var string; value: SomeFloat; specifier: string) =
  389. ## Standard format implementation for ``SomeFloat``. It makes little
  390. ## sense to call this directly, but it is required to exist
  391. ## by the ``&`` macro.
  392. if specifier.len == 0:
  393. result.add $value
  394. return
  395. let spec = parseStandardFormatSpecifier(specifier)
  396. var fmode = ffDefault
  397. case spec.typ
  398. of 'e', 'E':
  399. fmode = ffScientific
  400. of 'f', 'F':
  401. fmode = ffDecimal
  402. of 'g', 'G':
  403. fmode = ffDefault
  404. of '\0': discard
  405. else:
  406. raise newException(ValueError,
  407. "invalid type in format string for number, expected one " &
  408. " of 'e', 'E', 'f', 'F', 'g', 'G' but got: " & spec.typ)
  409. var f = formatBiggestFloat(value, fmode, spec.precision)
  410. var sign = false
  411. if value >= 0.0:
  412. if spec.sign != '-':
  413. sign = true
  414. if value == 0.0:
  415. if 1.0 / value == Inf:
  416. # only insert the sign if value != negZero
  417. f.insert($spec.sign, 0)
  418. else:
  419. f.insert($spec.sign, 0)
  420. else:
  421. sign = true
  422. if spec.padWithZero:
  423. var signStr = ""
  424. if sign:
  425. signStr = $f[0]
  426. f = f[1..^1]
  427. let toFill = spec.minimumWidth - f.len - ord(sign)
  428. if toFill > 0:
  429. f = repeat('0', toFill) & f
  430. if sign:
  431. f = signStr & f
  432. # the default for numbers is right-alignment:
  433. let align = if spec.align == '\0': '>' else: spec.align
  434. let res = alignString(f, spec.minimumWidth, align, spec.fill)
  435. if spec.typ in {'A'..'Z'}:
  436. result.add toUpperAscii(res)
  437. else:
  438. result.add res
  439. proc formatValue*(result: var string; value: string; specifier: string) =
  440. ## Standard format implementation for ``string``. It makes little
  441. ## sense to call this directly, but it is required to exist
  442. ## by the ``&`` macro.
  443. let spec = parseStandardFormatSpecifier(specifier)
  444. var value = value
  445. case spec.typ
  446. of 's', '\0': discard
  447. else:
  448. raise newException(ValueError,
  449. "invalid type in format string for string, expected 's', but got " &
  450. spec.typ)
  451. if spec.precision != -1:
  452. if spec.precision < runeLen(value):
  453. setLen(value, runeOffset(value, spec.precision))
  454. result.add alignString(value, spec.minimumWidth, spec.align, spec.fill)
  455. proc formatValue[T: not SomeInteger](result: var string; value: T;
  456. specifier: string) =
  457. mixin `$`
  458. formatValue(result, $value, specifier)
  459. template formatValue(result: var string; value: char; specifier: string) =
  460. result.add value
  461. template formatValue(result: var string; value: cstring; specifier: string) =
  462. result.add value
  463. proc strformatImpl(pattern: NimNode; openChar, closeChar: char): NimNode =
  464. if pattern.kind notin {nnkStrLit..nnkTripleStrLit}:
  465. error "string formatting (fmt(), &) only works with string literals", pattern
  466. if openChar == ':' or closeChar == ':':
  467. error "openChar and closeChar must not be ':'"
  468. let f = pattern.strVal
  469. var i = 0
  470. let res = genSym(nskVar, "fmtRes")
  471. result = newNimNode(nnkStmtListExpr, lineInfoFrom = pattern)
  472. # XXX: https://github.com/nim-lang/Nim/issues/8405
  473. # When compiling with -d:useNimRtl, certain procs such as `count` from the strutils
  474. # module are not accessible at compile-time:
  475. let expectedGrowth = when defined(useNimRtl): 0 else: count(f, '{') * 10
  476. result.add newVarStmt(res, newCall(bindSym"newStringOfCap",
  477. newLit(f.len + expectedGrowth)))
  478. var strlit = ""
  479. while i < f.len:
  480. if f[i] == openChar:
  481. inc i
  482. if f[i] == openChar:
  483. inc i
  484. strlit.add openChar
  485. else:
  486. if strlit.len > 0:
  487. result.add newCall(bindSym"add", res, newLit(strlit))
  488. strlit = ""
  489. var subexpr = ""
  490. while i < f.len and f[i] != closeChar and f[i] != ':':
  491. if f[i] == '=':
  492. let start = i
  493. inc i
  494. i += f.skipWhitespace(i)
  495. if f[i] == closeChar or f[i] == ':':
  496. result.add newCall(bindSym"add", res, newLit(subexpr & f[start ..< i]))
  497. else:
  498. subexpr.add f[start ..< i]
  499. else:
  500. subexpr.add f[i]
  501. inc i
  502. var x: NimNode
  503. try:
  504. x = parseExpr(subexpr)
  505. except ValueError:
  506. when declared(getCurrentExceptionMsg):
  507. let msg = getCurrentExceptionMsg()
  508. error("could not parse `" & subexpr & "`.\n" & msg, pattern)
  509. else:
  510. error("could not parse `" & subexpr & "`.\n", pattern)
  511. let formatSym = bindSym("formatValue", brOpen)
  512. var options = ""
  513. if f[i] == ':':
  514. inc i
  515. while i < f.len and f[i] != closeChar:
  516. options.add f[i]
  517. inc i
  518. if f[i] == closeChar:
  519. inc i
  520. else:
  521. doAssert false, "invalid format string: missing '}'"
  522. result.add newCall(formatSym, res, x, newLit(options))
  523. elif f[i] == closeChar:
  524. if f[i+1] == closeChar:
  525. strlit.add closeChar
  526. inc i, 2
  527. else:
  528. doAssert false, "invalid format string: '}' instead of '}}'"
  529. inc i
  530. else:
  531. strlit.add f[i]
  532. inc i
  533. if strlit.len > 0:
  534. result.add newCall(bindSym"add", res, newLit(strlit))
  535. result.add res
  536. when defined(debugFmtDsl):
  537. echo repr result
  538. macro `&`*(pattern: string): untyped = strformatImpl(pattern, '{', '}')
  539. ## For a specification of the ``&`` macro, see the module level documentation.
  540. macro fmt*(pattern: string): untyped = strformatImpl(pattern, '{', '}')
  541. ## An alias for ``&``.
  542. macro fmt*(pattern: string; openChar, closeChar: char): untyped =
  543. ## Use ``openChar`` instead of '{' and ``closeChar`` instead of '}'
  544. runnableExamples:
  545. let testInt = 123
  546. doAssert "<testInt>".fmt('<', '>') == "123"
  547. doAssert """(()"foo" & "bar"())""".fmt(')', '(') == "(foobar)"
  548. doAssert """ ""{"123+123"}"" """.fmt('"', '"') == " \"{246}\" "
  549. strformatImpl(pattern, openChar.intVal.char, closeChar.intVal.char)
  550. when isMainModule:
  551. template check(actual, expected: string) =
  552. doAssert actual == expected
  553. from strutils import toUpperAscii, repeat
  554. # Basic tests
  555. let s = "string"
  556. check &"{0} {s}", "0 string"
  557. check &"{s[0..2].toUpperAscii}", "STR"
  558. check &"{-10:04}", "-010"
  559. check &"{-10:<04}", "-010"
  560. check &"{-10:>04}", "-010"
  561. check &"0x{10:02X}", "0x0A"
  562. check &"{10:#04X}", "0x0A"
  563. check &"""{"test":#>5}""", "#test"
  564. check &"""{"test":>5}""", " test"
  565. check &"""{"test":#^7}""", "#test##"
  566. check &"""{"test": <5}""", "test "
  567. check &"""{"test":<5}""", "test "
  568. check &"{1f:.3f}", "1.000"
  569. check &"Hello, {s}!", "Hello, string!"
  570. # Tests for identifiers without parenthesis
  571. check &"{s} works{s}", "string worksstring"
  572. check &"{s:>7}", " string"
  573. doAssert(not compiles(&"{s_works}")) # parsed as identifier `s_works`
  574. # Misc general tests
  575. check &"{{}}", "{}"
  576. check &"{0}%", "0%"
  577. check &"{0}%asdf", "0%asdf"
  578. check &("\n{\"\\n\"}\n"), "\n\n\n"
  579. check &"""{"abc"}s""", "abcs"
  580. # String tests
  581. check &"""{"abc"}""", "abc"
  582. check &"""{"abc":>4}""", " abc"
  583. check &"""{"abc":<4}""", "abc "
  584. check &"""{"":>4}""", " "
  585. check &"""{"":<4}""", " "
  586. # Int tests
  587. check &"{12345}", "12345"
  588. check &"{ - 12345}", "-12345"
  589. check &"{12345:6}", " 12345"
  590. check &"{12345:>6}", " 12345"
  591. check &"{12345:4}", "12345"
  592. check &"{12345:08}", "00012345"
  593. check &"{-12345:08}", "-0012345"
  594. check &"{0:0}", "0"
  595. check &"{0:02}", "00"
  596. check &"{-1:3}", " -1"
  597. check &"{-1:03}", "-01"
  598. check &"{10}", "10"
  599. check &"{16:#X}", "0x10"
  600. check &"{16:^#7X}", " 0x10 "
  601. check &"{16:^+#7X}", " +0x10 "
  602. # Hex tests
  603. check &"{0:x}", "0"
  604. check &"{-0:x}", "0"
  605. check &"{255:x}", "ff"
  606. check &"{255:X}", "FF"
  607. check &"{-255:x}", "-ff"
  608. check &"{-255:X}", "-FF"
  609. check &"{255:x} uNaffeCteD CaSe", "ff uNaffeCteD CaSe"
  610. check &"{255:X} uNaffeCteD CaSe", "FF uNaffeCteD CaSe"
  611. check &"{255:4x}", " ff"
  612. check &"{255:04x}", "00ff"
  613. check &"{-255:4x}", " -ff"
  614. check &"{-255:04x}", "-0ff"
  615. # Float tests
  616. check &"{123.456}", "123.456"
  617. check &"{-123.456}", "-123.456"
  618. check &"{123.456:.3f}", "123.456"
  619. check &"{123.456:+.3f}", "+123.456"
  620. check &"{-123.456:+.3f}", "-123.456"
  621. check &"{-123.456:.3f}", "-123.456"
  622. check &"{123.456:1g}", "123.456"
  623. check &"{123.456:.1f}", "123.5"
  624. check &"{123.456:.0f}", "123."
  625. check &"{123.456:>9.3f}", " 123.456"
  626. check &"{123.456:9.3f}", " 123.456"
  627. check &"{123.456:>9.4f}", " 123.4560"
  628. check &"{123.456:>9.0f}", " 123."
  629. check &"{123.456:<9.4f}", "123.4560 "
  630. # Float (scientific) tests
  631. check &"{123.456:e}", "1.234560e+02"
  632. check &"{123.456:>13e}", " 1.234560e+02"
  633. check &"{123.456:<13e}", "1.234560e+02 "
  634. check &"{123.456:.1e}", "1.2e+02"
  635. check &"{123.456:.2e}", "1.23e+02"
  636. check &"{123.456:.3e}", "1.235e+02"
  637. # Note: times.format adheres to the format protocol. Test that this
  638. # works:
  639. import times
  640. var dt = initDateTime(01, mJan, 2000, 00, 00, 00)
  641. check &"{dt:yyyy-MM-dd}", "2000-01-01"
  642. var tm = fromUnix(0)
  643. discard &"{tm}"
  644. var noww = now()
  645. check &"{noww}", $noww
  646. # Unicode string tests
  647. check &"""{"αβγ"}""", "αβγ"
  648. check &"""{"αβγ":>5}""", " αβγ"
  649. check &"""{"αβγ":<5}""", "αβγ "
  650. check &"""a{"a"}α{"α"}€{"€"}𐍈{"𐍈"}""", "aaαα€€𐍈𐍈"
  651. check &"""a{"a":2}α{"α":2}€{"€":2}𐍈{"𐍈":2}""", "aa αα €€ 𐍈𐍈 "
  652. # Invalid unicode sequences should be handled as plain strings.
  653. # Invalid examples taken from: https://stackoverflow.com/a/3886015/1804173
  654. let invalidUtf8 = [
  655. "\xc3\x28", "\xa0\xa1",
  656. "\xe2\x28\xa1", "\xe2\x82\x28",
  657. "\xf0\x28\x8c\xbc", "\xf0\x90\x28\xbc", "\xf0\x28\x8c\x28"
  658. ]
  659. for s in invalidUtf8:
  660. check &"{s:>5}", repeat(" ", 5-s.len) & s
  661. # bug #11089
  662. let flfoo: float = 1.0
  663. check &"{flfoo}", "1.0"
  664. # bug #11092
  665. check &"{high(int64)}", "9223372036854775807"
  666. check &"{low(int64)}", "-9223372036854775808"
  667. doAssert fmt"{'a'} {'b'}" == "a b"
  668. echo("All tests ok")