diff --git a/ascii.libsonnet b/ascii.libsonnet index f7962d0..fabb867 100644 --- a/ascii.libsonnet +++ b/ascii.libsonnet @@ -28,8 +28,66 @@ local d = import 'github.com/jsonnet-libs/docsonnet/doc-util/main.libsonnet'; isNumber(c): std.isNumber(c) || (cp(c) >= 48 && cp(c) < 58), '#isStringNumeric':: d.fn( - '`isStringNumeric` reports whether string `s` consists only of numeric characters.', + '`isStringNumeric` reports whether string `s` is a number as defined by [JSON](https://www.json.org/json-en.html) but without the leading minus.', [d.arg('str', d.T.string)] ), - isStringNumeric(str): std.all(std.map(self.isNumber, std.stringChars(str))), + isStringNumeric(str): + // "1" "9" + local onenine(c) = (cp(c) >= 49 && cp(c) <= 57); + + // "0" + local digit(c) = (cp(c) == 48 || onenine(c)); + + local digits(str) = + std.length(str) > 0 + && std.all( + std.foldl( + function(acc, c) + acc + [digit(c)], + std.stringChars(str), + [], + ) + ); + + local fraction(str) = str == '' || (str[0] == '.' && digits(str[1:])); + + local sign(c) = (c == '-' || c == '+'); + + local exponent(str) = + str == '' + || (str[0] == 'E' && digits(str[1:])) + || (str[0] == 'e' && digits(str[1:])) + || (std.length(str) > 1 && str[0] == 'E' && sign(str[1]) && digits(str[2:])) + || (std.length(str) > 1 && str[0] == 'e' && sign(str[1]) && digits(str[2:])); + + + local integer(str) = + (std.length(str) == 1 && digit(str[0])) + || (std.length(str) > 0 && onenine(str[0]) && digits(str[1:])) + || (std.length(str) > 1 && str[0] == '-' && digit(str[1])) + || (std.length(str) > 1 && str[0] == '-' && onenine(str[1]) && digits(str[2:])); + + local expectInteger = + if std.member(str, '.') + then std.split(str, '.')[0] + else if std.member(str, 'e') + then std.split(str, 'e')[0] + else if std.member(str, 'E') + then std.split(str, 'E')[0] + else str; + + local expectFraction = + if std.member(str, 'e') + then std.split(str[std.length(expectInteger):], 'e')[0] + else if std.member(str, 'E') + then std.split(str[std.length(expectInteger):], 'E')[0] + else str[std.length(expectInteger):]; + + local expectExponent = str[std.length(expectInteger) + std.length(expectFraction):]; + + std.all([ + integer(expectInteger), + fraction(expectFraction), + exponent(expectExponent), + ]), } diff --git a/docs/ascii.md b/docs/ascii.md index 1ad7288..e0a2abd 100644 --- a/docs/ascii.md +++ b/docs/ascii.md @@ -41,7 +41,7 @@ isNumber(c) isStringNumeric(str) ``` -`isStringNumeric` reports whether string `s` consists only of numeric characters. +`isStringNumeric` reports whether string `s` is a number as defined by [JSON](https://www.json.org/json-en.html) but without the leading minus. ### fn isUpper diff --git a/test/ascii_test.jsonnet b/test/ascii_test.jsonnet index 077b350..d351bfb 100644 --- a/test/ascii_test.jsonnet +++ b/test/ascii_test.jsonnet @@ -39,6 +39,54 @@ test.new(std.thisFile) name='empty', test=test.expect.eq( actual=ascii.isStringNumeric(''), - expected=true, + expected=false, ) ) + ++ std.foldl( + function(acc, str) + acc + + test.case.new( + name='valid: ' + str, + test=test.expect.eq( + actual=ascii.isStringNumeric(str), + expected=true, + ) + ), + [ + '15', + '1.5', + '-1.5', + '1e5', + '1E5', + '1.5e5', + '1.5E5', + '1.5e-5', + '1.5E+5', + ], + {}, +) ++ std.foldl( + function(acc, str) + acc + + test.case.new( + name='invalid: ' + str, + test=test.expect.eq( + actual=ascii.isStringNumeric(str), + expected=false, + ) + ), + [ + '15e', + '1.', + '+', + '+1E5', + '.5', + 'E5', + 'e5', + '15e5garbage', + '1garbag5e5garbage', + 'garbage15e5garbage', + ], + {}, +)