mirror of
https://github.com/nim-lang/Nim.git
synced 2025-12-28 17:04:41 +00:00
3070 lines
102 KiB
Nim
3070 lines
102 KiB
Nim
#
|
|
#
|
|
# Nim's Runtime Library
|
|
# (c) Copyright 2012 Andreas Rumpf
|
|
#
|
|
# See the file "copying.txt", included in this
|
|
# distribution, for details about the copyright.
|
|
#
|
|
|
|
## The system module defines several common functions for working with strings,
|
|
## such as:
|
|
## * `$` for converting other data-types to strings
|
|
## * `&` for string concatenation
|
|
## * `add` for adding a new character or a string to the existing one
|
|
## * `in` (alias for `contains`) and `notin` for checking if a character
|
|
## is in a string
|
|
##
|
|
## This module builds upon that, providing additional functionality in form of
|
|
## procedures, iterators and templates for strings.
|
|
|
|
runnableExamples:
|
|
let
|
|
numbers = @[867, 5309]
|
|
multiLineString = "first line\nsecond line\nthird line"
|
|
|
|
let jenny = numbers.join("-")
|
|
assert jenny == "867-5309"
|
|
|
|
assert splitLines(multiLineString) ==
|
|
@["first line", "second line", "third line"]
|
|
assert split(multiLineString) == @["first", "line", "second",
|
|
"line", "third", "line"]
|
|
assert indent(multiLineString, 4) ==
|
|
" first line\n second line\n third line"
|
|
assert 'z'.repeat(5) == "zzzzz"
|
|
|
|
## The chaining of functions is possible thanks to the
|
|
## `method call syntax<manual.html#procedures-method-call-syntax>`_:
|
|
|
|
runnableExamples:
|
|
from std/sequtils import map
|
|
|
|
let jenny = "867-5309"
|
|
assert jenny.split('-').map(parseInt) == @[867, 5309]
|
|
|
|
assert "Beetlejuice".indent(1).repeat(3).strip ==
|
|
"Beetlejuice Beetlejuice Beetlejuice"
|
|
|
|
## This module is available for the `JavaScript target
|
|
## <backends.html#backends-the-javascript-target>`_.
|
|
##
|
|
## ----
|
|
##
|
|
## **See also:**
|
|
## * `strformat module<strformat.html>`_ for string interpolation and formatting
|
|
## * `unicode module<unicode.html>`_ for Unicode UTF-8 handling
|
|
## * `sequtils module<sequtils.html>`_ for operations on container
|
|
## types (including strings)
|
|
## * `parsecsv module<parsecsv.html>`_ for a high-performance CSV parser
|
|
## * `parseutils module<parseutils.html>`_ for lower-level parsing of tokens,
|
|
## numbers, identifiers, etc.
|
|
## * `parseopt module<parseopt.html>`_ for command-line parsing
|
|
## * `pegs module<pegs.html>`_ for PEG (Parsing Expression Grammar) support
|
|
## * `strtabs module<strtabs.html>`_ for efficient hash tables
|
|
## (dictionaries, in some programming languages) mapping from strings to strings
|
|
## * `ropes module<ropes.html>`_ for rope data type, which can represent very
|
|
## long strings efficiently
|
|
## * `re module<re.html>`_ for regular expression (regex) support
|
|
## * `strscans<strscans.html>`_ for `scanf` and `scanp` macros, which offer
|
|
## easier substring extraction than regular expressions
|
|
|
|
|
|
import std/parseutils
|
|
from std/math import pow, floor, log10
|
|
from std/algorithm import fill, reverse
|
|
import std/enumutils
|
|
from std/bitops import fastLog2
|
|
|
|
from std/unicode import toLower, toUpper
|
|
export toLower, toUpper
|
|
|
|
include "system/inclrtl"
|
|
import std/private/[since, jsutils]
|
|
from std/private/strimpl import cmpIgnoreStyleImpl, cmpIgnoreCaseImpl,
|
|
startsWithImpl, endsWithImpl
|
|
|
|
when defined(nimPreviewSlimSystem):
|
|
import std/assertions
|
|
|
|
|
|
const
|
|
Whitespace* = {' ', '\t', '\v', '\r', '\l', '\f'}
|
|
## All the characters that count as whitespace (space, tab, vertical tab,
|
|
## carriage return, new line, form feed).
|
|
|
|
Letters* = {'A'..'Z', 'a'..'z'}
|
|
## The set of letters.
|
|
|
|
UppercaseLetters* = {'A'..'Z'}
|
|
## The set of uppercase ASCII letters.
|
|
|
|
LowercaseLetters* = {'a'..'z'}
|
|
## The set of lowercase ASCII letters.
|
|
|
|
PunctuationChars* = {'!'..'/', ':'..'@', '['..'`', '{'..'~'}
|
|
## The set of all ASCII punctuation characters.
|
|
|
|
Digits* = {'0'..'9'}
|
|
## The set of digits.
|
|
|
|
HexDigits* = {'0'..'9', 'A'..'F', 'a'..'f'}
|
|
## The set of hexadecimal digits.
|
|
|
|
IdentChars* = {'a'..'z', 'A'..'Z', '0'..'9', '_'}
|
|
## The set of characters an identifier can consist of.
|
|
|
|
IdentStartChars* = {'a'..'z', 'A'..'Z', '_'}
|
|
## The set of characters an identifier can start with.
|
|
|
|
Newlines* = {'\13', '\10'}
|
|
## The set of characters a newline terminator can start with (carriage
|
|
## return, line feed).
|
|
|
|
PrintableChars* = Letters + Digits + PunctuationChars + Whitespace
|
|
## The set of all printable ASCII characters (letters, digits, whitespace, and punctuation characters).
|
|
|
|
AllChars* = {'\x00'..'\xFF'}
|
|
## A set with all the possible characters.
|
|
##
|
|
## Not very useful by its own, you can use it to create *inverted* sets to
|
|
## make the `find func<#find,string,set[char],Natural,int>`_
|
|
## find **invalid** characters in strings. Example:
|
|
## ```nim
|
|
## let invalid = AllChars - Digits
|
|
## doAssert "01234".find(invalid) == -1
|
|
## doAssert "01A34".find(invalid) == 2
|
|
## ```
|
|
|
|
func isAlphaAscii*(c: char): bool {.rtl, extern: "nsuIsAlphaAsciiChar".} =
|
|
## Checks whether or not character `c` is alphabetical.
|
|
##
|
|
## This checks a-z, A-Z ASCII characters only.
|
|
## Use `Unicode module<unicode.html>`_ for UTF-8 support.
|
|
runnableExamples:
|
|
doAssert isAlphaAscii('e') == true
|
|
doAssert isAlphaAscii('E') == true
|
|
doAssert isAlphaAscii('8') == false
|
|
return c in Letters
|
|
|
|
func isAlphaNumeric*(c: char): bool {.rtl, extern: "nsuIsAlphaNumericChar".} =
|
|
## Checks whether or not `c` is alphanumeric.
|
|
##
|
|
## This checks a-z, A-Z, 0-9 ASCII characters only.
|
|
runnableExamples:
|
|
doAssert isAlphaNumeric('n') == true
|
|
doAssert isAlphaNumeric('8') == true
|
|
doAssert isAlphaNumeric(' ') == false
|
|
return c in Letters+Digits
|
|
|
|
func isDigit*(c: char): bool {.rtl, extern: "nsuIsDigitChar".} =
|
|
## Checks whether or not `c` is a number.
|
|
##
|
|
## This checks 0-9 ASCII characters only.
|
|
runnableExamples:
|
|
doAssert isDigit('n') == false
|
|
doAssert isDigit('8') == true
|
|
return c in Digits
|
|
|
|
func isSpaceAscii*(c: char): bool {.rtl, extern: "nsuIsSpaceAsciiChar".} =
|
|
## Checks whether or not `c` is a whitespace character.
|
|
runnableExamples:
|
|
doAssert isSpaceAscii('n') == false
|
|
doAssert isSpaceAscii(' ') == true
|
|
doAssert isSpaceAscii('\t') == true
|
|
return c in Whitespace
|
|
|
|
func isLowerAscii*(c: char): bool {.rtl, extern: "nsuIsLowerAsciiChar".} =
|
|
## Checks whether or not `c` is a lower case character.
|
|
##
|
|
## This checks ASCII characters only.
|
|
## Use `Unicode module<unicode.html>`_ for UTF-8 support.
|
|
##
|
|
## See also:
|
|
## * `toLowerAscii func<#toLowerAscii,char>`_
|
|
runnableExamples:
|
|
doAssert isLowerAscii('e') == true
|
|
doAssert isLowerAscii('E') == false
|
|
doAssert isLowerAscii('7') == false
|
|
return c in LowercaseLetters
|
|
|
|
func isUpperAscii*(c: char): bool {.rtl, extern: "nsuIsUpperAsciiChar".} =
|
|
## Checks whether or not `c` is an upper case character.
|
|
##
|
|
## This checks ASCII characters only.
|
|
## Use `Unicode module<unicode.html>`_ for UTF-8 support.
|
|
##
|
|
## See also:
|
|
## * `toUpperAscii func<#toUpperAscii,char>`_
|
|
runnableExamples:
|
|
doAssert isUpperAscii('e') == false
|
|
doAssert isUpperAscii('E') == true
|
|
doAssert isUpperAscii('7') == false
|
|
return c in UppercaseLetters
|
|
|
|
func toLowerAscii*(c: char): char {.rtl, extern: "nsuToLowerAsciiChar".} =
|
|
## Returns the lower case version of character `c`.
|
|
##
|
|
## This works only for the letters `A-Z`. See `unicode.toLower
|
|
## <unicode.html#toLower,Rune>`_ for a version that works for any Unicode
|
|
## character.
|
|
##
|
|
## See also:
|
|
## * `isLowerAscii func<#isLowerAscii,char>`_
|
|
## * `toLowerAscii func<#toLowerAscii,string>`_ for converting a string
|
|
runnableExamples:
|
|
doAssert toLowerAscii('A') == 'a'
|
|
doAssert toLowerAscii('e') == 'e'
|
|
if c in UppercaseLetters:
|
|
result = char(uint8(c) xor 0b0010_0000'u8)
|
|
else:
|
|
result = c
|
|
|
|
template toImpl(call) =
|
|
result = newString(len(s))
|
|
for i in 0..len(s) - 1:
|
|
result[i] = call(s[i])
|
|
|
|
func toLowerAscii*(s: string): string {.rtl, extern: "nsuToLowerAsciiStr".} =
|
|
## Converts string `s` into lower case.
|
|
##
|
|
## This works only for the letters `A-Z`. See `unicode.toLower
|
|
## <unicode.html#toLower,string>`_ for a version that works for any Unicode
|
|
## character.
|
|
##
|
|
## See also:
|
|
## * `normalize func<#normalize,string>`_
|
|
runnableExamples:
|
|
doAssert toLowerAscii("FooBar!") == "foobar!"
|
|
toImpl toLowerAscii
|
|
|
|
func toUpperAscii*(c: char): char {.rtl, extern: "nsuToUpperAsciiChar".} =
|
|
## Converts character `c` into upper case.
|
|
##
|
|
## This works only for the letters `A-Z`. See `unicode.toUpper
|
|
## <unicode.html#toUpper,Rune>`_ for a version that works for any Unicode
|
|
## character.
|
|
##
|
|
## See also:
|
|
## * `isUpperAscii func<#isUpperAscii,char>`_
|
|
## * `toUpperAscii func<#toUpperAscii,string>`_ for converting a string
|
|
## * `capitalizeAscii func<#capitalizeAscii,string>`_
|
|
runnableExamples:
|
|
doAssert toUpperAscii('a') == 'A'
|
|
doAssert toUpperAscii('E') == 'E'
|
|
if c in LowercaseLetters:
|
|
result = char(uint8(c) xor 0b0010_0000'u8)
|
|
else:
|
|
result = c
|
|
|
|
func toUpperAscii*(s: string): string {.rtl, extern: "nsuToUpperAsciiStr".} =
|
|
## Converts string `s` into upper case.
|
|
##
|
|
## This works only for the letters `A-Z`. See `unicode.toUpper
|
|
## <unicode.html#toUpper,string>`_ for a version that works for any Unicode
|
|
## character.
|
|
##
|
|
## See also:
|
|
## * `capitalizeAscii func<#capitalizeAscii,string>`_
|
|
runnableExamples:
|
|
doAssert toUpperAscii("FooBar!") == "FOOBAR!"
|
|
toImpl toUpperAscii
|
|
|
|
func capitalizeAscii*(s: string): string {.rtl, extern: "nsuCapitalizeAscii".} =
|
|
## Converts the first character of string `s` into upper case.
|
|
##
|
|
## This works only for the letters `A-Z`.
|
|
## Use `Unicode module<unicode.html>`_ for UTF-8 support.
|
|
##
|
|
## See also:
|
|
## * `toUpperAscii func<#toUpperAscii,char>`_
|
|
runnableExamples:
|
|
doAssert capitalizeAscii("foo") == "Foo"
|
|
doAssert capitalizeAscii("-bar") == "-bar"
|
|
if s.len == 0: result = ""
|
|
else: result = toUpperAscii(s[0]) & substr(s, 1)
|
|
|
|
func nimIdentNormalize*(s: string): string =
|
|
## Normalizes the string `s` as a Nim identifier.
|
|
##
|
|
## That means to convert to lower case and remove any '_' on all characters
|
|
## except first one.
|
|
##
|
|
## .. Warning:: Backticks (`) are not handled: they remain *as is* and
|
|
## spaces are preserved. See `nimIdentBackticksNormalize
|
|
## <dochelpers.html#nimIdentBackticksNormalize,string>`_ for
|
|
## an alternative approach.
|
|
runnableExamples:
|
|
doAssert nimIdentNormalize("Foo_bar") == "Foobar"
|
|
result = newString(s.len)
|
|
if s.len == 0:
|
|
return
|
|
result[0] = s[0]
|
|
var j = 1
|
|
for i in 1..len(s) - 1:
|
|
if s[i] in UppercaseLetters:
|
|
result[j] = chr(ord(s[i]) + (ord('a') - ord('A')))
|
|
inc j
|
|
elif s[i] != '_':
|
|
result[j] = s[i]
|
|
inc j
|
|
if j != s.len: setLen(result, j)
|
|
|
|
func normalize*(s: string): string {.rtl, extern: "nsuNormalize".} =
|
|
## Normalizes the string `s`.
|
|
##
|
|
## That means to convert it to lower case and remove any '_'. This
|
|
## should NOT be used to normalize Nim identifier names.
|
|
##
|
|
## See also:
|
|
## * `toLowerAscii func<#toLowerAscii,string>`_
|
|
runnableExamples:
|
|
doAssert normalize("Foo_bar") == "foobar"
|
|
doAssert normalize("Foo Bar") == "foo bar"
|
|
result = newString(s.len)
|
|
var j = 0
|
|
for i in 0..len(s) - 1:
|
|
if s[i] in UppercaseLetters:
|
|
result[j] = chr(ord(s[i]) + (ord('a') - ord('A')))
|
|
inc j
|
|
elif s[i] != '_':
|
|
result[j] = s[i]
|
|
inc j
|
|
if j != s.len: setLen(result, j)
|
|
|
|
func cmpIgnoreCase*(a, b: string): int {.rtl, extern: "nsuCmpIgnoreCase".} =
|
|
## Compares two strings in a case insensitive manner. Returns:
|
|
##
|
|
## | `0` if a == b
|
|
## | `< 0` if a < b
|
|
## | `> 0` if a > b
|
|
runnableExamples:
|
|
doAssert cmpIgnoreCase("FooBar", "foobar") == 0
|
|
doAssert cmpIgnoreCase("bar", "Foo") < 0
|
|
doAssert cmpIgnoreCase("Foo5", "foo4") > 0
|
|
cmpIgnoreCaseImpl(a, b)
|
|
|
|
{.push checks: off, line_trace: off.} # this is a hot-spot in the compiler!
|
|
# thus we compile without checks here
|
|
|
|
func cmpIgnoreStyle*(a, b: string): int {.rtl, extern: "nsuCmpIgnoreStyle".} =
|
|
## Semantically the same as `cmp(normalize(a), normalize(b))`. It
|
|
## is just optimized to not allocate temporary strings. This should
|
|
## NOT be used to compare Nim identifier names.
|
|
## Use `macros.eqIdent<macros.html#eqIdent,string,string>`_ for that.
|
|
##
|
|
## Returns:
|
|
##
|
|
## | `0` if a == b
|
|
## | `< 0` if a < b
|
|
## | `> 0` if a > b
|
|
runnableExamples:
|
|
doAssert cmpIgnoreStyle("foo_bar", "FooBar") == 0
|
|
doAssert cmpIgnoreStyle("foo_bar_5", "FooBar4") > 0
|
|
cmpIgnoreStyleImpl(a, b)
|
|
{.pop.}
|
|
|
|
# --------- Private templates for different split separators -----------
|
|
|
|
func substrEq(s: string, pos: int, substr: string): bool =
|
|
# Always returns false for empty `substr`
|
|
var length = substr.len
|
|
if length > 0:
|
|
var i = 0
|
|
while i < length and pos+i < s.len and s[pos+i] == substr[i]:
|
|
inc i
|
|
i == length
|
|
else: false
|
|
|
|
template stringHasSep(s: string, index: int, seps: set[char]): bool =
|
|
s[index] in seps
|
|
|
|
template stringHasSep(s: string, index: int, sep: char): bool =
|
|
s[index] == sep
|
|
|
|
template stringHasSep(s: string, index: int, sep: string): bool =
|
|
s.substrEq(index, sep)
|
|
|
|
template splitCommon(s, sep, maxsplit, sepLen) =
|
|
## Common code for split procs
|
|
var last = 0
|
|
var splits = maxsplit
|
|
|
|
while last <= len(s):
|
|
var first = last
|
|
while last < len(s) and not stringHasSep(s, last, sep):
|
|
inc(last)
|
|
if splits == 0: last = len(s)
|
|
yield substr(s, first, last-1)
|
|
if splits == 0: break
|
|
dec(splits)
|
|
inc(last, sepLen)
|
|
|
|
template oldSplit(s, seps, maxsplit) =
|
|
var last = 0
|
|
var splits = maxsplit
|
|
assert(not ('\0' in seps))
|
|
while last < len(s):
|
|
while last < len(s) and s[last] in seps: inc(last)
|
|
var first = last
|
|
while last < len(s) and s[last] notin seps: inc(last)
|
|
if first <= last-1:
|
|
if splits == 0: last = len(s)
|
|
yield substr(s, first, last-1)
|
|
if splits == 0: break
|
|
dec(splits)
|
|
|
|
template accResult(iter: untyped) =
|
|
result = @[]
|
|
for x in iter: add(result, x)
|
|
|
|
|
|
iterator split*(s: string, sep: char, maxsplit: int = -1): string =
|
|
## Splits the string `s` into substrings using a single separator.
|
|
##
|
|
## Substrings are separated by the character `sep`.
|
|
## The code:
|
|
## ```nim
|
|
## for word in split(";;this;is;an;;example;;;", ';'):
|
|
## writeLine(stdout, word)
|
|
## ```
|
|
## Results in:
|
|
## ```
|
|
## ""
|
|
## ""
|
|
## "this"
|
|
## "is"
|
|
## "an"
|
|
## ""
|
|
## "example"
|
|
## ""
|
|
## ""
|
|
## ""
|
|
## ```
|
|
##
|
|
## See also:
|
|
## * `rsplit iterator<#rsplit.i,string,char,int>`_
|
|
## * `splitLines iterator<#splitLines.i,string>`_
|
|
## * `splitWhitespace iterator<#splitWhitespace.i,string,int>`_
|
|
## * `split func<#split,string,char,int>`_
|
|
splitCommon(s, sep, maxsplit, 1)
|
|
|
|
iterator split*(s: string, seps: set[char] = Whitespace,
|
|
maxsplit: int = -1): string =
|
|
## Splits the string `s` into substrings using a group of separators.
|
|
##
|
|
## Substrings are separated by a substring containing only `seps`.
|
|
##
|
|
## ```nim
|
|
## for word in split("this\lis an\texample"):
|
|
## writeLine(stdout, word)
|
|
## ```
|
|
##
|
|
## ...generates this output:
|
|
##
|
|
## ```
|
|
## "this"
|
|
## "is"
|
|
## "an"
|
|
## "example"
|
|
## ```
|
|
##
|
|
## And the following code:
|
|
##
|
|
## ```nim
|
|
## for word in split("this:is;an$example", {';', ':', '$'}):
|
|
## writeLine(stdout, word)
|
|
## ```
|
|
##
|
|
## ...produces the same output as the first example. The code:
|
|
##
|
|
## ```nim
|
|
## let date = "2012-11-20T22:08:08.398990"
|
|
## let separators = {' ', '-', ':', 'T'}
|
|
## for number in split(date, separators):
|
|
## writeLine(stdout, number)
|
|
## ```
|
|
##
|
|
## ...results in:
|
|
##
|
|
## ```
|
|
## "2012"
|
|
## "11"
|
|
## "20"
|
|
## "22"
|
|
## "08"
|
|
## "08.398990"
|
|
## ```
|
|
##
|
|
## .. note:: Empty separator set results in returning an original string,
|
|
## following the interpretation "split by no element".
|
|
##
|
|
## See also:
|
|
## * `rsplit iterator<#rsplit.i,string,set[char],int>`_
|
|
## * `splitLines iterator<#splitLines.i,string>`_
|
|
## * `splitWhitespace iterator<#splitWhitespace.i,string,int>`_
|
|
## * `split func<#split,string,set[char],int>`_
|
|
splitCommon(s, seps, maxsplit, 1)
|
|
|
|
iterator split*(s: string, sep: string, maxsplit: int = -1): string =
|
|
## Splits the string `s` into substrings using a string separator.
|
|
##
|
|
## Substrings are separated by the string `sep`.
|
|
## The code:
|
|
##
|
|
## ```nim
|
|
## for word in split("thisDATAisDATAcorrupted", "DATA"):
|
|
## writeLine(stdout, word)
|
|
## ```
|
|
##
|
|
## Results in:
|
|
##
|
|
## ```
|
|
## "this"
|
|
## "is"
|
|
## "corrupted"
|
|
## ```
|
|
##
|
|
## .. note:: Empty separator string results in returning an original string,
|
|
## following the interpretation "split by no element".
|
|
##
|
|
## See also:
|
|
## * `rsplit iterator<#rsplit.i,string,string,int,bool>`_
|
|
## * `splitLines iterator<#splitLines.i,string>`_
|
|
## * `splitWhitespace iterator<#splitWhitespace.i,string,int>`_
|
|
## * `split func<#split,string,string,int>`_
|
|
let sepLen = if sep.len == 0: 1 # prevents infinite loop
|
|
else: sep.len
|
|
splitCommon(s, sep, maxsplit, sepLen)
|
|
|
|
|
|
template rsplitCommon(s, sep, maxsplit, sepLen) =
|
|
## Common code for rsplit functions
|
|
var
|
|
last = s.len - 1
|
|
first = last
|
|
splits = maxsplit
|
|
startPos = 0
|
|
# go to -1 in order to get separators at the beginning
|
|
while first >= -1:
|
|
while first >= 0 and not stringHasSep(s, first, sep):
|
|
dec(first)
|
|
if splits == 0:
|
|
# No more splits means set first to the beginning
|
|
first = -1
|
|
if first == -1:
|
|
startPos = 0
|
|
else:
|
|
startPos = first + sepLen
|
|
yield substr(s, startPos, last)
|
|
if splits == 0: break
|
|
dec(splits)
|
|
dec(first)
|
|
last = first
|
|
|
|
iterator rsplit*(s: string, sep: char,
|
|
maxsplit: int = -1): string =
|
|
## Splits the string `s` into substrings from the right using a
|
|
## string separator. Works exactly the same as `split iterator
|
|
## <#split.i,string,char,int>`_ except in **reverse** order.
|
|
##
|
|
## ```nim
|
|
## for piece in "foo:bar".rsplit(':'):
|
|
## echo piece
|
|
## ```
|
|
##
|
|
## Results in:
|
|
##
|
|
## ```
|
|
## "bar"
|
|
## "foo"
|
|
## ```
|
|
##
|
|
## Substrings are separated from the right by the char `sep`.
|
|
##
|
|
## See also:
|
|
## * `split iterator<#split.i,string,char,int>`_
|
|
## * `splitLines iterator<#splitLines.i,string>`_
|
|
## * `splitWhitespace iterator<#splitWhitespace.i,string,int>`_
|
|
## * `rsplit func<#rsplit,string,char,int>`_
|
|
rsplitCommon(s, sep, maxsplit, 1)
|
|
|
|
iterator rsplit*(s: string, seps: set[char] = Whitespace,
|
|
maxsplit: int = -1): string =
|
|
## Splits the string `s` into substrings from the right using a
|
|
## string separator. Works exactly the same as `split iterator
|
|
## <#split.i,string,char,int>`_ except in **reverse** order.
|
|
##
|
|
## ```nim
|
|
## for piece in "foo bar".rsplit(WhiteSpace):
|
|
## echo piece
|
|
## ```
|
|
##
|
|
## Results in:
|
|
##
|
|
## ```
|
|
## "bar"
|
|
## "foo"
|
|
## ```
|
|
##
|
|
## Substrings are separated from the right by the set of chars `seps`
|
|
##
|
|
## .. note:: Empty separator set results in returning an original string,
|
|
## following the interpretation "split by no element".
|
|
##
|
|
## See also:
|
|
## * `split iterator<#split.i,string,set[char],int>`_
|
|
## * `splitLines iterator<#splitLines.i,string>`_
|
|
## * `splitWhitespace iterator<#splitWhitespace.i,string,int>`_
|
|
## * `rsplit func<#rsplit,string,set[char],int>`_
|
|
rsplitCommon(s, seps, maxsplit, 1)
|
|
|
|
iterator rsplit*(s: string, sep: string, maxsplit: int = -1,
|
|
keepSeparators: bool = false): string =
|
|
## Splits the string `s` into substrings from the right using a
|
|
## string separator. Works exactly the same as `split iterator
|
|
## <#split.i,string,string,int>`_ except in **reverse** order.
|
|
##
|
|
## ```nim
|
|
## for piece in "foothebar".rsplit("the"):
|
|
## echo piece
|
|
## ```
|
|
##
|
|
## Results in:
|
|
##
|
|
## ```
|
|
## "bar"
|
|
## "foo"
|
|
## ```
|
|
##
|
|
## Substrings are separated from the right by the string `sep`
|
|
##
|
|
## .. note:: Empty separator string results in returning an original string,
|
|
## following the interpretation "split by no element".
|
|
##
|
|
## See also:
|
|
## * `split iterator<#split.i,string,string,int>`_
|
|
## * `splitLines iterator<#splitLines.i,string>`_
|
|
## * `splitWhitespace iterator<#splitWhitespace.i,string,int>`_
|
|
## * `rsplit func<#rsplit,string,string,int>`_
|
|
let sepLen = if sep.len == 0: 1 # prevents infinite loop
|
|
else: sep.len
|
|
rsplitCommon(s, sep, maxsplit, sepLen)
|
|
|
|
iterator splitLines*(s: string, keepEol = false): string =
|
|
## Splits the string `s` into its containing lines.
|
|
##
|
|
## Every `character literal <manual.html#lexical-analysis-character-literals>`_
|
|
## newline combination (CR, LF, CR-LF) is supported. The result strings
|
|
## contain no trailing end of line characters unless the parameter `keepEol`
|
|
## is set to `true`.
|
|
##
|
|
## Example:
|
|
##
|
|
## ```nim
|
|
## for line in splitLines("\nthis\nis\nan\n\nexample\n"):
|
|
## writeLine(stdout, line)
|
|
## ```
|
|
##
|
|
## Results in:
|
|
##
|
|
## ```nim
|
|
## ""
|
|
## "this"
|
|
## "is"
|
|
## "an"
|
|
## ""
|
|
## "example"
|
|
## ""
|
|
## ```
|
|
##
|
|
## See also:
|
|
## * `splitWhitespace iterator<#splitWhitespace.i,string,int>`_
|
|
## * `splitLines func<#splitLines,string>`_
|
|
var first = 0
|
|
var last = 0
|
|
var eolpos = 0
|
|
while true:
|
|
while last < s.len and s[last] notin {'\c', '\l'}: inc(last)
|
|
|
|
eolpos = last
|
|
if last < s.len:
|
|
if s[last] == '\l': inc(last)
|
|
elif s[last] == '\c':
|
|
inc(last)
|
|
if last < s.len and s[last] == '\l': inc(last)
|
|
|
|
yield substr(s, first, if keepEol: last-1 else: eolpos-1)
|
|
|
|
# no eol characters consumed means that the string is over
|
|
if eolpos == last:
|
|
break
|
|
|
|
first = last
|
|
|
|
iterator splitWhitespace*(s: string, maxsplit: int = -1): string =
|
|
## Splits the string `s` at whitespace stripping leading and trailing
|
|
## whitespace if necessary. If `maxsplit` is specified and is positive,
|
|
## no more than `maxsplit` splits is made.
|
|
##
|
|
## The following code:
|
|
##
|
|
## ```nim
|
|
## let s = " foo \t bar baz "
|
|
## for ms in [-1, 1, 2, 3]:
|
|
## echo "------ maxsplit = ", ms, ":"
|
|
## for item in s.splitWhitespace(maxsplit=ms):
|
|
## echo '"', item, '"'
|
|
## ```
|
|
##
|
|
## ...results in:
|
|
##
|
|
## ```
|
|
## ------ maxsplit = -1:
|
|
## "foo"
|
|
## "bar"
|
|
## "baz"
|
|
## ------ maxsplit = 1:
|
|
## "foo"
|
|
## "bar baz "
|
|
## ------ maxsplit = 2:
|
|
## "foo"
|
|
## "bar"
|
|
## "baz "
|
|
## ------ maxsplit = 3:
|
|
## "foo"
|
|
## "bar"
|
|
## "baz"
|
|
## ```
|
|
##
|
|
## See also:
|
|
## * `splitLines iterator<#splitLines.i,string>`_
|
|
## * `splitWhitespace func<#splitWhitespace,string,int>`_
|
|
oldSplit(s, Whitespace, maxsplit)
|
|
|
|
|
|
|
|
func split*(s: string, sep: char, maxsplit: int = -1): seq[string] {.rtl,
|
|
extern: "nsuSplitChar".} =
|
|
## The same as the `split iterator <#split.i,string,char,int>`_ (see its
|
|
## documentation), but is a func that returns a sequence of substrings.
|
|
##
|
|
## See also:
|
|
## * `split iterator <#split.i,string,char,int>`_
|
|
## * `rsplit func<#rsplit,string,char,int>`_
|
|
## * `splitLines func<#splitLines,string>`_
|
|
## * `splitWhitespace func<#splitWhitespace,string,int>`_
|
|
runnableExamples:
|
|
doAssert "a,b,c".split(',') == @["a", "b", "c"]
|
|
doAssert "".split(' ') == @[""]
|
|
accResult(split(s, sep, maxsplit))
|
|
|
|
func split*(s: string, seps: set[char] = Whitespace, maxsplit: int = -1): seq[
|
|
string] {.rtl, extern: "nsuSplitCharSet".} =
|
|
## The same as the `split iterator <#split.i,string,set[char],int>`_ (see its
|
|
## documentation), but is a func that returns a sequence of substrings.
|
|
##
|
|
## .. note:: Empty separator set results in returning an original string,
|
|
## following the interpretation "split by no element".
|
|
##
|
|
## See also:
|
|
## * `split iterator <#split.i,string,set[char],int>`_
|
|
## * `rsplit func<#rsplit,string,set[char],int>`_
|
|
## * `splitLines func<#splitLines,string>`_
|
|
## * `splitWhitespace func<#splitWhitespace,string,int>`_
|
|
runnableExamples:
|
|
doAssert "a,b;c".split({',', ';'}) == @["a", "b", "c"]
|
|
doAssert "".split({' '}) == @[""]
|
|
doAssert "empty seps return unsplit s".split({}) == @["empty seps return unsplit s"]
|
|
accResult(split(s, seps, maxsplit))
|
|
|
|
func split*(s: string, sep: string, maxsplit: int = -1): seq[string] {.rtl,
|
|
extern: "nsuSplitString".} =
|
|
## Splits the string `s` into substrings using a string separator.
|
|
##
|
|
## Substrings are separated by the string `sep`. This is a wrapper around the
|
|
## `split iterator <#split.i,string,string,int>`_.
|
|
##
|
|
## .. note:: Empty separator string results in returning an original string,
|
|
## following the interpretation "split by no element".
|
|
##
|
|
## See also:
|
|
## * `split iterator <#split.i,string,string,int>`_
|
|
## * `rsplit func<#rsplit,string,string,int>`_
|
|
## * `splitLines func<#splitLines,string>`_
|
|
## * `splitWhitespace func<#splitWhitespace,string,int>`_
|
|
runnableExamples:
|
|
doAssert "a,b,c".split(",") == @["a", "b", "c"]
|
|
doAssert "a man a plan a canal panama".split("a ") == @["", "man ", "plan ", "canal panama"]
|
|
doAssert "".split("Elon Musk") == @[""]
|
|
doAssert "a largely spaced sentence".split(" ") == @["a", "", "largely",
|
|
"", "", "", "spaced", "sentence"]
|
|
doAssert "a largely spaced sentence".split(" ", maxsplit = 1) == @["a", " largely spaced sentence"]
|
|
doAssert "empty sep returns unsplit s".split("") == @["empty sep returns unsplit s"]
|
|
accResult(split(s, sep, maxsplit))
|
|
|
|
func rsplit*(s: string, sep: char, maxsplit: int = -1): seq[string] {.rtl,
|
|
extern: "nsuRSplitChar".} =
|
|
## The same as the `rsplit iterator <#rsplit.i,string,char,int>`_, but is a func
|
|
## that returns a sequence of substrings in original order.
|
|
##
|
|
## A possible common use case for `rsplit` is path manipulation,
|
|
## particularly on systems that don't use a common delimiter.
|
|
##
|
|
## For example, if a system had `#` as a delimiter, you could
|
|
## do the following to get the tail of the path:
|
|
##
|
|
## ```nim
|
|
## var tailSplit = rsplit("Root#Object#Method#Index", '#', maxsplit=1)
|
|
## ```
|
|
##
|
|
## Results in `tailSplit` containing:
|
|
##
|
|
## ```nim
|
|
## @["Root#Object#Method", "Index"]
|
|
## ```
|
|
##
|
|
## See also:
|
|
## * `rsplit iterator <#rsplit.i,string,char,int>`_
|
|
## * `split func<#split,string,char,int>`_
|
|
## * `splitLines func<#splitLines,string>`_
|
|
## * `splitWhitespace func<#splitWhitespace,string,int>`_
|
|
accResult(rsplit(s, sep, maxsplit))
|
|
result.reverse()
|
|
|
|
func rsplit*(s: string, seps: set[char] = Whitespace,
|
|
maxsplit: int = -1): seq[string]
|
|
{.rtl, extern: "nsuRSplitCharSet".} =
|
|
## The same as the `rsplit iterator <#rsplit.i,string,set[char],int>`_, but is a
|
|
## func that returns a sequence of substrings in original order.
|
|
##
|
|
## A possible common use case for `rsplit` is path manipulation,
|
|
## particularly on systems that don't use a common delimiter.
|
|
##
|
|
## For example, if a system had `#` as a delimiter, you could
|
|
## do the following to get the tail of the path:
|
|
##
|
|
## ```nim
|
|
## var tailSplit = rsplit("Root#Object#Method#Index", {'#'}, maxsplit=1)
|
|
## ```
|
|
##
|
|
## Results in `tailSplit` containing:
|
|
##
|
|
## ```nim
|
|
## @["Root#Object#Method", "Index"]
|
|
## ```
|
|
##
|
|
## .. note:: Empty separator set results in returning an original string,
|
|
## following the interpretation "split by no element".
|
|
##
|
|
## See also:
|
|
## * `rsplit iterator <#rsplit.i,string,set[char],int>`_
|
|
## * `split func<#split,string,set[char],int>`_
|
|
## * `splitLines func<#splitLines,string>`_
|
|
## * `splitWhitespace func<#splitWhitespace,string,int>`_
|
|
accResult(rsplit(s, seps, maxsplit))
|
|
result.reverse()
|
|
|
|
func rsplit*(s: string, sep: string, maxsplit: int = -1): seq[string] {.rtl,
|
|
extern: "nsuRSplitString".} =
|
|
## The same as the `rsplit iterator <#rsplit.i,string,string,int,bool>`_, but is a func
|
|
## that returns a sequence of substrings in original order.
|
|
##
|
|
## A possible common use case for `rsplit` is path manipulation,
|
|
## particularly on systems that don't use a common delimiter.
|
|
##
|
|
## For example, if a system had `#` as a delimiter, you could
|
|
## do the following to get the tail of the path:
|
|
##
|
|
## ```nim
|
|
## var tailSplit = rsplit("Root#Object#Method#Index", "#", maxsplit=1)
|
|
## ```
|
|
##
|
|
## Results in `tailSplit` containing:
|
|
##
|
|
## ```nim
|
|
## @["Root#Object#Method", "Index"]
|
|
## ```
|
|
##
|
|
## .. note:: Empty separator string results in returning an original string,
|
|
## following the interpretation "split by no element".
|
|
##
|
|
## See also:
|
|
## * `rsplit iterator <#rsplit.i,string,string,int,bool>`_
|
|
## * `split func<#split,string,string,int>`_
|
|
## * `splitLines func<#splitLines,string>`_
|
|
## * `splitWhitespace func<#splitWhitespace,string,int>`_
|
|
runnableExamples:
|
|
doAssert "a largely spaced sentence".rsplit(" ", maxsplit = 1) == @[
|
|
"a largely spaced", "sentence"]
|
|
doAssert "a,b,c".rsplit(",") == @["a", "b", "c"]
|
|
doAssert "a man a plan a canal panama".rsplit("a ") == @["", "man ",
|
|
"plan ", "canal panama"]
|
|
doAssert "".rsplit("Elon Musk") == @[""]
|
|
doAssert "a largely spaced sentence".rsplit(" ") == @["a", "",
|
|
"largely", "", "", "", "spaced", "sentence"]
|
|
doAssert "empty sep returns unsplit s".rsplit("") == @["empty sep returns unsplit s"]
|
|
accResult(rsplit(s, sep, maxsplit))
|
|
result.reverse()
|
|
|
|
func splitLines*(s: string, keepEol = false): seq[string] {.rtl,
|
|
extern: "nsuSplitLines".} =
|
|
## The same as the `splitLines iterator<#splitLines.i,string>`_ (see its
|
|
## documentation), but is a func that returns a sequence of substrings.
|
|
##
|
|
## See also:
|
|
## * `splitLines iterator<#splitLines.i,string>`_
|
|
## * `splitWhitespace func<#splitWhitespace,string,int>`_
|
|
## * `countLines func<#countLines,string>`_
|
|
accResult(splitLines(s, keepEol = keepEol))
|
|
|
|
func splitWhitespace*(s: string, maxsplit: int = -1): seq[string] {.rtl,
|
|
extern: "nsuSplitWhitespace".} =
|
|
## The same as the `splitWhitespace iterator <#splitWhitespace.i,string,int>`_
|
|
## (see its documentation), but is a func that returns a sequence of substrings.
|
|
##
|
|
## See also:
|
|
## * `splitWhitespace iterator <#splitWhitespace.i,string,int>`_
|
|
## * `splitLines func<#splitLines,string>`_
|
|
accResult(splitWhitespace(s, maxsplit))
|
|
|
|
func toBin*(x: BiggestInt, len: Positive): string {.rtl, extern: "nsuToBin".} =
|
|
## Converts `x` into its binary representation.
|
|
##
|
|
## The resulting string is always `len` characters long. No leading `0b`
|
|
## prefix is generated.
|
|
runnableExamples:
|
|
let
|
|
a = 29
|
|
b = 257
|
|
doAssert a.toBin(8) == "00011101"
|
|
doAssert b.toBin(8) == "00000001"
|
|
doAssert b.toBin(9) == "100000001"
|
|
var
|
|
mask = BiggestUInt 1
|
|
shift = BiggestUInt 0
|
|
assert(len > 0)
|
|
result = newString(len)
|
|
for j in countdown(len-1, 0):
|
|
result[j] = chr(int((BiggestUInt(x) and mask) shr shift) + ord('0'))
|
|
inc shift
|
|
mask = mask shl BiggestUInt(1)
|
|
|
|
func toOct*(x: BiggestInt, len: Positive): string {.rtl, extern: "nsuToOct".} =
|
|
## Converts `x` into its octal representation.
|
|
##
|
|
## The resulting string is always `len` characters long. No leading `0o`
|
|
## prefix is generated.
|
|
##
|
|
## Do not confuse it with `toOctal func<#toOctal,char>`_.
|
|
runnableExamples:
|
|
let
|
|
a = 62
|
|
b = 513
|
|
doAssert a.toOct(3) == "076"
|
|
doAssert b.toOct(3) == "001"
|
|
doAssert b.toOct(5) == "01001"
|
|
var
|
|
mask = BiggestUInt 7
|
|
shift = BiggestUInt 0
|
|
assert(len > 0)
|
|
result = newString(len)
|
|
for j in countdown(len-1, 0):
|
|
result[j] = chr(int((BiggestUInt(x) and mask) shr shift) + ord('0'))
|
|
inc shift, 3
|
|
mask = mask shl BiggestUInt(3)
|
|
|
|
func toHexImpl(x: BiggestUInt, len: Positive, handleNegative: bool): string =
|
|
const
|
|
HexChars = "0123456789ABCDEF"
|
|
var n = x
|
|
result = newString(len)
|
|
for j in countdown(len-1, 0):
|
|
result[j] = HexChars[int(n and 0xF)]
|
|
n = n shr 4
|
|
# handle negative overflow
|
|
if n == 0 and handleNegative: n = not(BiggestUInt 0)
|
|
|
|
func toHex*[T: SomeInteger](x: T, len: Positive): string =
|
|
## Converts `x` to its hexadecimal representation.
|
|
##
|
|
## The resulting string will be exactly `len` characters long. No prefix like
|
|
## `0x` is generated. `x` is treated as an unsigned value.
|
|
runnableExamples:
|
|
let
|
|
a = 62'u64
|
|
b = 4097'u64
|
|
doAssert a.toHex(3) == "03E"
|
|
doAssert b.toHex(3) == "001"
|
|
doAssert b.toHex(4) == "1001"
|
|
doAssert toHex(62, 3) == "03E"
|
|
doAssert toHex(-8, 6) == "FFFFF8"
|
|
when jsNoBigInt64:
|
|
toHexImpl(cast[BiggestUInt](x), len, x < 0)
|
|
else:
|
|
when T is SomeSignedInt:
|
|
toHexImpl(cast[BiggestUInt](BiggestInt(x)), len, x < 0)
|
|
else:
|
|
toHexImpl(BiggestUInt(x), len, x < 0)
|
|
|
|
func toHex*[T: SomeInteger](x: T): string =
|
|
## Shortcut for `toHex(x, T.sizeof * 2)`
|
|
runnableExamples:
|
|
doAssert toHex(1984'i64) == "00000000000007C0"
|
|
doAssert toHex(1984'i16) == "07C0"
|
|
when jsNoBigInt64:
|
|
toHexImpl(cast[BiggestUInt](x), 2*sizeof(T), x < 0)
|
|
else:
|
|
when T is SomeSignedInt:
|
|
toHexImpl(cast[BiggestUInt](BiggestInt(x)), 2*sizeof(T), x < 0)
|
|
else:
|
|
toHexImpl(BiggestUInt(x), 2*sizeof(T), x < 0)
|
|
|
|
func toHex*(s: string): string {.rtl.} =
|
|
## Converts a bytes string to its hexadecimal representation.
|
|
##
|
|
## The output is twice the input long. No prefix like
|
|
## `0x` is generated.
|
|
##
|
|
## See also:
|
|
## * `parseHexStr func<#parseHexStr,string>`_ for the reverse operation
|
|
runnableExamples:
|
|
let
|
|
a = "1"
|
|
b = "A"
|
|
c = "\0\255"
|
|
doAssert a.toHex() == "31"
|
|
doAssert b.toHex() == "41"
|
|
doAssert c.toHex() == "00FF"
|
|
|
|
const HexChars = "0123456789ABCDEF"
|
|
result = newString(s.len * 2)
|
|
for pos, c in s:
|
|
var n = ord(c)
|
|
result[pos * 2 + 1] = HexChars[n and 0xF]
|
|
n = n shr 4
|
|
result[pos * 2] = HexChars[n]
|
|
|
|
func toOctal*(c: char): string {.rtl, extern: "nsuToOctal".} =
|
|
## Converts a character `c` to its octal representation.
|
|
##
|
|
## The resulting string may not have a leading zero. Its length is always
|
|
## exactly 3.
|
|
##
|
|
## Do not confuse it with `toOct func<#toOct,BiggestInt,Positive>`_.
|
|
runnableExamples:
|
|
doAssert toOctal('1') == "061"
|
|
doAssert toOctal('A') == "101"
|
|
doAssert toOctal('a') == "141"
|
|
doAssert toOctal('!') == "041"
|
|
|
|
result = newString(3)
|
|
var val = ord(c)
|
|
for i in countdown(2, 0):
|
|
result[i] = chr(val mod 8 + ord('0'))
|
|
val = val div 8
|
|
|
|
func fromBin*[T: SomeInteger](s: string): T =
|
|
## Parses a binary integer value from a string `s`.
|
|
##
|
|
## If `s` is not a valid binary integer, `ValueError` is raised. `s` can have
|
|
## one of the following optional prefixes: `0b`, `0B`. Underscores within
|
|
## `s` are ignored.
|
|
##
|
|
## Does not check for overflow. If the value represented by `s`
|
|
## is too big to fit into a return type, only the value of the rightmost
|
|
## binary digits of `s` is returned without producing an error.
|
|
runnableExamples:
|
|
let s = "0b_0100_1000_1000_1000_1110_1110_1001_1001"
|
|
doAssert fromBin[int](s) == 1216933529
|
|
doAssert fromBin[int8](s) == 0b1001_1001'i8
|
|
doAssert fromBin[int8](s) == -103'i8
|
|
doAssert fromBin[uint8](s) == 153
|
|
doAssert s.fromBin[:int16] == 0b1110_1110_1001_1001'i16
|
|
doAssert s.fromBin[:uint64] == 1216933529'u64
|
|
result = T(0)
|
|
let p = parseutils.parseBin(s, result)
|
|
if p != s.len or p == 0:
|
|
raise newException(ValueError, "invalid binary integer: " & s)
|
|
|
|
func fromOct*[T: SomeInteger](s: string): T =
|
|
## Parses an octal integer value from a string `s`.
|
|
##
|
|
## If `s` is not a valid octal integer, `ValueError` is raised. `s` can have
|
|
## one of the following optional prefixes: `0o`, `0O`. Underscores within
|
|
## `s` are ignored.
|
|
##
|
|
## Does not check for overflow. If the value represented by `s`
|
|
## is too big to fit into a return type, only the value of the rightmost
|
|
## octal digits of `s` is returned without producing an error.
|
|
runnableExamples:
|
|
let s = "0o_123_456_777"
|
|
doAssert fromOct[int](s) == 21913087
|
|
doAssert fromOct[int8](s) == 0o377'i8
|
|
doAssert fromOct[int8](s) == -1'i8
|
|
doAssert fromOct[uint8](s) == 255'u8
|
|
doAssert s.fromOct[:int16] == 24063'i16
|
|
doAssert s.fromOct[:uint64] == 21913087'u64
|
|
result = T(0)
|
|
let p = parseutils.parseOct(s, result)
|
|
if p != s.len or p == 0:
|
|
raise newException(ValueError, "invalid oct integer: " & s)
|
|
|
|
func fromHex*[T: SomeInteger](s: string): T =
|
|
## Parses a hex integer value from a string `s`.
|
|
##
|
|
## If `s` is not a valid hex integer, `ValueError` is raised. `s` can have
|
|
## one of the following optional prefixes: `0x`, `0X`, `#`. Underscores within
|
|
## `s` are ignored.
|
|
##
|
|
## Does not check for overflow. If the value represented by `s`
|
|
## is too big to fit into a return type, only the value of the rightmost
|
|
## hex digits of `s` is returned without producing an error.
|
|
runnableExamples:
|
|
let s = "0x_1235_8df6"
|
|
doAssert fromHex[int](s) == 305499638
|
|
doAssert fromHex[int8](s) == 0xf6'i8
|
|
doAssert fromHex[int8](s) == -10'i8
|
|
doAssert fromHex[uint8](s) == 246'u8
|
|
doAssert s.fromHex[:int16] == -29194'i16
|
|
doAssert s.fromHex[:uint64] == 305499638'u64
|
|
result = T(0)
|
|
let p = parseutils.parseHex(s, result)
|
|
if p != s.len or p == 0:
|
|
raise newException(ValueError, "invalid hex integer: " & s)
|
|
|
|
func intToStr*(x: int, minchars: Positive = 1): string {.rtl,
|
|
extern: "nsuIntToStr".} =
|
|
## Converts `x` to its decimal representation.
|
|
##
|
|
## The resulting string will be minimally `minchars` characters long. This is
|
|
## achieved by adding leading zeros.
|
|
runnableExamples:
|
|
doAssert intToStr(1984) == "1984"
|
|
doAssert intToStr(1984, 6) == "001984"
|
|
result = $abs(x)
|
|
for i in 1 .. minchars - len(result):
|
|
result = '0' & result
|
|
if x < 0:
|
|
result = '-' & result
|
|
|
|
func parseInt*(s: string): int {.rtl, extern: "nsuParseInt".} =
|
|
## Parses a decimal integer value contained in `s`.
|
|
##
|
|
## If `s` is not a valid integer, `ValueError` is raised.
|
|
runnableExamples:
|
|
doAssert parseInt("-0042") == -42
|
|
result = 0
|
|
let L = parseutils.parseInt(s, result, 0)
|
|
if L != s.len or L == 0:
|
|
raise newException(ValueError, "invalid integer: " & s)
|
|
|
|
func parseBiggestInt*(s: string): BiggestInt {.rtl,
|
|
extern: "nsuParseBiggestInt".} =
|
|
## Parses a decimal integer value contained in `s`.
|
|
##
|
|
## If `s` is not a valid integer, `ValueError` is raised.
|
|
result = BiggestInt(0)
|
|
let L = parseutils.parseBiggestInt(s, result, 0)
|
|
if L != s.len or L == 0:
|
|
raise newException(ValueError, "invalid integer: " & s)
|
|
|
|
func parseUInt*(s: string): uint {.rtl, extern: "nsuParseUInt".} =
|
|
## Parses a decimal unsigned integer value contained in `s`.
|
|
##
|
|
## If `s` is not a valid integer, `ValueError` is raised.
|
|
result = uint(0)
|
|
let L = parseutils.parseUInt(s, result, 0)
|
|
if L != s.len or L == 0:
|
|
raise newException(ValueError, "invalid unsigned integer: " & s)
|
|
|
|
func parseBiggestUInt*(s: string): BiggestUInt {.rtl,
|
|
extern: "nsuParseBiggestUInt".} =
|
|
## Parses a decimal unsigned integer value contained in `s`.
|
|
##
|
|
## If `s` is not a valid integer, `ValueError` is raised.
|
|
result = BiggestUInt(0)
|
|
let L = parseutils.parseBiggestUInt(s, result, 0)
|
|
if L != s.len or L == 0:
|
|
raise newException(ValueError, "invalid unsigned integer: " & s)
|
|
|
|
func parseFloat*(s: string): float {.rtl, extern: "nsuParseFloat".} =
|
|
## Parses a decimal floating point value contained in `s`.
|
|
##
|
|
## If `s` is not a valid floating point number, `ValueError` is raised.
|
|
##`NAN`, `INF`, `-INF` are also supported (case insensitive comparison).
|
|
runnableExamples:
|
|
doAssert parseFloat("3.14") == 3.14
|
|
doAssert parseFloat("inf") == 1.0/0
|
|
result = 0.0
|
|
let L = parseutils.parseFloat(s, result, 0)
|
|
if L != s.len or L == 0:
|
|
raise newException(ValueError, "invalid float: " & s)
|
|
|
|
func parseBinInt*(s: string): int {.rtl, extern: "nsuParseBinInt".} =
|
|
## Parses a binary integer value contained in `s`.
|
|
##
|
|
## If `s` is not a valid binary integer, `ValueError` is raised. `s` can have
|
|
## one of the following optional prefixes: `0b`, `0B`. Underscores within
|
|
## `s` are ignored.
|
|
runnableExamples:
|
|
let
|
|
a = "0b11_0101"
|
|
b = "111"
|
|
doAssert a.parseBinInt() == 53
|
|
doAssert b.parseBinInt() == 7
|
|
|
|
result = 0
|
|
let L = parseutils.parseBin(s, result, 0)
|
|
if L != s.len or L == 0:
|
|
raise newException(ValueError, "invalid binary integer: " & s)
|
|
|
|
func parseOctInt*(s: string): int {.rtl, extern: "nsuParseOctInt".} =
|
|
## Parses an octal integer value contained in `s`.
|
|
##
|
|
## If `s` is not a valid oct integer, `ValueError` is raised. `s` can have one
|
|
## of the following optional prefixes: `0o`, `0O`. Underscores within
|
|
## `s` are ignored.
|
|
result = 0
|
|
let L = parseutils.parseOct(s, result, 0)
|
|
if L != s.len or L == 0:
|
|
raise newException(ValueError, "invalid oct integer: " & s)
|
|
|
|
func parseHexInt*(s: string): int {.rtl, extern: "nsuParseHexInt".} =
|
|
## Parses a hexadecimal integer value contained in `s`.
|
|
##
|
|
## If `s` is not a valid hex integer, `ValueError` is raised. `s` can have one
|
|
## of the following optional prefixes: `0x`, `0X`, `#`. Underscores
|
|
## within `s` are ignored.
|
|
result = 0
|
|
let L = parseutils.parseHex(s, result, 0)
|
|
if L != s.len or L == 0:
|
|
raise newException(ValueError, "invalid hex integer: " & s)
|
|
|
|
func generateHexCharToValueMap(): string =
|
|
## Generates a string to map a hex digit to uint value.
|
|
result = ""
|
|
for inp in 0..255:
|
|
let ch = chr(inp)
|
|
let o =
|
|
case ch
|
|
of '0'..'9': inp - ord('0')
|
|
of 'a'..'f': inp - ord('a') + 10
|
|
of 'A'..'F': inp - ord('A') + 10
|
|
else: 17 # indicates an invalid hex char
|
|
result.add chr(o)
|
|
|
|
const hexCharToValueMap = generateHexCharToValueMap()
|
|
|
|
func parseHexStr*(s: string): string {.rtl, extern: "nsuParseHexStr".} =
|
|
## Converts hex-encoded string to byte string, e.g.:
|
|
##
|
|
## Raises `ValueError` for an invalid hex values. The comparison is
|
|
## case-insensitive.
|
|
##
|
|
## See also:
|
|
## * `toHex func<#toHex,string>`_ for the reverse operation
|
|
runnableExamples:
|
|
let
|
|
a = "41"
|
|
b = "3161"
|
|
c = "00ff"
|
|
doAssert parseHexStr(a) == "A"
|
|
doAssert parseHexStr(b) == "1a"
|
|
doAssert parseHexStr(c) == "\0\255"
|
|
|
|
if s.len mod 2 != 0:
|
|
raise newException(ValueError, "Incorrect hex string len")
|
|
result = newString(s.len div 2)
|
|
var buf = 0
|
|
for pos, c in s:
|
|
let val = hexCharToValueMap[ord(c)].ord
|
|
if val == 17:
|
|
raise newException(ValueError, "Invalid hex char `" &
|
|
c & "` (ord " & $c.ord & ")")
|
|
if pos mod 2 == 0:
|
|
buf = val
|
|
else:
|
|
result[pos div 2] = chr(val + buf shl 4)
|
|
|
|
func parseBool*(s: string): bool =
|
|
## Parses a value into a `bool`.
|
|
##
|
|
## If `s` is one of the following values: `y, yes, true, 1, on`, then
|
|
## returns `true`. If `s` is one of the following values: `n, no, false,
|
|
## 0, off`, then returns `false`. If `s` is something else a
|
|
## `ValueError` exception is raised.
|
|
runnableExamples:
|
|
let a = "n"
|
|
doAssert parseBool(a) == false
|
|
|
|
case normalize(s)
|
|
of "y", "yes", "true", "1", "on": result = true
|
|
of "n", "no", "false", "0", "off": result = false
|
|
else: raise newException(ValueError, "cannot interpret as a bool: " & s)
|
|
|
|
func parseEnum*[T: enum](s: string): T =
|
|
## Parses an enum `T`. This errors at compile time, if the given enum
|
|
## type contains multiple fields with the same string value.
|
|
##
|
|
## Raises `ValueError` for an invalid value in `s`. The comparison is
|
|
## done in a style insensitive way (first letter is still case-sensitive).
|
|
runnableExamples:
|
|
type
|
|
MyEnum = enum
|
|
first = "1st",
|
|
second,
|
|
third = "3rd"
|
|
|
|
doAssert parseEnum[MyEnum]("1_st") == first
|
|
doAssert parseEnum[MyEnum]("second") == second
|
|
doAssertRaises(ValueError):
|
|
echo parseEnum[MyEnum]("third")
|
|
|
|
genEnumCaseStmt(T, s, default = nil, ord(low(T)), ord(high(T)), nimIdentNormalize)
|
|
|
|
func parseEnum*[T: enum](s: string, default: T): T =
|
|
## Parses an enum `T`. This errors at compile time, if the given enum
|
|
## type contains multiple fields with the same string value.
|
|
##
|
|
## Uses `default` for an invalid value in `s`. The comparison is done in a
|
|
## style insensitive way (first letter is still case-sensitive).
|
|
runnableExamples:
|
|
type
|
|
MyEnum = enum
|
|
first = "1st",
|
|
second,
|
|
third = "3rd"
|
|
|
|
doAssert parseEnum[MyEnum]("1_st") == first
|
|
doAssert parseEnum[MyEnum]("second") == second
|
|
doAssert parseEnum[MyEnum]("last", third) == third
|
|
|
|
genEnumCaseStmt(T, s, default, ord(low(T)), ord(high(T)), nimIdentNormalize)
|
|
|
|
func repeat*(c: char, count: Natural): string {.rtl, extern: "nsuRepeatChar".} =
|
|
## Returns a string of length `count` consisting only of
|
|
## the character `c`.
|
|
runnableExamples:
|
|
let a = 'z'
|
|
doAssert a.repeat(5) == "zzzzz"
|
|
result = newString(count)
|
|
for i in 0..count-1: result[i] = c
|
|
|
|
func repeat*(s: string, n: Natural): string {.rtl, extern: "nsuRepeatStr".} =
|
|
## Returns string `s` concatenated `n` times.
|
|
runnableExamples:
|
|
doAssert "+ foo +".repeat(3) == "+ foo ++ foo ++ foo +"
|
|
|
|
result = newStringOfCap(n * s.len)
|
|
for i in 1..n: result.add(s)
|
|
|
|
func spaces*(n: Natural): string {.inline.} =
|
|
## Returns a string with `n` space characters. You can use this func
|
|
## to left align strings.
|
|
##
|
|
## See also:
|
|
## * `align func<#align,string,Natural,char>`_
|
|
## * `alignLeft func<#alignLeft,string,Natural,char>`_
|
|
## * `indent func<#indent,string,Natural,string>`_
|
|
## * `center func<#center,string,int,char>`_
|
|
runnableExamples:
|
|
let
|
|
width = 15
|
|
text1 = "Hello user!"
|
|
text2 = "This is a very long string"
|
|
doAssert text1 & spaces(max(0, width - text1.len)) & "|" ==
|
|
"Hello user! |"
|
|
doAssert text2 & spaces(max(0, width - text2.len)) & "|" ==
|
|
"This is a very long string|"
|
|
repeat(' ', n)
|
|
|
|
func align*(s: string, count: Natural, padding = ' '): string {.rtl,
|
|
extern: "nsuAlignString".} =
|
|
## Aligns a string `s` with `padding`, so that it is of length `count`.
|
|
##
|
|
## `padding` characters (by default spaces) are added before `s` resulting in
|
|
## right alignment. If `s.len >= count`, no spaces are added and `s` is
|
|
## returned unchanged. If you need to left align a string use the `alignLeft
|
|
## func<#alignLeft,string,Natural,char>`_.
|
|
##
|
|
## See also:
|
|
## * `alignLeft func<#alignLeft,string,Natural,char>`_
|
|
## * `spaces func<#spaces,Natural>`_
|
|
## * `indent func<#indent,string,Natural,string>`_
|
|
## * `center func<#center,string,int,char>`_
|
|
runnableExamples:
|
|
assert align("abc", 4) == " abc"
|
|
assert align("a", 0) == "a"
|
|
assert align("1232", 6) == " 1232"
|
|
assert align("1232", 6, '#') == "##1232"
|
|
if s.len < count:
|
|
result = newString(count)
|
|
let spaces = count - s.len
|
|
for i in 0..spaces-1: result[i] = padding
|
|
for i in spaces..count-1: result[i] = s[i-spaces]
|
|
else:
|
|
result = s
|
|
|
|
func alignLeft*(s: string, count: Natural, padding = ' '): string =
|
|
## Left-Aligns a string `s` with `padding`, so that it is of length `count`.
|
|
##
|
|
## `padding` characters (by default spaces) are added after `s` resulting in
|
|
## left alignment. If `s.len >= count`, no spaces are added and `s` is
|
|
## returned unchanged. If you need to right align a string use the `align
|
|
## func<#align,string,Natural,char>`_.
|
|
##
|
|
## See also:
|
|
## * `align func<#align,string,Natural,char>`_
|
|
## * `spaces func<#spaces,Natural>`_
|
|
## * `indent func<#indent,string,Natural,string>`_
|
|
## * `center func<#center,string,int,char>`_
|
|
runnableExamples:
|
|
assert alignLeft("abc", 4) == "abc "
|
|
assert alignLeft("a", 0) == "a"
|
|
assert alignLeft("1232", 6) == "1232 "
|
|
assert alignLeft("1232", 6, '#') == "1232##"
|
|
if s.len < count:
|
|
result = newString(count)
|
|
if s.len > 0:
|
|
result[0 .. (s.len - 1)] = s
|
|
for i in s.len ..< count:
|
|
result[i] = padding
|
|
else:
|
|
result = s
|
|
|
|
func center*(s: string, width: int, fillChar: char = ' '): string {.rtl,
|
|
extern: "nsuCenterString".} =
|
|
## Return the contents of `s` centered in a string `width` long using
|
|
## `fillChar` (default: space) as padding.
|
|
##
|
|
## The original string is returned if `width` is less than or equal
|
|
## to `s.len`.
|
|
##
|
|
## See also:
|
|
## * `align func<#align,string,Natural,char>`_
|
|
## * `alignLeft func<#alignLeft,string,Natural,char>`_
|
|
## * `spaces func<#spaces,Natural>`_
|
|
## * `indent func<#indent,string,Natural,string>`_
|
|
runnableExamples:
|
|
let a = "foo"
|
|
doAssert a.center(2) == "foo"
|
|
doAssert a.center(5) == " foo "
|
|
doAssert a.center(6) == " foo "
|
|
if width <= s.len: return s
|
|
result = newString(width)
|
|
# Left padding will be one fillChar
|
|
# smaller if there are an odd number
|
|
# of characters
|
|
let
|
|
charsLeft = (width - s.len)
|
|
leftPadding = charsLeft div 2
|
|
for i in 0 ..< width:
|
|
if i >= leftPadding and i < leftPadding + s.len:
|
|
# we are where the string should be located
|
|
result[i] = s[i-leftPadding]
|
|
else:
|
|
# we are either before or after where
|
|
# the string s should go
|
|
result[i] = fillChar
|
|
|
|
func indent*(s: string, count: Natural, padding: string = " "): string {.rtl,
|
|
extern: "nsuIndent".} =
|
|
## Indents each line in `s` by `count` amount of `padding`.
|
|
##
|
|
## **Note:** This does not preserve the new line characters used in `s`.
|
|
##
|
|
## See also:
|
|
## * `align func<#align,string,Natural,char>`_
|
|
## * `alignLeft func<#alignLeft,string,Natural,char>`_
|
|
## * `spaces func<#spaces,Natural>`_
|
|
## * `unindent func<#unindent,string,Natural,string>`_
|
|
## * `dedent func<#dedent,string,Natural>`_
|
|
runnableExamples:
|
|
doAssert indent("First line\c\l and second line.", 2) ==
|
|
" First line\l and second line."
|
|
result = ""
|
|
var i = 0
|
|
for line in s.splitLines():
|
|
if i != 0:
|
|
result.add("\n")
|
|
for j in 1..count:
|
|
result.add(padding)
|
|
result.add(line)
|
|
i.inc
|
|
|
|
func unindent*(s: string, count: Natural = int.high,
|
|
padding: string = " "): string {.rtl, extern: "nsuUnindent".} =
|
|
## Unindents each line in `s` by `count` amount of `padding`.
|
|
##
|
|
## **Note:** This does not preserve the new line characters used in `s`.
|
|
##
|
|
## See also:
|
|
## * `dedent func<#dedent,string,Natural>`_
|
|
## * `align func<#align,string,Natural,char>`_
|
|
## * `alignLeft func<#alignLeft,string,Natural,char>`_
|
|
## * `spaces func<#spaces,Natural>`_
|
|
## * `indent func<#indent,string,Natural,string>`_
|
|
runnableExamples:
|
|
let x = """
|
|
Hello
|
|
There
|
|
""".unindent()
|
|
|
|
doAssert x == "Hello\nThere\n"
|
|
result = ""
|
|
var i = 0
|
|
for line in s.splitLines():
|
|
if i != 0:
|
|
result.add("\n")
|
|
var indentCount = 0
|
|
for j in 0..<count.int:
|
|
indentCount.inc
|
|
if j + padding.len-1 >= line.len or line[j .. j + padding.len-1] != padding:
|
|
indentCount = j
|
|
break
|
|
result.add(line[indentCount*padding.len .. ^1])
|
|
i.inc
|
|
|
|
func indentation*(s: string): Natural {.since: (1, 3).} =
|
|
## Returns the amount of indentation all lines of `s` have in common,
|
|
## ignoring lines that consist only of whitespace.
|
|
result = int.high
|
|
for line in s.splitLines:
|
|
for i, c in line:
|
|
if i >= result: break
|
|
elif c != ' ':
|
|
result = i
|
|
break
|
|
if result == int.high:
|
|
result = 0
|
|
|
|
func dedent*(s: string, count: Natural = indentation(s)): string {.rtl,
|
|
extern: "nsuDedent", since: (1, 3).} =
|
|
## Unindents each line in `s` by `count` amount of `padding`.
|
|
## The only difference between this and the
|
|
## `unindent func<#unindent,string,Natural,string>`_ is that this by default
|
|
## only cuts off the amount of indentation that all lines of `s` share as
|
|
## opposed to all indentation. It only supports spaces as padding.
|
|
##
|
|
## **Note:** This does not preserve the new line characters used in `s`.
|
|
##
|
|
## See also:
|
|
## * `unindent func<#unindent,string,Natural,string>`_
|
|
## * `align func<#align,string,Natural,char>`_
|
|
## * `alignLeft func<#alignLeft,string,Natural,char>`_
|
|
## * `spaces func<#spaces,Natural>`_
|
|
## * `indent func<#indent,string,Natural,string>`_
|
|
runnableExamples:
|
|
let x = """
|
|
Hello
|
|
There
|
|
""".dedent()
|
|
|
|
doAssert x == "Hello\n There\n"
|
|
unindent(s, count, " ")
|
|
|
|
func delete*(s: var string, slice: Slice[int]) =
|
|
## Deletes the items `s[slice]`, raising `IndexDefect` if the slice contains
|
|
## elements out of range.
|
|
##
|
|
## This operation moves all elements after `s[slice]` in linear time, and
|
|
## is the string analog to `sequtils.delete`.
|
|
runnableExamples:
|
|
var a = "abcde"
|
|
doAssertRaises(IndexDefect): a.delete(4..5)
|
|
assert a == "abcde"
|
|
a.delete(4..4)
|
|
assert a == "abcd"
|
|
a.delete(1..2)
|
|
assert a == "ad"
|
|
a.delete(1..<1) # empty slice
|
|
assert a == "ad"
|
|
when compileOption("boundChecks"):
|
|
if not (slice.a < s.len and slice.a >= 0 and slice.b < s.len):
|
|
raise newException(IndexDefect, $(slice: slice, len: s.len))
|
|
if slice.b >= slice.a:
|
|
var i = slice.a
|
|
var j = slice.b + 1
|
|
var newLen = s.len - j + i
|
|
# if j < s.len: moveMem(addr s[i], addr s[j], s.len - j) # pending benchmark
|
|
while i < newLen:
|
|
s[i] = s[j]
|
|
inc(i)
|
|
inc(j)
|
|
setLen(s, newLen)
|
|
|
|
func delete*(s: var string, first, last: int) {.rtl, extern: "nsuDelete",
|
|
deprecated: "use `delete(s, first..last)`".} =
|
|
## Deletes in `s` the characters at positions `first .. last` (both ends included).
|
|
runnableExamples("--warning:deprecated:off"):
|
|
var a = "abracadabra"
|
|
|
|
a.delete(4, 5)
|
|
doAssert a == "abradabra"
|
|
|
|
a.delete(1, 6)
|
|
doAssert a == "ara"
|
|
|
|
a.delete(2, 999)
|
|
doAssert a == "ar"
|
|
|
|
var i = first
|
|
var j = min(len(s), last+1)
|
|
var newLen = len(s)-j+i
|
|
while i < newLen:
|
|
s[i] = s[j]
|
|
inc(i)
|
|
inc(j)
|
|
setLen(s, newLen)
|
|
|
|
func startsWith*(s: string, prefix: char): bool {.inline.} =
|
|
## Returns true if `s` starts with character `prefix`.
|
|
##
|
|
## See also:
|
|
## * `endsWith func<#endsWith,string,char>`_
|
|
## * `continuesWith func<#continuesWith,string,string,Natural>`_
|
|
## * `removePrefix func<#removePrefix,string,char>`_
|
|
runnableExamples:
|
|
let a = "abracadabra"
|
|
doAssert a.startsWith('a') == true
|
|
doAssert a.startsWith('b') == false
|
|
result = s.len > 0 and s[0] == prefix
|
|
|
|
func startsWith*(s, prefix: string): bool {.rtl, extern: "nsuStartsWith".} =
|
|
## Returns true if `s` starts with string `prefix`.
|
|
##
|
|
## If `prefix == ""` true is returned.
|
|
##
|
|
## See also:
|
|
## * `endsWith func<#endsWith,string,string>`_
|
|
## * `continuesWith func<#continuesWith,string,string,Natural>`_
|
|
## * `removePrefix func<#removePrefix,string,string>`_
|
|
runnableExamples:
|
|
let a = "abracadabra"
|
|
doAssert a.startsWith("abra") == true
|
|
doAssert a.startsWith("bra") == false
|
|
result = false
|
|
startsWithImpl(s, prefix)
|
|
|
|
func endsWith*(s: string, suffix: char): bool {.inline.} =
|
|
## Returns true if `s` ends with `suffix`.
|
|
##
|
|
## See also:
|
|
## * `startsWith func<#startsWith,string,char>`_
|
|
## * `continuesWith func<#continuesWith,string,string,Natural>`_
|
|
## * `removeSuffix func<#removeSuffix,string,char>`_
|
|
runnableExamples:
|
|
let a = "abracadabra"
|
|
doAssert a.endsWith('a') == true
|
|
doAssert a.endsWith('b') == false
|
|
result = s.len > 0 and s[s.high] == suffix
|
|
|
|
func endsWith*(s, suffix: string): bool {.rtl, extern: "nsuEndsWith".} =
|
|
## Returns true if `s` ends with `suffix`.
|
|
##
|
|
## If `suffix == ""` true is returned.
|
|
##
|
|
## See also:
|
|
## * `startsWith func<#startsWith,string,string>`_
|
|
## * `continuesWith func<#continuesWith,string,string,Natural>`_
|
|
## * `removeSuffix func<#removeSuffix,string,string>`_
|
|
runnableExamples:
|
|
let a = "abracadabra"
|
|
doAssert a.endsWith("abra") == true
|
|
doAssert a.endsWith("dab") == false
|
|
result = false
|
|
endsWithImpl(s, suffix)
|
|
|
|
func continuesWith*(s, substr: string, start: Natural): bool {.rtl,
|
|
extern: "nsuContinuesWith".} =
|
|
## Returns true if `s` continues with `substr` at position `start`.
|
|
##
|
|
## If `substr == ""` true is returned.
|
|
##
|
|
## See also:
|
|
## * `startsWith func<#startsWith,string,string>`_
|
|
## * `endsWith func<#endsWith,string,string>`_
|
|
runnableExamples:
|
|
let a = "abracadabra"
|
|
doAssert a.continuesWith("ca", 4) == true
|
|
doAssert a.continuesWith("ca", 5) == false
|
|
doAssert a.continuesWith("dab", 6) == true
|
|
result = false
|
|
var i = 0
|
|
while true:
|
|
if i >= substr.len: return true
|
|
if i+start >= s.len or s[i+start] != substr[i]: return false
|
|
inc(i)
|
|
|
|
|
|
func 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).
|
|
##
|
|
## See also:
|
|
## * `removeSuffix func<#removeSuffix,string,set[char]>`_
|
|
runnableExamples:
|
|
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)
|
|
|
|
func removePrefix*(s: var string, c: char) {.rtl,
|
|
extern: "nsuRemovePrefixChar".} =
|
|
## Removes all occurrences of a single character (in-place) from the start
|
|
## of a string.
|
|
##
|
|
## See also:
|
|
## * `removeSuffix func<#removeSuffix,string,char>`_
|
|
## * `startsWith func<#startsWith,string,char>`_
|
|
runnableExamples:
|
|
var ident = "pControl"
|
|
ident.removePrefix('p')
|
|
doAssert ident == "Control"
|
|
removePrefix(s, chars = {c})
|
|
|
|
func removePrefix*(s: var string, prefix: string) {.rtl,
|
|
extern: "nsuRemovePrefixString".} =
|
|
## Remove the first matching prefix (in-place) from a string.
|
|
##
|
|
## See also:
|
|
## * `removeSuffix func<#removeSuffix,string,string>`_
|
|
## * `startsWith func<#startsWith,string,string>`_
|
|
runnableExamples:
|
|
var answers = "yesyes"
|
|
answers.removePrefix("yes")
|
|
doAssert answers == "yes"
|
|
if s.startsWith(prefix) and prefix.len > 0:
|
|
s.delete(0..prefix.len - 1)
|
|
|
|
func removeSuffix*(s: var string, chars: set[char] = Newlines) {.rtl,
|
|
extern: "nsuRemoveSuffixCharSet".} =
|
|
## Removes all characters from `chars` from the end of the string `s`
|
|
## (in-place).
|
|
##
|
|
## See also:
|
|
## * `removePrefix func<#removePrefix,string,set[char]>`_
|
|
runnableExamples:
|
|
var userInput = "Hello World!*~\r\n"
|
|
userInput.removeSuffix
|
|
doAssert userInput == "Hello World!*~"
|
|
userInput.removeSuffix({'~', '*'})
|
|
doAssert userInput == "Hello World!"
|
|
|
|
var otherInput = "Hello!?!"
|
|
otherInput.removeSuffix({'!', '?'})
|
|
doAssert otherInput == "Hello"
|
|
|
|
if s.len == 0: return
|
|
var last = s.high
|
|
while last > -1 and s[last] in chars: last -= 1
|
|
s.setLen(last + 1)
|
|
|
|
func removeSuffix*(s: var string, c: char) {.rtl,
|
|
extern: "nsuRemoveSuffixChar".} =
|
|
## Removes all occurrences of a single character (in-place) from the end
|
|
## of a string.
|
|
##
|
|
## See also:
|
|
## * `removePrefix func<#removePrefix,string,char>`_
|
|
## * `endsWith func<#endsWith,string,char>`_
|
|
runnableExamples:
|
|
var table = "users"
|
|
table.removeSuffix('s')
|
|
doAssert table == "user"
|
|
|
|
var dots = "Trailing dots......."
|
|
dots.removeSuffix('.')
|
|
doAssert dots == "Trailing dots"
|
|
|
|
removeSuffix(s, chars = {c})
|
|
|
|
func removeSuffix*(s: var string, suffix: string) {.rtl,
|
|
extern: "nsuRemoveSuffixString".} =
|
|
## Remove the first matching suffix (in-place) from a string.
|
|
##
|
|
## See also:
|
|
## * `removePrefix func<#removePrefix,string,string>`_
|
|
## * `endsWith func<#endsWith,string,string>`_
|
|
runnableExamples:
|
|
var answers = "yeses"
|
|
answers.removeSuffix("es")
|
|
doAssert answers == "yes"
|
|
var newLen = s.len
|
|
if s.endsWith(suffix):
|
|
newLen -= len(suffix)
|
|
s.setLen(newLen)
|
|
|
|
|
|
func addSep*(dest: var string, sep = ", ", startLen: Natural = 0) {.inline.} =
|
|
## Adds a separator to `dest` only if its length is bigger than `startLen`.
|
|
##
|
|
## A shorthand for:
|
|
##
|
|
## ```nim
|
|
## if dest.len > startLen: add(dest, sep)
|
|
## ```
|
|
##
|
|
## This is often useful for generating some code where the items need to
|
|
## be *separated* by `sep`. `sep` is only added if `dest` is longer than
|
|
## `startLen`. The following example creates a string describing
|
|
## an array of integers.
|
|
runnableExamples:
|
|
var arr = "["
|
|
for x in items([2, 3, 5, 7, 11]):
|
|
addSep(arr, startLen = len("["))
|
|
add(arr, $x)
|
|
add(arr, "]")
|
|
doAssert arr == "[2, 3, 5, 7, 11]"
|
|
|
|
if dest.len > startLen: add(dest, sep)
|
|
|
|
func allCharsInSet*(s: string, theSet: set[char]): bool =
|
|
## Returns true if every character of `s` is in the set `theSet`.
|
|
runnableExamples:
|
|
doAssert allCharsInSet("aeea", {'a', 'e'}) == true
|
|
doAssert allCharsInSet("", {'a', 'e'}) == true
|
|
|
|
for c in items(s):
|
|
if c notin theSet: return false
|
|
return true
|
|
|
|
func abbrev*(s: string, possibilities: openArray[string]): int =
|
|
## Returns the index of the first item in `possibilities` which starts
|
|
## with `s`, if not ambiguous.
|
|
##
|
|
## Returns -1 if no item has been found and -2 if multiple items match.
|
|
runnableExamples:
|
|
doAssert abbrev("fac", ["college", "faculty", "industry"]) == 1
|
|
doAssert abbrev("foo", ["college", "faculty", "industry"]) == -1 # Not found
|
|
doAssert abbrev("fac", ["college", "faculty", "faculties"]) == -2 # Ambiguous
|
|
doAssert abbrev("college", ["college", "colleges", "industry"]) == 0
|
|
|
|
result = -1 # none found
|
|
for i in 0..possibilities.len-1:
|
|
if possibilities[i].startsWith(s):
|
|
if possibilities[i] == s:
|
|
# special case: exact match shouldn't be ambiguous
|
|
return i
|
|
if result >= 0: return -2 # ambiguous
|
|
result = i
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
func join*(a: openArray[string], sep: string = ""): string {.rtl,
|
|
extern: "nsuJoinSep".} =
|
|
## Concatenates all strings in the container `a`, separating them with `sep`.
|
|
runnableExamples:
|
|
doAssert join(["A", "B", "Conclusion"], " -> ") == "A -> B -> Conclusion"
|
|
|
|
if len(a) > 0:
|
|
var L = sep.len * (a.len-1)
|
|
for i in 0..high(a): inc(L, a[i].len)
|
|
result = newStringOfCap(L)
|
|
add(result, a[0])
|
|
for i in 1..high(a):
|
|
add(result, sep)
|
|
add(result, a[i])
|
|
else:
|
|
result = ""
|
|
|
|
proc join*[T: not string](a: openArray[T], sep: string = ""): string =
|
|
## Converts all elements in the container `a` to strings using `$`,
|
|
## and concatenates them with `sep`.
|
|
runnableExamples:
|
|
doAssert join([1, 2, 3], " -> ") == "1 -> 2 -> 3"
|
|
|
|
result = ""
|
|
for i, x in a:
|
|
if i > 0:
|
|
add(result, sep)
|
|
add(result, $x)
|
|
|
|
type
|
|
SkipTable* = array[char, int] ## Character table for efficient substring search.
|
|
|
|
func initSkipTable*(a: var SkipTable, sub: string) {.rtl,
|
|
extern: "nsuInitSkipTable".} =
|
|
## Initializes table `a` for efficient search of substring `sub`.
|
|
##
|
|
## See also:
|
|
## * `initSkipTable func<#initSkipTable,string>`_
|
|
## * `find func<#find,SkipTable,string,string,Natural,int>`_
|
|
# TODO: this should be the `default()` initializer for the type.
|
|
let m = len(sub)
|
|
fill(a, m)
|
|
|
|
for i in 0 ..< m - 1:
|
|
a[sub[i]] = m - 1 - i
|
|
|
|
func initSkipTable*(sub: string): SkipTable {.noinit, rtl,
|
|
extern: "nsuInitNewSkipTable".} =
|
|
## Returns a new table initialized for `sub`.
|
|
##
|
|
## See also:
|
|
## * `initSkipTable func<#initSkipTable,SkipTable,string>`_
|
|
## * `find func<#find,SkipTable,string,string,Natural,int>`_
|
|
initSkipTable(result, sub)
|
|
|
|
func find*(a: SkipTable, s, sub: string, start: Natural = 0, last = -1): int {.
|
|
rtl, extern: "nsuFindStrA".} =
|
|
## Searches for `sub` in `s` inside range `start..last` using preprocessed
|
|
## table `a`. If `last` is unspecified, it defaults to `s.high` (the last
|
|
## element).
|
|
##
|
|
## Searching is case-sensitive. If `sub` is not in `s`, -1 is returned.
|
|
##
|
|
## See also:
|
|
## * `initSkipTable func<#initSkipTable,string>`_
|
|
## * `initSkipTable func<#initSkipTable,SkipTable,string>`_
|
|
let
|
|
last = if last < 0: s.high else: last
|
|
subLast = sub.len - 1
|
|
|
|
if subLast == -1:
|
|
# this was an empty needle string,
|
|
# we count this as match in the first possible position:
|
|
return start
|
|
|
|
# This is an implementation of the Boyer-Moore Horspool algorithms
|
|
# https://en.wikipedia.org/wiki/Boyer%E2%80%93Moore%E2%80%93Horspool_algorithm
|
|
result = -1
|
|
var skip = start
|
|
|
|
while last - skip >= subLast:
|
|
var i = subLast
|
|
while s[skip + i] == sub[i]:
|
|
if i == 0:
|
|
return skip
|
|
dec i
|
|
inc skip, a[s[skip + subLast]]
|
|
|
|
when not (defined(js) or defined(nimdoc) or defined(nimscript)):
|
|
from system/ansi_c import c_memchr
|
|
|
|
const hasCStringBuiltin = true
|
|
else:
|
|
const hasCStringBuiltin = false
|
|
|
|
func find*(s: string, sub: char, start: Natural = 0, last = -1): int {.rtl,
|
|
extern: "nsuFindChar".} =
|
|
## Searches for `sub` in `s` inside range `start..last` (both ends included).
|
|
## If `last` is unspecified or negative, it defaults to `s.high` (the last element).
|
|
##
|
|
## Searching is case-sensitive. If `sub` is not in `s`, -1 is returned.
|
|
## Otherwise the index returned is relative to `s[0]`, not `start`.
|
|
## Subtract `start` from the result for a `start`-origin index.
|
|
##
|
|
## See also:
|
|
## * `rfind func<#rfind,string,char,Natural,int>`_
|
|
## * `replace func<#replace,string,char,char>`_
|
|
result = -1
|
|
let last = if last < 0: s.high else: last
|
|
|
|
template findImpl =
|
|
for i in int(start)..last:
|
|
if s[i] == sub:
|
|
return i
|
|
|
|
when nimvm:
|
|
findImpl()
|
|
else:
|
|
when hasCStringBuiltin:
|
|
let length = last-start+1
|
|
if length > 0:
|
|
let found = c_memchr(s[start].unsafeAddr, cint(sub), cast[csize_t](length))
|
|
if not found.isNil:
|
|
return cast[int](found) -% cast[int](s.cstring)
|
|
else:
|
|
findImpl()
|
|
|
|
func find*(s: string, chars: set[char], start: Natural = 0, last = -1): int {.
|
|
rtl, extern: "nsuFindCharSet".} =
|
|
## Searches for `chars` in `s` inside range `start..last` (both ends included).
|
|
## If `last` is unspecified or negative, it defaults to `s.high` (the last element).
|
|
##
|
|
## If `s` contains none of the characters in `chars`, -1 is returned.
|
|
## Otherwise the index returned is relative to `s[0]`, not `start`.
|
|
## Subtract `start` from the result for a `start`-origin index.
|
|
##
|
|
## See also:
|
|
## * `rfind func<#rfind,string,set[char],Natural,int>`_
|
|
## * `multiReplace func<#multiReplace,string,varargs[]>`_
|
|
result = -1
|
|
let last = if last < 0: s.high else: last
|
|
for i in int(start)..last:
|
|
if s[i] in chars:
|
|
return i
|
|
|
|
when defined(linux):
|
|
proc memmem(haystack: pointer, haystacklen: csize_t,
|
|
needle: pointer, needlelen: csize_t): pointer {.importc, header: """#ifndef _GNU_SOURCE
|
|
#define _GNU_SOURCE
|
|
#endif
|
|
#include <string.h>""".}
|
|
elif defined(bsd) or (defined(macosx) and not defined(ios)):
|
|
proc memmem(haystack: pointer, haystacklen: csize_t,
|
|
needle: pointer, needlelen: csize_t): pointer {.importc, header: "#include <string.h>".}
|
|
|
|
func find*(s, sub: string, start: Natural = 0, last = -1): int {.rtl,
|
|
extern: "nsuFindStr".} =
|
|
## Searches for `sub` in `s` inside range `start..last` (both ends included).
|
|
## If `last` is unspecified or negative, it defaults to `s.high` (the last element).
|
|
##
|
|
## Searching is case-sensitive. If `sub` is not in `s`, -1 is returned.
|
|
## Otherwise the index returned is relative to `s[0]`, not `start`.
|
|
## Subtract `start` from the result for a `start`-origin index.
|
|
##
|
|
## See also:
|
|
## * `rfind func<#rfind,string,string,Natural,int>`_
|
|
## * `replace func<#replace,string,string,string>`_
|
|
if sub.len > s.len - start: return -1
|
|
if sub.len == 1: return find(s, sub[0], start, last)
|
|
|
|
template useSkipTable =
|
|
result = find(initSkipTable(sub), s, sub, start, last)
|
|
|
|
when nimvm:
|
|
useSkipTable()
|
|
else:
|
|
when declared(memmem):
|
|
let subLen = sub.len
|
|
if last < 0 and start < s.len and subLen != 0:
|
|
let found = memmem(s[start].unsafeAddr, csize_t(s.len - start), sub.cstring, csize_t(subLen))
|
|
result = if not found.isNil:
|
|
cast[int](found) -% cast[int](s.cstring)
|
|
else:
|
|
-1
|
|
else:
|
|
useSkipTable()
|
|
else:
|
|
useSkipTable()
|
|
|
|
func rfind*(s: string, sub: char, start: Natural = 0, last = -1): int {.rtl,
|
|
extern: "nsuRFindChar".} =
|
|
## Searches for `sub` in `s` inside range `start..last` (both ends included)
|
|
## in reverse -- starting at high indexes and moving lower to the first
|
|
## character or `start`. If `last` is unspecified, it defaults to `s.high`
|
|
## (the last element).
|
|
##
|
|
## Searching is case-sensitive. If `sub` is not in `s`, -1 is returned.
|
|
## Otherwise the index returned is relative to `s[0]`, not `start`.
|
|
## Subtract `start` from the result for a `start`-origin index.
|
|
##
|
|
## See also:
|
|
## * `find func<#find,string,char,Natural,int>`_
|
|
let last = if last == -1: s.high else: last
|
|
for i in countdown(last, start):
|
|
if sub == s[i]: return i
|
|
return -1
|
|
|
|
func rfind*(s: string, chars: set[char], start: Natural = 0, last = -1): int {.
|
|
rtl, extern: "nsuRFindCharSet".} =
|
|
## Searches for `chars` in `s` inside range `start..last` (both ends
|
|
## included) in reverse -- starting at high indexes and moving lower to the
|
|
## first character or `start`. If `last` is unspecified, it defaults to
|
|
## `s.high` (the last element).
|
|
##
|
|
## If `s` contains none of the characters in `chars`, -1 is returned.
|
|
## Otherwise the index returned is relative to `s[0]`, not `start`.
|
|
## Subtract `start` from the result for a `start`-origin index.
|
|
##
|
|
## See also:
|
|
## * `find func<#find,string,set[char],Natural,int>`_
|
|
let last = if last == -1: s.high else: last
|
|
for i in countdown(last, start):
|
|
if s[i] in chars: return i
|
|
return -1
|
|
|
|
func rfind*(s, sub: string, start: Natural = 0, last = -1): int {.rtl,
|
|
extern: "nsuRFindStr".} =
|
|
## Searches for `sub` in `s` inside range `start..last` (both ends included)
|
|
## included) in reverse -- starting at high indexes and moving lower to the
|
|
## first character or `start`. If `last` is unspecified, it defaults to
|
|
## `s.high` (the last element).
|
|
##
|
|
## Searching is case-sensitive. If `sub` is not in `s`, -1 is returned.
|
|
## Otherwise the index returned is relative to `s[0]`, not `start`.
|
|
## Subtract `start` from the result for a `start`-origin index.
|
|
##
|
|
## See also:
|
|
## * `find func<#find,string,string,Natural,int>`_
|
|
if sub.len == 0:
|
|
let rightIndex: Natural = if last < 0: s.len else: last
|
|
return max(start, rightIndex)
|
|
if sub.len > s.len - start:
|
|
return -1
|
|
let last = if last == -1: s.high else: last
|
|
result = 0
|
|
for i in countdown(last - sub.len + 1, start):
|
|
for j in 0..sub.len-1:
|
|
result = i
|
|
if sub[j] != s[i+j]:
|
|
result = -1
|
|
break
|
|
if result != -1: return
|
|
return -1
|
|
|
|
|
|
func count*(s: string, sub: char): int {.rtl, extern: "nsuCountChar".} =
|
|
## Counts the occurrences of the character `sub` in the string `s`.
|
|
##
|
|
## See also:
|
|
## * `countLines func<#countLines,string>`_
|
|
result = 0
|
|
for c in s:
|
|
if c == sub: inc result
|
|
|
|
func count*(s: string, subs: set[char]): int {.rtl,
|
|
extern: "nsuCountCharSet".} =
|
|
## Counts the occurrences of the group of character `subs` in the string `s`.
|
|
##
|
|
## See also:
|
|
## * `countLines func<#countLines,string>`_
|
|
doAssert card(subs) > 0
|
|
result = 0
|
|
for c in s:
|
|
if c in subs: inc result
|
|
|
|
func count*(s: string, sub: string, overlapping: bool = false): int {.rtl,
|
|
extern: "nsuCountString".} =
|
|
## Counts the occurrences of a substring `sub` in the string `s`.
|
|
## Overlapping occurrences of `sub` only count when `overlapping`
|
|
## is set to true (default: false).
|
|
##
|
|
## See also:
|
|
## * `countLines func<#countLines,string>`_
|
|
doAssert sub.len > 0
|
|
result = 0
|
|
var i = 0
|
|
while true:
|
|
i = s.find(sub, i)
|
|
if i < 0: break
|
|
if overlapping: inc i
|
|
else: i += sub.len
|
|
inc result
|
|
|
|
func countLines*(s: string): int {.rtl, extern: "nsuCountLines".} =
|
|
## Returns the number of lines in the string `s`.
|
|
##
|
|
## This is the same as `len(splitLines(s))`, but much more efficient
|
|
## because it doesn't modify the string creating temporary objects. Every
|
|
## `character literal <manual.html#lexical-analysis-character-literals>`_
|
|
## newline combination (CR, LF, CR-LF) is supported.
|
|
##
|
|
## In this context, a line is any string separated by a newline combination.
|
|
## A line can be an empty string.
|
|
##
|
|
## See also:
|
|
## * `splitLines func<#splitLines,string>`_
|
|
runnableExamples:
|
|
doAssert countLines("First line\l and second line.") == 2
|
|
result = 1
|
|
var i = 0
|
|
while i < s.len:
|
|
case s[i]
|
|
of '\c':
|
|
if i+1 < s.len and s[i+1] == '\l': inc i
|
|
inc result
|
|
of '\l': inc result
|
|
else: discard
|
|
inc i
|
|
|
|
|
|
func contains*(s, sub: string): bool =
|
|
## Same as `find(s, sub) >= 0`.
|
|
##
|
|
## See also:
|
|
## * `find func<#find,string,string,Natural,int>`_
|
|
return find(s, sub) >= 0
|
|
|
|
func contains*(s: string, chars: set[char]): bool =
|
|
## Same as `find(s, chars) >= 0`.
|
|
##
|
|
## See also:
|
|
## * `find func<#find,string,set[char],Natural,int>`_
|
|
return find(s, chars) >= 0
|
|
|
|
func replace*(s, sub: string, by = ""): string {.rtl,
|
|
extern: "nsuReplaceStr".} =
|
|
## Replaces every occurrence of the string `sub` in `s` with the string `by`.
|
|
##
|
|
## See also:
|
|
## * `find func<#find,string,string,Natural,int>`_
|
|
## * `replace func<#replace,string,char,char>`_ for replacing
|
|
## single characters
|
|
## * `replaceWord func<#replaceWord,string,string,string>`_
|
|
## * `multiReplace func<#multiReplace,string,varargs[]>`_ for substrings
|
|
## * `multiReplace func<#multiReplace,openArray[char],varargs[]>`_ for single characters
|
|
result = ""
|
|
let subLen = sub.len
|
|
if subLen == 0:
|
|
result = s
|
|
elif subLen == 1:
|
|
# when the pattern is a single char, we use a faster
|
|
# char-based search that doesn't need a skip table:
|
|
let c = sub[0]
|
|
let last = s.high
|
|
var i = 0
|
|
while true:
|
|
let j = find(s, c, i, last)
|
|
if j < 0: break
|
|
add result, substr(s, i, j - 1)
|
|
add result, by
|
|
i = j + subLen
|
|
# copy the rest:
|
|
add result, substr(s, i)
|
|
else:
|
|
var a = initSkipTable(sub)
|
|
let last = s.high
|
|
var i = 0
|
|
while true:
|
|
let j = find(a, s, sub, i, last)
|
|
if j < 0: break
|
|
add result, substr(s, i, j - 1)
|
|
add result, by
|
|
i = j + subLen
|
|
# copy the rest:
|
|
add result, substr(s, i)
|
|
|
|
func replace*(s: string, sub, by: char): string {.rtl,
|
|
extern: "nsuReplaceChar".} =
|
|
## Replaces every occurrence of the character `sub` in `s` with the character
|
|
## `by`.
|
|
##
|
|
## Optimized version of `replace <#replace,string,string,string>`_ for
|
|
## characters.
|
|
##
|
|
## See also:
|
|
## * `find func<#find,string,char,Natural,int>`_
|
|
## * `replaceWord func<#replaceWord,string,string,string>`_
|
|
## * `multiReplace func<#multiReplace,string,varargs[]>`_ for substrings
|
|
## * `multiReplace func<#multiReplace,openArray[char],varargs[]>`_ for single characters
|
|
result = newString(s.len)
|
|
var i = 0
|
|
while i < s.len:
|
|
if s[i] == sub: result[i] = by
|
|
else: result[i] = s[i]
|
|
inc(i)
|
|
|
|
func replaceWord*(s, sub: string, by = ""): string {.rtl,
|
|
extern: "nsuReplaceWord".} =
|
|
## Replaces every occurrence of the string `sub` in `s` with the string `by`.
|
|
##
|
|
## Each occurrence of `sub` has to be surrounded by word boundaries
|
|
## (comparable to `\b` in regular expressions), otherwise it is not
|
|
## replaced.
|
|
if sub.len == 0: return s
|
|
const wordChars = {'a'..'z', 'A'..'Z', '0'..'9', '_', '\128'..'\255'}
|
|
result = ""
|
|
var a = initSkipTable(sub)
|
|
var i = 0
|
|
let last = s.high
|
|
let sublen = sub.len
|
|
if sublen > 0:
|
|
while true:
|
|
var j = find(a, s, sub, i, last)
|
|
if j < 0: break
|
|
# word boundary?
|
|
if (j == 0 or s[j-1] notin wordChars) and
|
|
(j+sub.len >= s.len or s[j+sub.len] notin wordChars):
|
|
add result, substr(s, i, j - 1)
|
|
add result, by
|
|
i = j + sublen
|
|
else:
|
|
add result, substr(s, i, j)
|
|
i = j + 1
|
|
# copy the rest:
|
|
add result, substr(s, i)
|
|
|
|
func multiReplace*(s: string, replacements: varargs[(string, string)]): string =
|
|
## Same as `replace<#replace,string,string,string>`_, but specialized for
|
|
## doing multiple replacements in a single pass through the input string.
|
|
##
|
|
## `multiReplace` scans the input string from left to right and replaces the
|
|
## matching substrings in the same order as passed in the argument list.
|
|
##
|
|
## The implications of the order of scanning the string and matching the
|
|
## replacements:
|
|
## - In case of multiple matches at a given position, the earliest
|
|
## replacement is applied.
|
|
## - Overlaps are not handled. After performing a replacement, the scan
|
|
## continues from the character after the matched substring. If the
|
|
## resulting string then contains a possible match starting in a newly
|
|
## placed substring, the additional replacement is not performed.
|
|
##
|
|
## If the resulting string is not longer than the original input string,
|
|
## only a single memory allocation is required.
|
|
##
|
|
runnableExamples:
|
|
# Swapping occurrences of 'a' and 'b':
|
|
doAssert multireplace("abba", [("a", "b"), ("b", "a")]) == "baab"
|
|
|
|
# The second replacement ("ab") is matched and performed first, the scan then
|
|
# continues from 'c', so the "bc" replacement is never matched and thus skipped.
|
|
doAssert multireplace("abc", [("bc", "x"), ("ab", "_b")]) == "_bc"
|
|
result = newStringOfCap(s.len)
|
|
var i = 0
|
|
var fastChk: set[char] = {}
|
|
for sub, by in replacements.items:
|
|
if sub.len > 0:
|
|
# Include first character of all replacements
|
|
fastChk.incl sub[0]
|
|
while i < s.len:
|
|
block sIteration:
|
|
# Assume most chars in s are not candidates for any replacement operation
|
|
if s[i] in fastChk:
|
|
for sub, by in replacements.items:
|
|
if sub.len > 0 and s.continuesWith(sub, i):
|
|
add result, by
|
|
inc(i, sub.len)
|
|
break sIteration
|
|
# No matching replacement found
|
|
# copy current character from s
|
|
add result, s[i]
|
|
inc(i)
|
|
|
|
func multiReplace*(s: openArray[char]; replacements: varargs[(set[char], char)]): string {.noinit.} =
|
|
## Performs multiple character replacements in a single pass through the input.
|
|
##
|
|
## `multiReplace` scans the input `s` from left to right and replaces
|
|
## characters based on character sets, applying the first matching replacement
|
|
## at each position. Useful for sanitizing or transforming strings with
|
|
## predefined character mappings.
|
|
##
|
|
## The order of the `replacements` matters:
|
|
## - First matching replacement is applied
|
|
## - Subsequent replacements are not considered for the same character
|
|
##
|
|
## See also:
|
|
## - `multiReplace(s: string; replacements: varargs[(string, string)]) <#multiReplace,string,varargs[]>`_,
|
|
runnableExamples:
|
|
const WinSanitationRules = [
|
|
({'\0'..'\31'}, ' '),
|
|
({'"'}, '\''),
|
|
({'/', '\\', ':', '|'}, '-'),
|
|
({'*', '?', '<', '>'}, '_'),
|
|
]
|
|
# Sanitize a filename with Windows-incompatible characters
|
|
const file = "a/file:with?invalid*chars.txt"
|
|
doAssert file.multiReplace(WinSanitationRules) == "a-file-with_invalid_chars.txt"
|
|
result = newStringUninit(s.len)
|
|
for i in 0..<s.len:
|
|
var nextChar = s[i]
|
|
for subs, by in replacements.items:
|
|
if nextChar in subs:
|
|
nextChar = by
|
|
break
|
|
result[i] = nextChar
|
|
|
|
func insertSep*(s: string, sep = '_', digits = 3): string {.rtl,
|
|
extern: "nsuInsertSep".} =
|
|
## Inserts the separator `sep` after `digits` characters (default: 3)
|
|
## from right to left.
|
|
##
|
|
## Even though the algorithm works with any string `s`, it is only useful
|
|
## if `s` contains a number.
|
|
runnableExamples:
|
|
doAssert insertSep("1000000") == "1_000_000"
|
|
result = newStringOfCap(s.len)
|
|
let hasPrefix = isDigit(s[s.low]) == false
|
|
var idx: int = 0
|
|
if hasPrefix:
|
|
result.add s[s.low]
|
|
for i in (s.low + 1)..s.high:
|
|
idx = i
|
|
if not isDigit(s[i]):
|
|
result.add s[i]
|
|
else:
|
|
break
|
|
let partsLen = s.len - idx
|
|
var L = (partsLen-1) div digits + partsLen
|
|
result.setLen(L + idx)
|
|
var j = 0
|
|
dec(L)
|
|
for i in countdown(partsLen-1, 0):
|
|
if j == digits:
|
|
result[L + idx] = sep
|
|
dec(L)
|
|
j = 0
|
|
result[L + idx] = s[i + idx]
|
|
inc(j)
|
|
dec(L)
|
|
|
|
func escape*(s: string, prefix = "\"", suffix = "\""): string {.rtl,
|
|
extern: "nsuEscape".} =
|
|
## Escapes a string `s`.
|
|
##
|
|
## .. note:: The escaping scheme is different from
|
|
## `system.addEscapedChar`.
|
|
##
|
|
## * replaces `'\0'..'\31'` and `'\127'..'\255'` by `\xHH` where `HH` is its hexadecimal value
|
|
## * replaces ``\`` by `\\`
|
|
## * replaces `'` by `\'`
|
|
## * replaces `"` by `\"`
|
|
##
|
|
## The resulting string is prefixed with `prefix` and suffixed with `suffix`.
|
|
## Both may be empty strings.
|
|
##
|
|
## See also:
|
|
## * `addEscapedChar proc<system.html#addEscapedChar,string,char>`_
|
|
## * `unescape func<#unescape,string,string,string>`_ for the opposite
|
|
## operation
|
|
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)
|
|
add(result, suffix)
|
|
|
|
func unescape*(s: string, prefix = "\"", suffix = "\""): string {.rtl,
|
|
extern: "nsuUnescape".} =
|
|
## Unescapes a string `s`.
|
|
##
|
|
## This complements `escape func<#escape,string,string,string>`_
|
|
## as it performs the opposite operations.
|
|
##
|
|
## If `s` does not begin with `prefix` and end with `suffix` a
|
|
## ValueError exception will be raised.
|
|
result = newStringOfCap(s.len)
|
|
var i = prefix.len
|
|
if not s.startsWith(prefix):
|
|
raise newException(ValueError,
|
|
"String does not start with: " & prefix)
|
|
while true:
|
|
if i >= s.len-suffix.len: break
|
|
if s[i] == '\\':
|
|
if i+1 >= s.len:
|
|
result.add('\\')
|
|
break
|
|
case s[i+1]:
|
|
of 'x':
|
|
inc i, 2
|
|
var c = 0
|
|
i += parseutils.parseHex(s, c, i, maxLen = 2)
|
|
result.add(chr(c))
|
|
dec i, 2
|
|
of '\\':
|
|
result.add('\\')
|
|
of '\'':
|
|
result.add('\'')
|
|
of '\"':
|
|
result.add('\"')
|
|
else:
|
|
result.add("\\" & s[i+1])
|
|
inc(i, 2)
|
|
else:
|
|
result.add(s[i])
|
|
inc(i)
|
|
if not s.endsWith(suffix):
|
|
raise newException(ValueError,
|
|
"String does not end in: " & suffix)
|
|
|
|
func validIdentifier*(s: string): bool {.rtl, extern: "nsuValidIdentifier".} =
|
|
## Returns true if `s` is a valid identifier.
|
|
##
|
|
## A valid identifier starts with a character of the set `IdentStartChars`
|
|
## and is followed by any number of characters of the set `IdentChars`.
|
|
runnableExamples:
|
|
doAssert "abc_def08".validIdentifier
|
|
result = false
|
|
if s.len > 0 and s[0] in IdentStartChars:
|
|
for i in 1..s.len-1:
|
|
if s[i] notin IdentChars: return false
|
|
return true
|
|
|
|
|
|
# floating point formatting:
|
|
when not defined(js):
|
|
func c_snprintf(buf: cstring, n: csize_t, frmt: cstring): cint {.header: "<stdio.h>",
|
|
importc: "snprintf", varargs.}
|
|
|
|
type
|
|
FloatFormatMode* = enum
|
|
## The different modes of floating point formatting.
|
|
ffDefault, ## use the shorter floating point notation
|
|
ffDecimal, ## use decimal floating point notation
|
|
ffScientific ## use scientific notation (using `e` character)
|
|
|
|
func formatBiggestFloat*(f: BiggestFloat, format: FloatFormatMode = ffDefault,
|
|
precision: range[-1..32] = 16;
|
|
decimalSep = '.'): string {.rtl, extern: "nsu$1".} =
|
|
## Converts a floating point value `f` to a string.
|
|
##
|
|
## If `format == ffDecimal` then precision is the number of digits to
|
|
## be printed after the decimal point.
|
|
## If `format == ffScientific` then precision is the maximum number
|
|
## of significant digits to be printed.
|
|
## `precision`'s default value is the maximum number of meaningful digits
|
|
## after the decimal point for Nim's `biggestFloat` type.
|
|
##
|
|
## If `precision == -1`, it tries to format it nicely.
|
|
runnableExamples:
|
|
let x = 123.456
|
|
doAssert x.formatBiggestFloat() == "123.4560000000000"
|
|
doAssert x.formatBiggestFloat(ffDecimal, 4) == "123.4560"
|
|
doAssert x.formatBiggestFloat(ffScientific, 2) == "1.23e+02"
|
|
when nimvm:
|
|
discard "implemented in the vmops"
|
|
else:
|
|
when defined(js):
|
|
var precision = precision
|
|
if precision == -1:
|
|
# use the same default precision as c_snprintf
|
|
precision = 6
|
|
var res: cstring
|
|
case format
|
|
of ffDefault:
|
|
{.emit: "`res` = `f`.toString();".}
|
|
of ffDecimal:
|
|
{.emit: "`res` = `f`.toFixed(`precision`);".}
|
|
of ffScientific:
|
|
{.emit: "`res` = `f`.toExponential(`precision`);".}
|
|
result = $res
|
|
if 1.0 / f == -Inf:
|
|
# JavaScript removes the "-" from negative Zero, add it back here
|
|
result = "-" & $res
|
|
for i in 0 ..< result.len:
|
|
# Depending on the locale either dot or comma is produced,
|
|
# but nothing else is possible:
|
|
if result[i] in {'.', ','}: result[i] = decimalSep
|
|
else:
|
|
const floatFormatToChar: array[FloatFormatMode, char] = ['g', 'f', 'e']
|
|
var
|
|
frmtstr {.noinit.}: array[0..5, char]
|
|
buf {.noinit.}: array[0..2500, char]
|
|
L: cint
|
|
frmtstr[0] = '%'
|
|
if precision >= 0:
|
|
frmtstr[1] = '#'
|
|
frmtstr[2] = '.'
|
|
frmtstr[3] = '*'
|
|
frmtstr[4] = floatFormatToChar[format]
|
|
frmtstr[5] = '\0'
|
|
L = c_snprintf(cast[cstring](addr buf), csize_t(2501), cast[cstring](addr frmtstr), precision, f)
|
|
else:
|
|
frmtstr[1] = floatFormatToChar[format]
|
|
frmtstr[2] = '\0'
|
|
L = c_snprintf(cast[cstring](addr buf), csize_t(2501), cast[cstring](addr frmtstr), f)
|
|
result = newString(L)
|
|
for i in 0 ..< L:
|
|
# Depending on the locale either dot or comma is produced,
|
|
# but nothing else is possible:
|
|
if buf[i] in {'.', ','}: result[i] = decimalSep
|
|
else: result[i] = buf[i]
|
|
when defined(windows):
|
|
# VS pre 2015 violates the C standard: "The exponent always contains at
|
|
# least two digits, and only as many more digits as necessary to
|
|
# represent the exponent." [C11 §7.21.6.1]
|
|
# The following post-processing fixes this behavior.
|
|
if result.len > 4 and result[^4] == '+' and result[^3] == '0':
|
|
result[^3] = result[^2]
|
|
result[^2] = result[^1]
|
|
result.setLen(result.len - 1)
|
|
|
|
func formatFloat*(f: float, format: FloatFormatMode = ffDefault,
|
|
precision: range[-1..32] = 16; decimalSep = '.'): string {.
|
|
rtl, extern: "nsu$1".} =
|
|
## Converts a floating point value `f` to a string.
|
|
##
|
|
## If `format == ffDecimal` then precision is the number of digits to
|
|
## be printed after the decimal point.
|
|
## If `format == ffScientific` then precision is the maximum number
|
|
## of significant digits to be printed.
|
|
## `precision`'s default value is the maximum number of meaningful digits
|
|
## after the decimal point for Nim's `float` type.
|
|
##
|
|
## If `precision == -1`, it tries to format it nicely.
|
|
runnableExamples:
|
|
let x = 123.456
|
|
doAssert x.formatFloat() == "123.4560000000000"
|
|
doAssert x.formatFloat(ffDecimal, 4) == "123.4560"
|
|
doAssert x.formatFloat(ffScientific, 2) == "1.23e+02"
|
|
|
|
result = formatBiggestFloat(f, format, precision, decimalSep)
|
|
|
|
func trimZeros*(x: var string; decimalSep = '.') =
|
|
## Trim trailing zeros from a formatted floating point
|
|
## value `x` (must be declared as `var`).
|
|
##
|
|
## This modifies `x` itself, it does not return a copy.
|
|
runnableExamples:
|
|
var x = "123.456000000"
|
|
x.trimZeros()
|
|
doAssert x == "123.456"
|
|
|
|
let sPos = find(x, decimalSep)
|
|
if sPos >= 0:
|
|
var last = find(x, 'e', start = sPos)
|
|
last = if last >= 0: last - 1 else: high(x)
|
|
var pos = last
|
|
while pos >= 0 and x[pos] == '0': dec(pos)
|
|
if pos > sPos: inc(pos)
|
|
if last >= pos:
|
|
x.delete(pos..last)
|
|
|
|
type
|
|
BinaryPrefixMode* = enum ## The different names for binary prefixes.
|
|
bpIEC, # use the IEC/ISO standard prefixes such as kibi
|
|
bpColloquial # use the colloquial kilo, mega etc
|
|
|
|
func formatSize*(bytes: int64,
|
|
decimalSep = '.',
|
|
prefix = bpIEC,
|
|
includeSpace = false): string =
|
|
## Rounds and formats `bytes`.
|
|
##
|
|
## By default, uses the IEC/ISO standard binary prefixes, so 1024 will be
|
|
## formatted as 1KiB. Set prefix to `bpColloquial` to use the colloquial
|
|
## names from the SI standard (e.g. k for 1000 being reused as 1024).
|
|
##
|
|
## `includeSpace` can be set to true to include the (SI preferred) space
|
|
## between the number and the unit (e.g. 1 KiB).
|
|
##
|
|
## See also:
|
|
## * `strformat module<strformat.html>`_ for string interpolation and formatting
|
|
runnableExamples:
|
|
doAssert formatSize((1'i64 shl 31) + (300'i64 shl 20)) == "2.293GiB"
|
|
doAssert formatSize((2.234*1024*1024).int) == "2.233MiB"
|
|
doAssert formatSize(4096, includeSpace = true) == "4 KiB"
|
|
doAssert formatSize(4096, prefix = bpColloquial, includeSpace = true) == "4 kB"
|
|
doAssert formatSize(4096) == "4KiB"
|
|
doAssert formatSize(5_378_934, prefix = bpColloquial, decimalSep = ',') == "5,129MB"
|
|
|
|
# It doesn't needs Zi and larger units until we use int72 or larger ints.
|
|
const iecPrefixes = ["", "Ki", "Mi", "Gi", "Ti", "Pi", "Ei"]
|
|
const collPrefixes = ["", "k", "M", "G", "T", "P", "E"]
|
|
|
|
let lg2 = if bytes == 0:
|
|
0
|
|
else:
|
|
when hasWorkingInt64:
|
|
fastLog2(bytes)
|
|
else:
|
|
fastLog2(int32 bytes)
|
|
let matchedIndex = lg2 div 10
|
|
# Lower bits that are smaller than 0.001 when `bytes` is converted to a real number and added prefix, are discard.
|
|
# Then it is converted to float with round down.
|
|
let discardBits = (lg2 div 10 - 1) * 10
|
|
|
|
var prefixes: array[7, string]
|
|
if prefix == bpColloquial:
|
|
prefixes = collPrefixes
|
|
else:
|
|
prefixes = iecPrefixes
|
|
|
|
let fbytes = if lg2 < 10: bytes.float elif lg2 < 20: bytes.float / 1024.0 else: (bytes shr discardBits).float / 1024.0
|
|
result = formatFloat(fbytes, format = ffDecimal, precision = 3,
|
|
decimalSep = decimalSep)
|
|
result.trimZeros(decimalSep)
|
|
if includeSpace:
|
|
result &= " "
|
|
result &= prefixes[matchedIndex]
|
|
result &= "B"
|
|
|
|
func formatEng*(f: BiggestFloat,
|
|
precision: range[0..32] = 10,
|
|
trim: bool = true,
|
|
siPrefix: bool = false,
|
|
unit: string = "",
|
|
decimalSep = '.',
|
|
useUnitSpace = false): string =
|
|
## Converts a floating point value `f` to a string using engineering notation.
|
|
##
|
|
## Numbers in of the range -1000.0<f<1000.0 will be formatted without an
|
|
## exponent. Numbers outside of this range will be formatted as a
|
|
## significand in the range -1000.0<f<1000.0 and an exponent that will always
|
|
## be an integer multiple of 3, corresponding with the SI prefix scale k, M,
|
|
## G, T etc for numbers with an absolute value greater than 1 and m, μ, n, p
|
|
## etc for numbers with an absolute value less than 1.
|
|
##
|
|
## The default configuration (`trim=true` and `precision=10`) shows the
|
|
## **shortest** form that precisely (up to a maximum of 10 decimal places)
|
|
## displays the value. For example, 4.100000 will be displayed as 4.1 (which
|
|
## is mathematically identical) whereas 4.1000003 will be displayed as
|
|
## 4.1000003.
|
|
##
|
|
## If `trim` is set to true, trailing zeros will be removed; if false, the
|
|
## number of digits specified by `precision` will always be shown.
|
|
##
|
|
## `precision` can be used to set the number of digits to be shown after the
|
|
## decimal point or (if `trim` is true) the maximum number of digits to be
|
|
## shown.
|
|
##
|
|
## ```nim
|
|
## formatEng(0, 2, trim=false) == "0.00"
|
|
## formatEng(0, 2) == "0"
|
|
## formatEng(0.053, 0) == "53e-3"
|
|
## formatEng(52731234, 2) == "52.73e6"
|
|
## formatEng(-52731234, 2) == "-52.73e6"
|
|
## ```
|
|
##
|
|
## If `siPrefix` is set to true, the number will be displayed with the SI
|
|
## prefix corresponding to the exponent. For example 4100 will be displayed
|
|
## as "4.1 k" instead of "4.1e3". Note that `u` is used for micro- in place
|
|
## of the greek letter mu (μ) as per ISO 2955. Numbers with an absolute
|
|
## value outside of the range 1e-18<f<1000e18 (1a<f<1000E) will be displayed
|
|
## with an exponent rather than an SI prefix, regardless of whether
|
|
## `siPrefix` is true.
|
|
##
|
|
## If `useUnitSpace` is true, the provided unit will be appended to the string
|
|
## (with a space as required by the SI standard). This behaviour is slightly
|
|
## different to appending the unit to the result as the location of the space
|
|
## is altered depending on whether there is an exponent.
|
|
##
|
|
## ```nim
|
|
## formatEng(4100, siPrefix=true, unit="V") == "4.1 kV"
|
|
## formatEng(4.1, siPrefix=true, unit="V") == "4.1 V"
|
|
## formatEng(4.1, siPrefix=true) == "4.1" # Note lack of space
|
|
## formatEng(4100, siPrefix=true) == "4.1 k"
|
|
## formatEng(4.1, siPrefix=true, unit="") == "4.1 " # Space with unit=""
|
|
## formatEng(4100, siPrefix=true, unit="") == "4.1 k"
|
|
## formatEng(4100) == "4.1e3"
|
|
## formatEng(4100, unit="V") == "4.1e3 V"
|
|
## formatEng(4100, unit="", useUnitSpace=true) == "4.1e3 " # Space with useUnitSpace=true
|
|
## ```
|
|
##
|
|
## `decimalSep` is used as the decimal separator.
|
|
##
|
|
## See also:
|
|
## * `strformat module<strformat.html>`_ for string interpolation and formatting
|
|
var
|
|
absolute: BiggestFloat
|
|
significand: BiggestFloat
|
|
fexponent: BiggestFloat
|
|
exponent: int
|
|
splitResult: seq[string]
|
|
suffix: string = ""
|
|
func getPrefix(exp: int): char =
|
|
## Get the SI prefix for a given exponent
|
|
##
|
|
## Assumes exponent is a multiple of 3; returns ' ' if no prefix found
|
|
const siPrefixes = ['a', 'f', 'p', 'n', 'u', 'm', ' ', 'k', 'M', 'G', 'T',
|
|
'P', 'E']
|
|
var index: int = (exp div 3) + 6
|
|
result = ' '
|
|
if index in low(siPrefixes)..high(siPrefixes):
|
|
result = siPrefixes[index]
|
|
|
|
# Most of the work is done with the sign ignored, so get the absolute value
|
|
absolute = abs(f)
|
|
significand = f
|
|
|
|
if absolute == 0.0:
|
|
# Simple case: just format it and force the exponent to 0
|
|
exponent = 0
|
|
result = significand.formatBiggestFloat(ffDecimal, precision,
|
|
decimalSep = '.')
|
|
else:
|
|
# Find the best exponent that's a multiple of 3
|
|
fexponent = floor(log10(absolute))
|
|
fexponent = 3.0 * floor(fexponent / 3.0)
|
|
# Adjust the significand for the new exponent
|
|
significand /= pow(10.0, fexponent)
|
|
|
|
# Adjust the significand and check whether it has affected
|
|
# the exponent
|
|
absolute = abs(significand)
|
|
if absolute >= 1000.0:
|
|
significand *= 0.001
|
|
fexponent += 3
|
|
# Components of the result:
|
|
result = significand.formatBiggestFloat(ffDecimal, precision,
|
|
decimalSep = '.')
|
|
exponent = fexponent.int()
|
|
|
|
splitResult = result.split('.')
|
|
result = splitResult[0]
|
|
# result should have at most one decimal character
|
|
if splitResult.len() > 1:
|
|
# If trim is set, we get rid of trailing zeros. Don't use trimZeros here as
|
|
# we can be a bit more efficient through knowledge that there will never be
|
|
# an exponent in this part.
|
|
if trim:
|
|
while splitResult[1].endsWith("0"):
|
|
# Trim last character
|
|
splitResult[1].setLen(splitResult[1].len-1)
|
|
if splitResult[1].len() > 0:
|
|
result &= decimalSep & splitResult[1]
|
|
else:
|
|
result &= decimalSep & splitResult[1]
|
|
|
|
# Combine the results accordingly
|
|
if siPrefix and exponent != 0:
|
|
var p = getPrefix(exponent)
|
|
if p != ' ':
|
|
suffix = " " & p
|
|
exponent = 0 # Exponent replaced by SI prefix
|
|
if suffix == "" and useUnitSpace:
|
|
suffix = " "
|
|
suffix &= unit
|
|
if exponent != 0:
|
|
result &= "e" & $exponent
|
|
result &= suffix
|
|
|
|
func findNormalized(x: string, inArray: openArray[string]): int =
|
|
var i = 0
|
|
while i < high(inArray):
|
|
if cmpIgnoreStyle(x, inArray[i]) == 0: return i
|
|
inc(i, 2) # incrementing by 1 would probably lead to a
|
|
# security hole...
|
|
return -1
|
|
|
|
func invalidFormatString(formatstr: string) {.noinline.} =
|
|
raise newException(ValueError, "invalid format string: " & formatstr)
|
|
|
|
func addf*(s: var string, formatstr: string, a: varargs[string, `$`]) {.rtl,
|
|
extern: "nsuAddf".} =
|
|
## The same as `add(s, formatstr % a)`, but more efficient.
|
|
const PatternChars = {'a'..'z', 'A'..'Z', '0'..'9', '\128'..'\255', '_'}
|
|
var i = 0
|
|
var num = 0
|
|
while i < len(formatstr):
|
|
if formatstr[i] == '$' and i+1 < len(formatstr):
|
|
case formatstr[i+1]
|
|
of '#':
|
|
if num > a.high: invalidFormatString(formatstr)
|
|
add s, a[num]
|
|
inc i, 2
|
|
inc num
|
|
of '$':
|
|
add s, '$'
|
|
inc(i, 2)
|
|
of '1'..'9', '-':
|
|
var j = 0
|
|
inc(i) # skip $
|
|
var negative = formatstr[i] == '-'
|
|
if negative: inc i
|
|
while i < formatstr.len and formatstr[i] in Digits:
|
|
j = j * 10 + ord(formatstr[i]) - ord('0')
|
|
inc(i)
|
|
let idx = if not negative: j-1 else: a.len-j
|
|
if idx < 0 or idx > a.high: invalidFormatString(formatstr)
|
|
add s, a[idx]
|
|
of '{':
|
|
var j = i+2
|
|
var k = 0
|
|
var negative = formatstr[j] == '-'
|
|
if negative: inc j
|
|
var isNumber = 0
|
|
while j < formatstr.len and formatstr[j] notin {'\0', '}'}:
|
|
if formatstr[j] in Digits:
|
|
k = k * 10 + ord(formatstr[j]) - ord('0')
|
|
if isNumber == 0: isNumber = 1
|
|
else:
|
|
isNumber = -1
|
|
inc(j)
|
|
if isNumber == 1:
|
|
let idx = if not negative: k-1 else: a.len-k
|
|
if idx < 0 or idx > a.high: invalidFormatString(formatstr)
|
|
add s, a[idx]
|
|
else:
|
|
var x = findNormalized(substr(formatstr, i+2, j-1), a)
|
|
if x >= 0 and x < high(a): add s, a[x+1]
|
|
else: invalidFormatString(formatstr)
|
|
i = j+1
|
|
of 'a'..'z', 'A'..'Z', '\128'..'\255', '_':
|
|
var j = i+1
|
|
while j < formatstr.len and formatstr[j] in PatternChars: inc(j)
|
|
var x = findNormalized(substr(formatstr, i+1, j-1), a)
|
|
if x >= 0 and x < high(a): add s, a[x+1]
|
|
else: invalidFormatString(formatstr)
|
|
i = j
|
|
else:
|
|
invalidFormatString(formatstr)
|
|
else:
|
|
add s, formatstr[i]
|
|
inc(i)
|
|
|
|
func `%`*(formatstr: string, a: openArray[string]): string {.rtl,
|
|
extern: "nsuFormatOpenArray".} =
|
|
## Interpolates a format string with the values from `a`.
|
|
##
|
|
## The `substitution`:idx: operator performs string substitutions in
|
|
## `formatstr` and returns a modified `formatstr`. This is often called
|
|
## `string interpolation`:idx:.
|
|
##
|
|
## This is best explained by an example:
|
|
##
|
|
## ```nim
|
|
## "$1 eats $2." % ["The cat", "fish"]
|
|
## ```
|
|
##
|
|
## Results in:
|
|
##
|
|
## ```nim
|
|
## "The cat eats fish."
|
|
## ```
|
|
##
|
|
## The substitution variables (the thing after the `$`) are enumerated
|
|
## from 1 to `a.len`.
|
|
## To produce a verbatim `$`, use `$$`.
|
|
## The notation `$#` can be used to refer to the next substitution
|
|
## variable:
|
|
##
|
|
## ```nim
|
|
## "$# eats $#." % ["The cat", "fish"]
|
|
## ```
|
|
##
|
|
## Substitution variables can also be words (that is
|
|
## `[A-Za-z_]+[A-Za-z0-9_]*`) in which case the arguments in `a` with even
|
|
## indices are keys and with odd indices are the corresponding values.
|
|
## An example:
|
|
##
|
|
## ```nim
|
|
## "$animal eats $food." % ["animal", "The cat", "food", "fish"]
|
|
## ```
|
|
##
|
|
## Results in:
|
|
##
|
|
## ```nim
|
|
## "The cat eats fish."
|
|
## ```
|
|
##
|
|
## The variables are compared with `cmpIgnoreStyle`. `ValueError` is
|
|
## raised if an ill-formed format string has been passed to the `%` operator.
|
|
##
|
|
## See also:
|
|
## * `strformat module<strformat.html>`_ for string interpolation and formatting
|
|
result = newStringOfCap(formatstr.len + a.len shl 4)
|
|
addf(result, formatstr, a)
|
|
|
|
func `%`*(formatstr, a: string): string {.rtl,
|
|
extern: "nsuFormatSingleElem".} =
|
|
## This is the same as `formatstr % [a]` (see
|
|
## `% func<#%25,string,openArray[string]>`_).
|
|
result = newStringOfCap(formatstr.len + a.len)
|
|
addf(result, formatstr, [a])
|
|
|
|
func format*(formatstr: string, a: varargs[string, `$`]): string {.rtl,
|
|
extern: "nsuFormatVarargs".} =
|
|
## This is the same as `formatstr % a` (see
|
|
## `% func<#%25,string,openArray[string]>`_) except that it supports
|
|
## auto stringification.
|
|
##
|
|
## See also:
|
|
## * `strformat module<strformat.html>`_ for string interpolation and formatting
|
|
result = newStringOfCap(formatstr.len + a.len)
|
|
addf(result, formatstr, a)
|
|
|
|
|
|
func strip*(s: string, leading = true, trailing = true,
|
|
chars: set[char] = Whitespace): string {.rtl, extern: "nsuStrip".} =
|
|
## Strips leading or trailing `chars` (default: whitespace characters)
|
|
## from `s` and returns the resulting string.
|
|
##
|
|
## If `leading` is true (default), leading `chars` are stripped.
|
|
## If `trailing` is true (default), trailing `chars` are stripped.
|
|
## If both are false, the string is returned unchanged.
|
|
##
|
|
## See also:
|
|
## * `strip proc<strbasics.html#strip,string,set[char]>`_ Inplace version.
|
|
## * `stripLineEnd func<#stripLineEnd,string>`_
|
|
runnableExamples:
|
|
let a = " vhellov "
|
|
let b = strip(a)
|
|
doAssert b == "vhellov"
|
|
|
|
doAssert a.strip(leading = false) == " vhellov"
|
|
doAssert a.strip(trailing = false) == "vhellov "
|
|
|
|
doAssert b.strip(chars = {'v'}) == "hello"
|
|
doAssert b.strip(leading = false, chars = {'v'}) == "vhello"
|
|
|
|
let c = "blaXbla"
|
|
doAssert c.strip(chars = {'b', 'a'}) == "laXbl"
|
|
doAssert c.strip(chars = {'b', 'a', 'l'}) == "X"
|
|
|
|
var
|
|
first = 0
|
|
last = len(s)-1
|
|
if leading:
|
|
while first <= last and s[first] in chars: inc(first)
|
|
if trailing:
|
|
while last >= first and s[last] in chars: dec(last)
|
|
result = substr(s, first, last)
|
|
|
|
func stripLineEnd*(s: var string) =
|
|
## Strips one of these suffixes from `s` in-place:
|
|
## `\r, \n, \r\n, \f, \v` (at most once instance).
|
|
## For example, can be useful in conjunction with `osproc.execCmdEx`.
|
|
## aka: `chomp`:idx:
|
|
runnableExamples:
|
|
var s = "foo\n\n"
|
|
s.stripLineEnd
|
|
doAssert s == "foo\n"
|
|
s = "foo\r\n"
|
|
s.stripLineEnd
|
|
doAssert s == "foo"
|
|
|
|
if s.len > 0:
|
|
case s[^1]
|
|
of '\n':
|
|
if s.len > 1 and s[^2] == '\r':
|
|
s.setLen s.len-2
|
|
else:
|
|
s.setLen s.len-1
|
|
of '\r', '\v', '\f':
|
|
s.setLen s.len-1
|
|
else:
|
|
discard
|
|
|
|
|
|
iterator tokenize*(s: string, seps: set[char] = Whitespace): tuple[
|
|
token: string, isSep: bool] =
|
|
## Tokenizes the string `s` into substrings.
|
|
##
|
|
## Substrings are separated by a substring containing only `seps`.
|
|
## Example:
|
|
##
|
|
## ```nim
|
|
## for word in tokenize(" this is an example "):
|
|
## writeLine(stdout, word)
|
|
## ```
|
|
##
|
|
## Results in:
|
|
##
|
|
## ```nim
|
|
## (" ", true)
|
|
## ("this", false)
|
|
## (" ", true)
|
|
## ("is", false)
|
|
## (" ", true)
|
|
## ("an", false)
|
|
## (" ", true)
|
|
## ("example", false)
|
|
## (" ", true)
|
|
## ```
|
|
var i = 0
|
|
while true:
|
|
var j = i
|
|
var isSep = j < s.len and s[j] in seps
|
|
while j < s.len and (s[j] in seps) == isSep: inc(j)
|
|
if j > i:
|
|
yield (substr(s, i, j-1), isSep)
|
|
else:
|
|
break
|
|
i = j
|
|
|
|
func isEmptyOrWhitespace*(s: string): bool {.rtl,
|
|
extern: "nsuIsEmptyOrWhitespace".} =
|
|
## Checks if `s` is empty or consists entirely of whitespace characters.
|
|
result = s.allCharsInSet(Whitespace)
|