bugfix: strutils.find was broken for strings with uneven number of chars

For some reason, the problem was manifesting only inside the VM, it was
detecting an attempt to read past the string end (i.e. the formerly
accessible null byte).

To catch such errors, strutils now performs static tests too.

I've solved the problem by re-implementing the Boyer-Moore algotihm
in a cleaner way and I took the opportunity to make some other
optimisations to strutils.
This commit is contained in:
Zahary Karadjov 2018-05-04 19:56:03 +03:00 • committed by Andreas Rumpf
commit 70ec344bbf

View file

@ -1262,21 +1262,20 @@ proc initSkipTable*(a: var SkipTable, sub: string)
{.noSideEffect, rtl, extern: "nsuInitSkipTable".} = {.noSideEffect, rtl, extern: "nsuInitSkipTable".} =
## Preprocess table `a` for `sub`. ## Preprocess table `a` for `sub`.
let m = len(sub) let m = len(sub)
let m1 = m + 1
var i = 0 var i = 0
while i <= 0xff-7: while i <= 0xff-7:
a[chr(i + 0)] = m1 a[chr(i + 0)] = m
a[chr(i + 1)] = m1 a[chr(i + 1)] = m
a[chr(i + 2)] = m1 a[chr(i + 2)] = m
a[chr(i + 3)] = m1 a[chr(i + 3)] = m
a[chr(i + 4)] = m1 a[chr(i + 4)] = m
a[chr(i + 5)] = m1 a[chr(i + 5)] = m
a[chr(i + 6)] = m1 a[chr(i + 6)] = m
a[chr(i + 7)] = m1 a[chr(i + 7)] = m
i += 8 i += 8
for i in 0..m-1: for i in 0 ..< m - 1:
a[sub[i]] = m-i a[sub[i]] = m - 1 - i
proc find*(a: SkipTable, s, sub: string, start: Natural = 0, last: Natural = 0): int proc find*(a: SkipTable, s, sub: string, start: Natural = 0, last: Natural = 0): int
{.noSideEffect, rtl, extern: "nsuFindStrA".} = {.noSideEffect, rtl, extern: "nsuFindStrA".} =
@ -1284,18 +1283,29 @@ proc find*(a: SkipTable, s, sub: string, start: Natural = 0, last: Natural = 0):
## If `last` is unspecified, it defaults to `s.high`. ## If `last` is unspecified, it defaults to `s.high`.
## ##
## Searching is case-sensitive. If `sub` is not in `s`, -1 is returned. ## Searching is case-sensitive. If `sub` is not in `s`, -1 is returned.
let let
last = if last==0: s.high else: last last = if last==0: s.high else: last
m = len(sub) sLen = last - start + 1
n = last + 1 subLast = sub.len - 1
# search:
var j = start if subLast == -1:
while j <= n - m: # this was an empty needle string,
block match: # we count this as match in the first possible position:
for k in 0..m-1: return start
if sub[k] != s[k+j]: break match
return j # This is an implementation of the Boyer-Moore Horspool algorithms
inc(j, a[s[j+m]]) # https://en.wikipedia.org/wiki/Boyer%E2%80%93Moore%E2%80%93Horspool_algorithm
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]]
return -1 return -1
when not (defined(js) or defined(nimdoc) or defined(nimscript)): when not (defined(js) or defined(nimdoc) or defined(nimscript)):
@ -1455,23 +1465,41 @@ proc contains*(s: string, chars: set[char]): bool {.noSideEffect.} =
proc replace*(s, sub: string, by = ""): string {.noSideEffect, proc replace*(s, sub: string, by = ""): string {.noSideEffect,
rtl, extern: "nsuReplaceStr".} = rtl, extern: "nsuReplaceStr".} =
## Replaces `sub` in `s` by the string `by`. ## Replaces `sub` in `s` by the string `by`.
var a {.noinit.}: SkipTable
result = "" result = ""
initSkipTable(a, sub) let subLen = sub.len
let last = s.high if subLen == 0:
var i = 0 for c in s:
while true: add result, by
let j = find(a, s, sub, i, last) add result, c
if j < 0: break
add result, substr(s, i, j - 1)
add result, by add result, by
if sub.len == 0: return
if i < s.len: add result, s[i] elif subLen == 1:
i = j + 1 # when the pattern is a single char, we use a faster
else: # char-based search that doesn't need a skip table:
i = j + sub.len var c = sub[0]
# copy the rest: let last = s.high
add result, substr(s, i) 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 {.noinit.}: SkipTable
initSkipTable(a, 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)
proc replace*(s: string, sub, by: char): string {.noSideEffect, proc replace*(s: string, sub, by: char): string {.noSideEffect,
rtl, extern: "nsuReplaceChar".} = rtl, extern: "nsuReplaceChar".} =
@ -1492,6 +1520,7 @@ proc replaceWord*(s, sub: string, by = ""): string {.noSideEffect,
## Each occurrence of `sub` has to be surrounded by word boundaries ## Each occurrence of `sub` has to be surrounded by word boundaries
## (comparable to ``\\w`` in regular expressions), otherwise it is not ## (comparable to ``\\w`` in regular expressions), otherwise it is not
## replaced. ## replaced.
if sub.len == 0: return s
const wordChars = {'a'..'z', 'A'..'Z', '0'..'9', '_', '\128'..'\255'} const wordChars = {'a'..'z', 'A'..'Z', '0'..'9', '_', '\128'..'\255'}
var a {.noinit.}: SkipTable var a {.noinit.}: SkipTable
result = "" result = ""
@ -2326,236 +2355,243 @@ proc removePrefix*(s: var string, prefix: string) {.
s.delete(0, prefix.len - 1) s.delete(0, prefix.len - 1)
when isMainModule: when isMainModule:
doAssert align("abc", 4) == " abc" proc nonStaticTests =
doAssert align("a", 0) == "a" doAssert formatBiggestFloat(1234.567, ffDecimal, -1) == "1234.567000"
doAssert align("1232", 6) == " 1232" doAssert formatBiggestFloat(1234.567, ffDecimal, 0) == "1235."
doAssert align("1232", 6, '#') == "##1232" doAssert formatBiggestFloat(1234.567, ffDecimal, 1) == "1234.6"
doAssert formatBiggestFloat(0.00000000001, ffDecimal, 11) == "0.00000000001"
doAssert formatBiggestFloat(0.00000000001, ffScientific, 1, ',') in
["1,0e-11", "1,0e-011"]
# bug #6589
doAssert formatFloat(123.456, ffScientific, precision = -1) == "1.234560e+02"
doAssert alignLeft("abc", 4) == "abc " doAssert "$# $3 $# $#" % ["a", "b", "c"] == "a c b c"
doAssert alignLeft("a", 0) == "a" doAssert "${1}12 ${-1}$2" % ["a", "b"] == "a12 bb"
doAssert alignLeft("1232", 6) == "1232 "
doAssert alignLeft("1232", 6, '#') == "1232##"
let block: # formatSize tests
inp = """ this is a long text -- muchlongerthan10chars and here doAssert formatSize((1'i64 shl 31) + (300'i64 shl 20)) == "2.293GiB"
it goes""" doAssert formatSize((2.234*1024*1024).int) == "2.234MiB"
outp = " this is a\nlong text\n--\nmuchlongerthan10chars\nand here\nit goes" doAssert formatSize(4096) == "4KiB"
doAssert wordWrap(inp, 10, false) == outp doAssert formatSize(4096, prefix=bpColloquial, includeSpace=true) == "4 kB"
doAssert formatSize(4096, includeSpace=true) == "4 KiB"
doAssert formatSize(5_378_934, prefix=bpColloquial, decimalSep=',') == "5,13MB"
let block: # formatEng tests
longInp = """ThisIsOneVeryLongStringWhichWeWillSplitIntoEightSeparatePartsNow""" doAssert formatEng(0, 2, trim=false) == "0.00"
longOutp = "ThisIsOn\neVeryLon\ngStringW\nhichWeWi\nllSplitI\nntoEight\nSeparate\nPartsNow" doAssert formatEng(0, 2) == "0"
doAssert wordWrap(longInp, 8, true) == longOutp doAssert formatEng(53, 2, trim=false) == "53.00"
doAssert formatEng(0.053, 2, trim=false) == "53.00e-3"
doAssert formatEng(0.053, 4, trim=false) == "53.0000e-3"
doAssert formatEng(0.053, 4, trim=true) == "53e-3"
doAssert formatEng(0.053, 0) == "53e-3"
doAssert formatEng(52731234) == "52.731234e6"
doAssert formatEng(-52731234) == "-52.731234e6"
doAssert formatEng(52731234, 1) == "52.7e6"
doAssert formatEng(-52731234, 1) == "-52.7e6"
doAssert formatEng(52731234, 1, decimalSep=',') == "52,7e6"
doAssert formatEng(-52731234, 1, decimalSep=',') == "-52,7e6"
doAssert formatBiggestFloat(1234.567, ffDecimal, -1) == "1234.567000" doAssert formatEng(4100, siPrefix=true, unit="V") == "4.1 kV"
doAssert formatBiggestFloat(1234.567, ffDecimal, 0) == "1235." doAssert formatEng(4.1, siPrefix=true, unit="V", useUnitSpace=true) == "4.1 V"
doAssert formatBiggestFloat(1234.567, ffDecimal, 1) == "1234.6" doAssert formatEng(4.1, siPrefix=true) == "4.1" # Note lack of space
doAssert formatBiggestFloat(0.00000000001, ffDecimal, 11) == "0.00000000001" doAssert formatEng(4100, siPrefix=true) == "4.1 k"
doAssert formatBiggestFloat(0.00000000001, ffScientific, 1, ',') in doAssert formatEng(4.1, siPrefix=true, unit="", useUnitSpace=true) == "4.1 " # Includes space
["1,0e-11", "1,0e-011"] doAssert formatEng(4100, siPrefix=true, unit="") == "4.1 k"
# bug #6589 doAssert formatEng(4100) == "4.1e3"
doAssert formatFloat(123.456, ffScientific, precision = -1) == "1.234560e+02" doAssert formatEng(4100, unit="V", useUnitSpace=true) == "4.1e3 V"
doAssert formatEng(4100, unit="", useUnitSpace=true) == "4.1e3 "
# Don't use SI prefix as number is too big
doAssert formatEng(3.1e22, siPrefix=true, unit="a", useUnitSpace=true) == "31e21 a"
# Don't use SI prefix as number is too small
doAssert formatEng(3.1e-25, siPrefix=true, unit="A", useUnitSpace=true) == "310e-27 A"
doAssert "$# $3 $# $#" % ["a", "b", "c"] == "a c b c" proc staticTests =
doAssert "${1}12 ${-1}$2" % ["a", "b"] == "a12 bb" doAssert align("abc", 4) == " abc"
doAssert align("a", 0) == "a"
doAssert align("1232", 6) == " 1232"
doAssert align("1232", 6, '#') == "##1232"
block: # formatSize tests doAssert alignLeft("abc", 4) == "abc "
doAssert formatSize((1'i64 shl 31) + (300'i64 shl 20)) == "2.293GiB" doAssert alignLeft("a", 0) == "a"
doAssert formatSize((2.234*1024*1024).int) == "2.234MiB" doAssert alignLeft("1232", 6) == "1232 "
doAssert formatSize(4096) == "4KiB" doAssert alignLeft("1232", 6, '#') == "1232##"
doAssert formatSize(4096, prefix=bpColloquial, includeSpace=true) == "4 kB"
doAssert formatSize(4096, includeSpace=true) == "4 KiB"
doAssert formatSize(5_378_934, prefix=bpColloquial, decimalSep=',') == "5,13MB"
doAssert "$animal eats $food." % ["animal", "The cat", "food", "fish"] == let
"The cat eats fish." inp = """ this is a long text -- muchlongerthan10chars and here
it goes"""
outp = " this is a\nlong text\n--\nmuchlongerthan10chars\nand here\nit goes"
doAssert wordWrap(inp, 10, false) == outp
doAssert "-ld a-ldz -ld".replaceWord("-ld") == " a-ldz " let
doAssert "-lda-ldz -ld abc".replaceWord("-ld") == "-lda-ldz abc" longInp = """ThisIsOneVeryLongStringWhichWeWillSplitIntoEightSeparatePartsNow"""
longOutp = "ThisIsOn\neVeryLon\ngStringW\nhichWeWi\nllSplitI\nntoEight\nSeparate\nPartsNow"
doAssert wordWrap(longInp, 8, true) == longOutp
doAssert "-lda-ldz -ld abc".replaceWord("") == "lda-ldz ld abc" doAssert "$animal eats $food." % ["animal", "The cat", "food", "fish"] ==
doAssert "oo".replace("", "abc") == "abcoabcoabc" "The cat eats fish."
type MyEnum = enum enA, enB, enC, enuD, enE doAssert "-ld a-ldz -ld".replaceWord("-ld") == " a-ldz "
doAssert parseEnum[MyEnum]("enu_D") == enuD doAssert "-lda-ldz -ld abc".replaceWord("-ld") == "-lda-ldz abc"
doAssert parseEnum("invalid enum value", enC) == enC doAssert "-lda-ldz -ld abc".replaceWord("") == "-lda-ldz -ld abc"
doAssert "oo".replace("", "abc") == "abcoabcoabc"
doAssert center("foo", 13) == " foo " type MyEnum = enum enA, enB, enC, enuD, enE
doAssert center("foo", 0) == "foo" doAssert parseEnum[MyEnum]("enu_D") == enuD
doAssert center("foo", 3, fillChar = 'a') == "foo"
doAssert center("foo", 10, fillChar = '\t') == "\t\t\tfoo\t\t\t\t"
doAssert count("foofoofoo", "foofoo") == 1 doAssert parseEnum("invalid enum value", enC) == enC
doAssert count("foofoofoo", "foofoo", overlapping = true) == 2
doAssert count("foofoofoo", 'f') == 3
doAssert count("foofoofoobar", {'f','b'}) == 4
doAssert strip(" foofoofoo ") == "foofoofoo" doAssert center("foo", 13) == " foo "
doAssert strip("sfoofoofoos", chars = {'s'}) == "foofoofoo" doAssert center("foo", 0) == "foo"
doAssert strip("barfoofoofoobar", chars = {'b', 'a', 'r'}) == "foofoofoo" doAssert center("foo", 3, fillChar = 'a') == "foo"
doAssert strip("stripme but don't strip this stripme", doAssert center("foo", 10, fillChar = '\t') == "\t\t\tfoo\t\t\t\t"
chars = {'s', 't', 'r', 'i', 'p', 'm', 'e'}) ==
" but don't strip this "
doAssert strip("sfoofoofoos", leading = false, chars = {'s'}) == "sfoofoofoo"
doAssert strip("sfoofoofoos", trailing = false, chars = {'s'}) == "foofoofoos"
doAssert " foo\n bar".indent(4, "Q") == "QQQQ foo\nQQQQ bar" doAssert count("foofoofoo", "foofoo") == 1
doAssert count("foofoofoo", "foofoo", overlapping = true) == 2
doAssert count("foofoofoo", 'f') == 3
doAssert count("foofoofoobar", {'f','b'}) == 4
doAssert "abba".multiReplace(("a", "b"), ("b", "a")) == "baab" doAssert strip(" foofoofoo ") == "foofoofoo"
doAssert "Hello World.".multiReplace(("ello", "ELLO"), ("World.", "PEOPLE!")) == "HELLO PEOPLE!" doAssert strip("sfoofoofoos", chars = {'s'}) == "foofoofoo"
doAssert "aaaa".multiReplace(("a", "aa"), ("aa", "bb")) == "aaaaaaaa" doAssert strip("barfoofoofoobar", chars = {'b', 'a', 'r'}) == "foofoofoo"
doAssert strip("stripme but don't strip this stripme",
chars = {'s', 't', 'r', 'i', 'p', 'm', 'e'}) ==
" but don't strip this "
doAssert strip("sfoofoofoos", leading = false, chars = {'s'}) == "sfoofoofoo"
doAssert strip("sfoofoofoos", trailing = false, chars = {'s'}) == "foofoofoos"
doAssert isAlphaAscii('r') doAssert " foo\n bar".indent(4, "Q") == "QQQQ foo\nQQQQ bar"
doAssert isAlphaAscii('A')
doAssert(not isAlphaAscii('$'))
doAssert isAlphaAscii("Rasp") doAssert "abba".multiReplace(("a", "b"), ("b", "a")) == "baab"
doAssert isAlphaAscii("Args") doAssert "Hello World.".multiReplace(("ello", "ELLO"), ("World.", "PEOPLE!")) == "HELLO PEOPLE!"
doAssert(not isAlphaAscii("$Tomato")) doAssert "aaaa".multiReplace(("a", "aa"), ("aa", "bb")) == "aaaaaaaa"
doAssert isAlphaNumeric('3') doAssert isAlphaAscii('r')
doAssert isAlphaNumeric('R') doAssert isAlphaAscii('A')
doAssert(not isAlphaNumeric('!')) doAssert(not isAlphaAscii('$'))
doAssert isAlphaNumeric("34ABc") doAssert isAlphaAscii("Rasp")
doAssert isAlphaNumeric("Rad") doAssert isAlphaAscii("Args")
doAssert isAlphaNumeric("1234") doAssert(not isAlphaAscii("$Tomato"))
doAssert(not isAlphaNumeric("@nose"))
doAssert isDigit('3') doAssert isAlphaNumeric('3')
doAssert(not isDigit('a')) doAssert isAlphaNumeric('R')
doAssert(not isDigit('%')) doAssert(not isAlphaNumeric('!'))
doAssert isDigit("12533") doAssert isAlphaNumeric("34ABc")
doAssert(not isDigit("12.33")) doAssert isAlphaNumeric("Rad")
doAssert(not isDigit("A45b")) doAssert isAlphaNumeric("1234")
doAssert(not isAlphaNumeric("@nose"))
doAssert isSpaceAscii('\t') doAssert isDigit('3')
doAssert isSpaceAscii('\l') doAssert(not isDigit('a'))
doAssert(not isSpaceAscii('A')) doAssert(not isDigit('%'))
doAssert isSpaceAscii("\t\l \v\r\f") doAssert isDigit("12533")
doAssert isSpaceAscii(" ") doAssert(not isDigit("12.33"))
doAssert(not isSpaceAscii("ABc \td")) doAssert(not isDigit("A45b"))
doAssert(isNilOrWhitespace("")) doAssert isSpaceAscii('\t')
doAssert(isNilOrWhitespace(" ")) doAssert isSpaceAscii('\l')
doAssert(isNilOrWhitespace("\t\l \v\r\f")) doAssert(not isSpaceAscii('A'))
doAssert(not isNilOrWhitespace("ABc \td"))
doAssert isLowerAscii('a') doAssert isSpaceAscii("\t\l \v\r\f")
doAssert isLowerAscii('z') doAssert isSpaceAscii(" ")
doAssert(not isLowerAscii('A')) doAssert(not isSpaceAscii("ABc \td"))
doAssert(not isLowerAscii('5'))
doAssert(not isLowerAscii('&'))
doAssert isLowerAscii("abcd") doAssert(isNilOrWhitespace(""))
doAssert(not isLowerAscii("abCD")) doAssert(isNilOrWhitespace(" "))
doAssert(not isLowerAscii("33aa")) doAssert(isNilOrWhitespace("\t\l \v\r\f"))
doAssert(not isNilOrWhitespace("ABc \td"))
doAssert isUpperAscii('A') doAssert isLowerAscii('a')
doAssert(not isUpperAscii('b')) doAssert isLowerAscii('z')
doAssert(not isUpperAscii('5')) doAssert(not isLowerAscii('A'))
doAssert(not isUpperAscii('%')) doAssert(not isLowerAscii('5'))
doAssert(not isLowerAscii('&'))
doAssert isUpperAscii("ABC") doAssert isLowerAscii("abcd")
doAssert(not isUpperAscii("AAcc")) doAssert(not isLowerAscii("abCD"))
doAssert(not isUpperAscii("A#$")) doAssert(not isLowerAscii("33aa"))
doAssert rsplit("foo bar", seps=Whitespace) == @["foo", "bar"] doAssert isUpperAscii('A')
doAssert rsplit(" foo bar", seps=Whitespace, maxsplit=1) == @[" foo", "bar"] doAssert(not isUpperAscii('b'))
doAssert rsplit(" foo bar ", seps=Whitespace, maxsplit=1) == @[" foo bar", ""] doAssert(not isUpperAscii('5'))
doAssert rsplit(":foo:bar", sep=':') == @["", "foo", "bar"] doAssert(not isUpperAscii('%'))
doAssert rsplit(":foo:bar", sep=':', maxsplit=2) == @["", "foo", "bar"]
doAssert rsplit(":foo:bar", sep=':', maxsplit=3) == @["", "foo", "bar"]
doAssert rsplit("foothebar", sep="the") == @["foo", "bar"]
doAssert(unescape(r"\x013", "", "") == "\x013") doAssert isUpperAscii("ABC")
doAssert(not isUpperAscii("AAcc"))
doAssert(not isUpperAscii("A#$"))
doAssert join(["foo", "bar", "baz"]) == "foobarbaz" doAssert rsplit("foo bar", seps=Whitespace) == @["foo", "bar"]
doAssert join(@["foo", "bar", "baz"], ", ") == "foo, bar, baz" doAssert rsplit(" foo bar", seps=Whitespace, maxsplit=1) == @[" foo", "bar"]
doAssert join([1, 2, 3]) == "123" doAssert rsplit(" foo bar ", seps=Whitespace, maxsplit=1) == @[" foo bar", ""]
doAssert join(@[1, 2, 3], ", ") == "1, 2, 3" doAssert rsplit(":foo:bar", sep=':') == @["", "foo", "bar"]
doAssert rsplit(":foo:bar", sep=':', maxsplit=2) == @["", "foo", "bar"]
doAssert rsplit(":foo:bar", sep=':', maxsplit=3) == @["", "foo", "bar"]
doAssert rsplit("foothebar", sep="the") == @["foo", "bar"]
doAssert """~~!!foo doAssert(unescape(r"\x013", "", "") == "\x013")
doAssert join(["foo", "bar", "baz"]) == "foobarbaz"
doAssert join(@["foo", "bar", "baz"], ", ") == "foo, bar, baz"
doAssert join([1, 2, 3]) == "123"
doAssert join(@[1, 2, 3], ", ") == "1, 2, 3"
doAssert """~~!!foo
~~!!bar ~~!!bar
~~!!baz""".unindent(2, "~~!!") == "foo\nbar\nbaz" ~~!!baz""".unindent(2, "~~!!") == "foo\nbar\nbaz"
doAssert """~~!!foo doAssert """~~!!foo
~~!!bar ~~!!bar
~~!!baz""".unindent(2, "~~!!aa") == "~~!!foo\n~~!!bar\n~~!!baz" ~~!!baz""".unindent(2, "~~!!aa") == "~~!!foo\n~~!!bar\n~~!!baz"
doAssert """~~foo doAssert """~~foo
~~ bar ~~ bar
~~ baz""".unindent(4, "~") == "foo\n bar\n baz" ~~ baz""".unindent(4, "~") == "foo\n bar\n baz"
doAssert """foo doAssert """foo
bar bar
baz baz
""".unindent(4) == "foo\nbar\nbaz\n" """.unindent(4) == "foo\nbar\nbaz\n"
doAssert """foo doAssert """foo
bar bar
baz baz
""".unindent(2) == "foo\n bar\n baz\n" """.unindent(2) == "foo\n bar\n baz\n"
doAssert """foo doAssert """foo
bar bar
baz baz
""".unindent(100) == "foo\nbar\nbaz\n" """.unindent(100) == "foo\nbar\nbaz\n"
doAssert """foo doAssert """foo
foo foo
bar bar
""".unindent() == "foo\nfoo\nbar\n" """.unindent() == "foo\nfoo\nbar\n"
let s = " this is an example " let s = " this is an example "
let s2 = ":this;is;an:example;;" let s2 = ":this;is;an:example;;"
doAssert s.split() == @["", "this", "is", "an", "example", "", ""] doAssert s.split() == @["", "this", "is", "an", "example", "", ""]
doAssert s2.split(seps={':', ';'}) == @["", "this", "is", "an", "example", "", ""] doAssert s2.split(seps={':', ';'}) == @["", "this", "is", "an", "example", "", ""]
doAssert s.split(maxsplit=4) == @["", "this", "is", "an", "example "] doAssert s.split(maxsplit=4) == @["", "this", "is", "an", "example "]
doAssert s.split(' ', maxsplit=1) == @["", "this is an example "] doAssert s.split(' ', maxsplit=1) == @["", "this is an example "]
doAssert s.split(" ", maxsplit=4) == @["", "this", "is", "an", "example "] doAssert s.split(" ", maxsplit=4) == @["", "this", "is", "an", "example "]
doAssert s.splitWhitespace() == @["this", "is", "an", "example"] doAssert s.splitWhitespace() == @["this", "is", "an", "example"]
doAssert s.splitWhitespace(maxsplit=1) == @["this", "is an example "] doAssert s.splitWhitespace(maxsplit=1) == @["this", "is an example "]
doAssert s.splitWhitespace(maxsplit=2) == @["this", "is", "an example "] doAssert s.splitWhitespace(maxsplit=2) == @["this", "is", "an example "]
doAssert s.splitWhitespace(maxsplit=3) == @["this", "is", "an", "example "] doAssert s.splitWhitespace(maxsplit=3) == @["this", "is", "an", "example "]
doAssert s.splitWhitespace(maxsplit=4) == @["this", "is", "an", "example"] doAssert s.splitWhitespace(maxsplit=4) == @["this", "is", "an", "example"]
block: # formatEng tests block: # startsWith / endsWith char tests
doAssert formatEng(0, 2, trim=false) == "0.00" var s = "abcdef"
doAssert formatEng(0, 2) == "0" doAssert s.startsWith('a')
doAssert formatEng(53, 2, trim=false) == "53.00" doAssert s.startsWith('b') == false
doAssert formatEng(0.053, 2, trim=false) == "53.00e-3" doAssert s.endsWith('f')
doAssert formatEng(0.053, 4, trim=false) == "53.0000e-3" doAssert s.endsWith('a') == false
doAssert formatEng(0.053, 4, trim=true) == "53e-3" doAssert s.endsWith('\0') == false
doAssert formatEng(0.053, 0) == "53e-3"
doAssert formatEng(52731234) == "52.731234e6"
doAssert formatEng(-52731234) == "-52.731234e6"
doAssert formatEng(52731234, 1) == "52.7e6"
doAssert formatEng(-52731234, 1) == "-52.7e6"
doAssert formatEng(52731234, 1, decimalSep=',') == "52,7e6"
doAssert formatEng(-52731234, 1, decimalSep=',') == "-52,7e6"
doAssert formatEng(4100, siPrefix=true, unit="V") == "4.1 kV" #echo("strutils tests passed")
doAssert formatEng(4.1, siPrefix=true, unit="V", useUnitSpace=true) == "4.1 V"
doAssert formatEng(4.1, siPrefix=true) == "4.1" # Note lack of space
doAssert formatEng(4100, siPrefix=true) == "4.1 k"
doAssert formatEng(4.1, siPrefix=true, unit="", useUnitSpace=true) == "4.1 " # Includes space
doAssert formatEng(4100, siPrefix=true, unit="") == "4.1 k"
doAssert formatEng(4100) == "4.1e3"
doAssert formatEng(4100, unit="V", useUnitSpace=true) == "4.1e3 V"
doAssert formatEng(4100, unit="", useUnitSpace=true) == "4.1e3 "
# Don't use SI prefix as number is too big
doAssert formatEng(3.1e22, siPrefix=true, unit="a", useUnitSpace=true) == "31e21 a"
# Don't use SI prefix as number is too small
doAssert formatEng(3.1e-25, siPrefix=true, unit="A", useUnitSpace=true) == "310e-27 A"
block: # startsWith / endsWith char tests nonStaticTests()
var s = "abcdef" staticTests()
doAssert s.startsWith('a') static: staticTests()
doAssert s.startsWith('b') == false
doAssert s.endsWith('f')
doAssert s.endsWith('a') == false
doAssert s.endsWith('\0') == false
#echo("strutils tests passed")