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
parent cf13c5fba4
commit 70ec344bbf

View File

@@ -1262,21 +1262,20 @@ proc initSkipTable*(a: var SkipTable, sub: string)
{.noSideEffect, rtl, extern: "nsuInitSkipTable".} =
## Preprocess table `a` for `sub`.
let m = len(sub)
let m1 = m + 1
var i = 0
while i <= 0xff-7:
a[chr(i + 0)] = m1
a[chr(i + 1)] = m1
a[chr(i + 2)] = m1
a[chr(i + 3)] = m1
a[chr(i + 4)] = m1
a[chr(i + 5)] = m1
a[chr(i + 6)] = m1
a[chr(i + 7)] = m1
a[chr(i + 0)] = m
a[chr(i + 1)] = m
a[chr(i + 2)] = m
a[chr(i + 3)] = m
a[chr(i + 4)] = m
a[chr(i + 5)] = m
a[chr(i + 6)] = m
a[chr(i + 7)] = m
i += 8
for i in 0..m-1:
a[sub[i]] = m-i
for i in 0 ..< m - 1:
a[sub[i]] = m - 1 - i
proc find*(a: SkipTable, s, sub: string, start: Natural = 0, last: Natural = 0): int
{.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`.
##
## Searching is case-sensitive. If `sub` is not in `s`, -1 is returned.
let
last = if last==0: s.high else: last
m = len(sub)
n = last + 1
# search:
var j = start
while j <= n - m:
block match:
for k in 0..m-1:
if sub[k] != s[k+j]: break match
return j
inc(j, a[s[j+m]])
sLen = last - start + 1
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
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
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,
rtl, extern: "nsuReplaceStr".} =
## Replaces `sub` in `s` by the string `by`.
var a {.noinit.}: SkipTable
result = ""
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)
let subLen = sub.len
if subLen == 0:
for c in s:
add result, by
add result, c
add result, by
if sub.len == 0:
if i < s.len: add result, s[i]
i = j + 1
else:
i = j + sub.len
# copy the rest:
add result, substr(s, i)
return
elif subLen == 1:
# when the pattern is a single char, we use a faster
# char-based search that doesn't need a skip table:
var 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 {.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,
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
## (comparable to ``\\w`` in regular expressions), otherwise it is not
## replaced.
if sub.len == 0: return s
const wordChars = {'a'..'z', 'A'..'Z', '0'..'9', '_', '\128'..'\255'}
var a {.noinit.}: SkipTable
result = ""
@@ -2326,236 +2355,243 @@ proc removePrefix*(s: var string, prefix: string) {.
s.delete(0, prefix.len - 1)
when isMainModule:
doAssert align("abc", 4) == " abc"
doAssert align("a", 0) == "a"
doAssert align("1232", 6) == " 1232"
doAssert align("1232", 6, '#') == "##1232"
proc nonStaticTests =
doAssert formatBiggestFloat(1234.567, ffDecimal, -1) == "1234.567000"
doAssert formatBiggestFloat(1234.567, ffDecimal, 0) == "1235."
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 alignLeft("a", 0) == "a"
doAssert alignLeft("1232", 6) == "1232 "
doAssert alignLeft("1232", 6, '#') == "1232##"
doAssert "$# $3 $# $#" % ["a", "b", "c"] == "a c b c"
doAssert "${1}12 ${-1}$2" % ["a", "b"] == "a12 bb"
let
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
block: # formatSize tests
doAssert formatSize((1'i64 shl 31) + (300'i64 shl 20)) == "2.293GiB"
doAssert formatSize((2.234*1024*1024).int) == "2.234MiB"
doAssert formatSize(4096) == "4KiB"
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
longInp = """ThisIsOneVeryLongStringWhichWeWillSplitIntoEightSeparatePartsNow"""
longOutp = "ThisIsOn\neVeryLon\ngStringW\nhichWeWi\nllSplitI\nntoEight\nSeparate\nPartsNow"
doAssert wordWrap(longInp, 8, true) == longOutp
block: # formatEng tests
doAssert formatEng(0, 2, trim=false) == "0.00"
doAssert formatEng(0, 2) == "0"
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 formatBiggestFloat(1234.567, ffDecimal, 0) == "1235."
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 formatEng(4100, siPrefix=true, unit="V") == "4.1 kV"
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"
doAssert "$# $3 $# $#" % ["a", "b", "c"] == "a c b c"
doAssert "${1}12 ${-1}$2" % ["a", "b"] == "a12 bb"
proc staticTests =
doAssert align("abc", 4) == " abc"
doAssert align("a", 0) == "a"
doAssert align("1232", 6) == " 1232"
doAssert align("1232", 6, '#') == "##1232"
block: # formatSize tests
doAssert formatSize((1'i64 shl 31) + (300'i64 shl 20)) == "2.293GiB"
doAssert formatSize((2.234*1024*1024).int) == "2.234MiB"
doAssert formatSize(4096) == "4KiB"
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 alignLeft("abc", 4) == "abc "
doAssert alignLeft("a", 0) == "a"
doAssert alignLeft("1232", 6) == "1232 "
doAssert alignLeft("1232", 6, '#') == "1232##"
doAssert "$animal eats $food." % ["animal", "The cat", "food", "fish"] ==
"The cat eats fish."
let
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 "
doAssert "-lda-ldz -ld abc".replaceWord("-ld") == "-lda-ldz abc"
let
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 "oo".replace("", "abc") == "abcoabcoabc"
doAssert "$animal eats $food." % ["animal", "The cat", "food", "fish"] ==
"The cat eats fish."
type MyEnum = enum enA, enB, enC, enuD, enE
doAssert parseEnum[MyEnum]("enu_D") == enuD
doAssert "-ld a-ldz -ld".replaceWord("-ld") == " a-ldz "
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 "
doAssert center("foo", 0) == "foo"
doAssert center("foo", 3, fillChar = 'a') == "foo"
doAssert center("foo", 10, fillChar = '\t') == "\t\t\tfoo\t\t\t\t"
type MyEnum = enum enA, enB, enC, enuD, enE
doAssert parseEnum[MyEnum]("enu_D") == enuD
doAssert count("foofoofoo", "foofoo") == 1
doAssert count("foofoofoo", "foofoo", overlapping = true) == 2
doAssert count("foofoofoo", 'f') == 3
doAssert count("foofoofoobar", {'f','b'}) == 4
doAssert parseEnum("invalid enum value", enC) == enC
doAssert strip(" foofoofoo ") == "foofoofoo"
doAssert strip("sfoofoofoos", chars = {'s'}) == "foofoofoo"
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 center("foo", 13) == " foo "
doAssert center("foo", 0) == "foo"
doAssert center("foo", 3, fillChar = 'a') == "foo"
doAssert center("foo", 10, fillChar = '\t') == "\t\t\tfoo\t\t\t\t"
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 "Hello World.".multiReplace(("ello", "ELLO"), ("World.", "PEOPLE!")) == "HELLO PEOPLE!"
doAssert "aaaa".multiReplace(("a", "aa"), ("aa", "bb")) == "aaaaaaaa"
doAssert strip(" foofoofoo ") == "foofoofoo"
doAssert strip("sfoofoofoos", chars = {'s'}) == "foofoofoo"
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 isAlphaAscii('A')
doAssert(not isAlphaAscii('$'))
doAssert " foo\n bar".indent(4, "Q") == "QQQQ foo\nQQQQ bar"
doAssert isAlphaAscii("Rasp")
doAssert isAlphaAscii("Args")
doAssert(not isAlphaAscii("$Tomato"))
doAssert "abba".multiReplace(("a", "b"), ("b", "a")) == "baab"
doAssert "Hello World.".multiReplace(("ello", "ELLO"), ("World.", "PEOPLE!")) == "HELLO PEOPLE!"
doAssert "aaaa".multiReplace(("a", "aa"), ("aa", "bb")) == "aaaaaaaa"
doAssert isAlphaNumeric('3')
doAssert isAlphaNumeric('R')
doAssert(not isAlphaNumeric('!'))
doAssert isAlphaAscii('r')
doAssert isAlphaAscii('A')
doAssert(not isAlphaAscii('$'))
doAssert isAlphaNumeric("34ABc")
doAssert isAlphaNumeric("Rad")
doAssert isAlphaNumeric("1234")
doAssert(not isAlphaNumeric("@nose"))
doAssert isAlphaAscii("Rasp")
doAssert isAlphaAscii("Args")
doAssert(not isAlphaAscii("$Tomato"))
doAssert isDigit('3')
doAssert(not isDigit('a'))
doAssert(not isDigit('%'))
doAssert isAlphaNumeric('3')
doAssert isAlphaNumeric('R')
doAssert(not isAlphaNumeric('!'))
doAssert isDigit("12533")
doAssert(not isDigit("12.33"))
doAssert(not isDigit("A45b"))
doAssert isAlphaNumeric("34ABc")
doAssert isAlphaNumeric("Rad")
doAssert isAlphaNumeric("1234")
doAssert(not isAlphaNumeric("@nose"))
doAssert isSpaceAscii('\t')
doAssert isSpaceAscii('\l')
doAssert(not isSpaceAscii('A'))
doAssert isDigit('3')
doAssert(not isDigit('a'))
doAssert(not isDigit('%'))
doAssert isSpaceAscii("\t\l \v\r\f")
doAssert isSpaceAscii(" ")
doAssert(not isSpaceAscii("ABc \td"))
doAssert isDigit("12533")
doAssert(not isDigit("12.33"))
doAssert(not isDigit("A45b"))
doAssert(isNilOrWhitespace(""))
doAssert(isNilOrWhitespace(" "))
doAssert(isNilOrWhitespace("\t\l \v\r\f"))
doAssert(not isNilOrWhitespace("ABc \td"))
doAssert isSpaceAscii('\t')
doAssert isSpaceAscii('\l')
doAssert(not isSpaceAscii('A'))
doAssert isLowerAscii('a')
doAssert isLowerAscii('z')
doAssert(not isLowerAscii('A'))
doAssert(not isLowerAscii('5'))
doAssert(not isLowerAscii('&'))
doAssert isSpaceAscii("\t\l \v\r\f")
doAssert isSpaceAscii(" ")
doAssert(not isSpaceAscii("ABc \td"))
doAssert isLowerAscii("abcd")
doAssert(not isLowerAscii("abCD"))
doAssert(not isLowerAscii("33aa"))
doAssert(isNilOrWhitespace(""))
doAssert(isNilOrWhitespace(" "))
doAssert(isNilOrWhitespace("\t\l \v\r\f"))
doAssert(not isNilOrWhitespace("ABc \td"))
doAssert isUpperAscii('A')
doAssert(not isUpperAscii('b'))
doAssert(not isUpperAscii('5'))
doAssert(not isUpperAscii('%'))
doAssert isLowerAscii('a')
doAssert isLowerAscii('z')
doAssert(not isLowerAscii('A'))
doAssert(not isLowerAscii('5'))
doAssert(not isLowerAscii('&'))
doAssert isUpperAscii("ABC")
doAssert(not isUpperAscii("AAcc"))
doAssert(not isUpperAscii("A#$"))
doAssert isLowerAscii("abcd")
doAssert(not isLowerAscii("abCD"))
doAssert(not isLowerAscii("33aa"))
doAssert rsplit("foo bar", seps=Whitespace) == @["foo", "bar"]
doAssert rsplit(" foo bar", seps=Whitespace, maxsplit=1) == @[" foo", "bar"]
doAssert rsplit(" foo bar ", seps=Whitespace, maxsplit=1) == @[" foo bar", ""]
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 isUpperAscii('A')
doAssert(not isUpperAscii('b'))
doAssert(not isUpperAscii('5'))
doAssert(not isUpperAscii('%'))
doAssert(unescape(r"\x013", "", "") == "\x013")
doAssert isUpperAscii("ABC")
doAssert(not isUpperAscii("AAcc"))
doAssert(not isUpperAscii("A#$"))
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 rsplit("foo bar", seps=Whitespace) == @["foo", "bar"]
doAssert rsplit(" foo bar", seps=Whitespace, maxsplit=1) == @[" foo", "bar"]
doAssert rsplit(" foo bar ", seps=Whitespace, maxsplit=1) == @[" foo bar", ""]
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
~~!!baz""".unindent(2, "~~!!") == "foo\nbar\nbaz"
doAssert """~~!!foo
doAssert """~~!!foo
~~!!bar
~~!!baz""".unindent(2, "~~!!aa") == "~~!!foo\n~~!!bar\n~~!!baz"
doAssert """~~foo
doAssert """~~foo
~~ bar
~~ baz""".unindent(4, "~") == "foo\n bar\n baz"
doAssert """foo
doAssert """foo
bar
baz
""".unindent(4) == "foo\nbar\nbaz\n"
doAssert """foo
doAssert """foo
bar
baz
""".unindent(2) == "foo\n bar\n baz\n"
doAssert """foo
doAssert """foo
bar
baz
""".unindent(100) == "foo\nbar\nbaz\n"
doAssert """foo
doAssert """foo
foo
bar
""".unindent() == "foo\nfoo\nbar\n"
let s = " this is an example "
let s2 = ":this;is;an:example;;"
let s = " this is an example "
let s2 = ":this;is;an:example;;"
doAssert s.split() == @["", "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=1) == @["", "this is an example "]
doAssert s.split(" ", maxsplit=4) == @["", "this", "is", "an", "example "]
doAssert s.split() == @["", "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=1) == @["", "this is an example "]
doAssert s.split(" ", maxsplit=4) == @["", "this", "is", "an", "example "]
doAssert s.splitWhitespace() == @["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=3) == @["this", "is", "an", "example "]
doAssert s.splitWhitespace(maxsplit=4) == @["this", "is", "an", "example"]
doAssert s.splitWhitespace() == @["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=3) == @["this", "is", "an", "example "]
doAssert s.splitWhitespace(maxsplit=4) == @["this", "is", "an", "example"]
block: # formatEng tests
doAssert formatEng(0, 2, trim=false) == "0.00"
doAssert formatEng(0, 2) == "0"
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"
block: # startsWith / endsWith char tests
var s = "abcdef"
doAssert s.startsWith('a')
doAssert s.startsWith('b') == false
doAssert s.endsWith('f')
doAssert s.endsWith('a') == false
doAssert s.endsWith('\0') == false
doAssert formatEng(4100, siPrefix=true, unit="V") == "4.1 kV"
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"
#echo("strutils tests passed")
block: # startsWith / endsWith char tests
var s = "abcdef"
doAssert s.startsWith('a')
doAssert s.startsWith('b') == false
doAssert s.endsWith('f')
doAssert s.endsWith('a') == false
doAssert s.endsWith('\0') == false
nonStaticTests()
staticTests()
static: staticTests()
#echo("strutils tests passed")