mirror of
https://github.com/apple/pkl.git
synced 2026-08-26 21:54:03 +02:00
Add hex/binary/octal support to String.toInt() (#1808)
This commit is contained in:
@@ -572,19 +572,21 @@ public class AstBuilder extends AbstractAstBuilder<Object> {
|
||||
}
|
||||
}
|
||||
|
||||
private <T> T parseNumber(IntLiteralExpr expr, BiFunction<String, Integer, T> parser) {
|
||||
var text = remove_(expr.getNumber());
|
||||
|
||||
private <T> T parseInt(IntLiteralExpr expr, BiFunction<String, Integer, T> parser) {
|
||||
var text =
|
||||
VmUtils.removeUnderscoresFromNumber(expr.getNumber(), false).toLowerCase(Locale.ROOT);
|
||||
var radix = 10;
|
||||
if (text.startsWith("0x") || text.startsWith("0b") || text.startsWith("0o")) {
|
||||
if (text.length() >= 2 && text.charAt(0) == '0') {
|
||||
radix =
|
||||
switch (text.charAt(1)) {
|
||||
case 'x' -> 16;
|
||||
case 'b' -> 2;
|
||||
default -> 8;
|
||||
case 'x', 'X' -> 16;
|
||||
case 'b', 'B' -> 2;
|
||||
case 'o', 'O' -> 8;
|
||||
default -> 10;
|
||||
};
|
||||
|
||||
text = text.substring(2);
|
||||
if (radix != 10) {
|
||||
text = text.substring(2);
|
||||
}
|
||||
}
|
||||
|
||||
// relies on grammar rule nesting depth, but a breakage won't go unnoticed by tests
|
||||
@@ -600,7 +602,7 @@ public class AstBuilder extends AbstractAstBuilder<Object> {
|
||||
public IntLiteralNode visitIntLiteralExpr(IntLiteralExpr expr) {
|
||||
var section = createSourceSection(expr);
|
||||
try {
|
||||
var num = parseNumber(expr, Long::parseLong);
|
||||
var num = parseInt(expr, Long::parseLong);
|
||||
return new IntLiteralNode(section, num);
|
||||
} catch (NumberFormatException e) {
|
||||
var text = expr.getNumber();
|
||||
@@ -611,7 +613,7 @@ public class AstBuilder extends AbstractAstBuilder<Object> {
|
||||
@Override
|
||||
public FloatLiteralNode visitFloatLiteralExpr(FloatLiteralExpr expr) {
|
||||
var section = createSourceSection(expr);
|
||||
var text = remove_(expr.getNumber());
|
||||
var text = VmUtils.removeUnderscoresFromNumber(expr.getNumber(), true);
|
||||
// relies on grammar rule nesting depth, but a breakage won't go unnoticed by tests
|
||||
if (expr.parent() instanceof UnaryMinusExpr) {
|
||||
// handle negation here for consistency with visitIntegerLiteral
|
||||
@@ -627,16 +629,6 @@ public class AstBuilder extends AbstractAstBuilder<Object> {
|
||||
}
|
||||
}
|
||||
|
||||
private static String remove_(String number) {
|
||||
var builder = new StringBuilder(number.length());
|
||||
for (var i = 0; i < number.length(); i++) {
|
||||
var ch = number.charAt(i);
|
||||
if (ch == '_') continue;
|
||||
builder.append(ch);
|
||||
}
|
||||
return builder.toString();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ExpressionNode visitThrowExpr(ThrowExpr expr) {
|
||||
return ThrowNodeGen.create(createSourceSection(expr), visitExpr(expr.getExpr()));
|
||||
@@ -1314,7 +1306,7 @@ public class AstBuilder extends AbstractAstBuilder<Object> {
|
||||
var expr = args.get(i);
|
||||
if (expr instanceof IntLiteralExpr intLiteralExpr && isAllByteLiterals) {
|
||||
try {
|
||||
var byt = parseNumber(intLiteralExpr, Byte::parseByte);
|
||||
var byt = parseInt(intLiteralExpr, Byte::parseByte);
|
||||
expressionNodes[i] = new ByteConstantValueNode(byt);
|
||||
} catch (NumberFormatException e) {
|
||||
// proceed with initializing a constant value node; we'll throw an error inside
|
||||
|
||||
@@ -1092,4 +1092,25 @@ public final class VmUtils {
|
||||
var truffleStackTraceElements = TruffleStackTrace.getStackTrace(e);
|
||||
return truffleStackTraceElements != null && truffleStackTraceElements.size() < 100;
|
||||
}
|
||||
|
||||
/** Removes `_` from numbers to be parsed. Returns the string unmodified if it's invalid. */
|
||||
public static String removeUnderscoresFromNumber(String number, boolean allowExponents) {
|
||||
if (number.indexOf('_') < 0) return number;
|
||||
|
||||
var builder = new StringBuilder();
|
||||
var numberStart = true;
|
||||
for (var i = 0; i < number.length(); i++) {
|
||||
var c = number.charAt(i);
|
||||
if (c != '_') {
|
||||
builder.append(c);
|
||||
} else if (numberStart) {
|
||||
// invalid: _ at start or after [.eE]
|
||||
return number;
|
||||
}
|
||||
|
||||
numberStart = c == '.' || (allowExponents && (c == 'e' || c == 'E'));
|
||||
}
|
||||
|
||||
return builder.toString();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,6 +33,9 @@ import org.pkl.core.util.GlobResolver;
|
||||
import org.pkl.core.util.GlobResolver.InvalidGlobPatternException;
|
||||
import org.pkl.core.util.Pair;
|
||||
import org.pkl.core.util.StringUtils;
|
||||
import org.pkl.parser.Lexer;
|
||||
import org.pkl.parser.ParserError;
|
||||
import org.pkl.parser.Token;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public final class StringNodes {
|
||||
@@ -830,12 +833,57 @@ public final class StringNodes {
|
||||
}
|
||||
}
|
||||
|
||||
/** Use the lexer to parse integer values in the same forms that Pkl itself accepts */
|
||||
private static long toInt(String self) throws NumberFormatException {
|
||||
try {
|
||||
var lexer = new Lexer(self);
|
||||
var tk = lexer.next();
|
||||
var prefix = "";
|
||||
switch (tk) {
|
||||
case MINUS -> {
|
||||
prefix = "-";
|
||||
tk = lexer.next();
|
||||
}
|
||||
case PLUS -> tk = lexer.next();
|
||||
}
|
||||
|
||||
int radix;
|
||||
var text = lexer.text();
|
||||
switch (tk) {
|
||||
case INT -> radix = 10;
|
||||
case BIN -> {
|
||||
radix = 2;
|
||||
text = text.substring(2); // strip "0b"
|
||||
}
|
||||
case OCT -> {
|
||||
radix = 8;
|
||||
text = text.substring(2); // strip "0o"
|
||||
}
|
||||
case HEX -> {
|
||||
radix = 16;
|
||||
text = text.substring(2); // strip "0x"
|
||||
}
|
||||
default -> throw new NumberFormatException();
|
||||
}
|
||||
var parsed = Long.parseLong(prefix + VmUtils.removeUnderscoresFromNumber(text, false), radix);
|
||||
|
||||
// ensure no trailing garbage
|
||||
if (lexer.next() != Token.EOF) {
|
||||
throw new NumberFormatException();
|
||||
}
|
||||
|
||||
return parsed;
|
||||
} catch (ParserError ignored) {
|
||||
throw new NumberFormatException();
|
||||
}
|
||||
}
|
||||
|
||||
public abstract static class toInt extends ExternalMethod0Node {
|
||||
@TruffleBoundary
|
||||
@Specialization
|
||||
protected long eval(String self) {
|
||||
try {
|
||||
return Long.parseLong(removeUnderlinesFromNumber(self));
|
||||
return toInt(self);
|
||||
} catch (NumberFormatException e) {
|
||||
throw exceptionBuilder()
|
||||
.evalError("cannotParseStringAs", "Int")
|
||||
@@ -850,7 +898,7 @@ public final class StringNodes {
|
||||
@Specialization
|
||||
protected Object eval(String self) {
|
||||
try {
|
||||
return Long.parseLong(removeUnderlinesFromNumber(self));
|
||||
return toInt(self);
|
||||
} catch (NumberFormatException e) {
|
||||
return VmNull.withoutDefault();
|
||||
}
|
||||
@@ -862,7 +910,7 @@ public final class StringNodes {
|
||||
@Specialization
|
||||
protected double eval(String self) {
|
||||
try {
|
||||
return Double.parseDouble(removeUnderlinesFromNumber(self));
|
||||
return Double.parseDouble(VmUtils.removeUnderscoresFromNumber(self, true));
|
||||
} catch (NumberFormatException e) {
|
||||
throw exceptionBuilder()
|
||||
.evalError("cannotParseStringAs", "Float")
|
||||
@@ -877,7 +925,7 @@ public final class StringNodes {
|
||||
@Specialization
|
||||
protected Object eval(String self) {
|
||||
try {
|
||||
return Double.parseDouble(removeUnderlinesFromNumber(self));
|
||||
return Double.parseDouble(VmUtils.removeUnderscoresFromNumber(self, true));
|
||||
} catch (NumberFormatException e) {
|
||||
return VmNull.withoutDefault();
|
||||
}
|
||||
@@ -1031,23 +1079,4 @@ public final class StringNodes {
|
||||
var replacement = applyNode.executeString(mapper, regexMatch);
|
||||
return Matcher.quoteReplacement(replacement);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes `_` from numbers to be parsed to be compatible with how Pkl parses numbers. Will return
|
||||
* the string unmodified if it's invalid.
|
||||
*/
|
||||
private static String removeUnderlinesFromNumber(String number) {
|
||||
var builder = new StringBuilder();
|
||||
var numberStart = true;
|
||||
for (var i = 0; i < number.length(); i++) {
|
||||
var c = number.charAt(i);
|
||||
if (c != '_') {
|
||||
builder.append(c);
|
||||
} else if (numberStart) return number;
|
||||
|
||||
numberStart = c == '.' || c == 'e' || c == 'E';
|
||||
}
|
||||
|
||||
return builder.toString();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -263,18 +263,80 @@ examples {
|
||||
str1.codePoints
|
||||
}
|
||||
|
||||
["toInt()"] {
|
||||
["toInt() decimal"] {
|
||||
"123".toInt()
|
||||
"-123".toInt()
|
||||
"- 123".toInt()
|
||||
"+123".toInt()
|
||||
"+ 123".toInt()
|
||||
"1_2__3___".toInt()
|
||||
"-1_2__3___".toInt()
|
||||
"+1_2__3___".toInt()
|
||||
"0".toInt()
|
||||
"-0".toInt()
|
||||
"+0".toInt()
|
||||
"-9223372036854775808".toInt() // math.minInt
|
||||
"9223372036854775807".toInt() // math.maxInt
|
||||
}
|
||||
|
||||
["toInt() hex"] {
|
||||
"0x123".toInt()
|
||||
"0X123".toInt()
|
||||
"-0x123".toInt()
|
||||
"- 0x123".toInt()
|
||||
"+0x123".toInt()
|
||||
"+ 0x123".toInt()
|
||||
"0x1_2__3___".toInt()
|
||||
"-0x1_2__3___".toInt()
|
||||
"+0x1_2__3___".toInt()
|
||||
"0x0".toInt()
|
||||
"-0x0".toInt()
|
||||
"+0x0".toInt()
|
||||
"0x1E_2".toInt() // test underscore removal after 'E' succeeds for toInt (fails for toFloat)
|
||||
}
|
||||
|
||||
["toInt() binary"] {
|
||||
"0b101".toInt()
|
||||
"0B101".toInt()
|
||||
"-0b101".toInt()
|
||||
"- 0b101".toInt()
|
||||
"+0b101".toInt()
|
||||
"+ 0b101".toInt()
|
||||
"0b1_0__1___".toInt()
|
||||
"-0b1_0__1___".toInt()
|
||||
"+0b1_0__1___".toInt()
|
||||
"0b0".toInt()
|
||||
"-0b0".toInt()
|
||||
"+0b0".toInt()
|
||||
}
|
||||
|
||||
["toInt() octal"] {
|
||||
"0o123".toInt()
|
||||
"0O123".toInt()
|
||||
"-0o123".toInt()
|
||||
"- 0o123".toInt()
|
||||
"+0o123".toInt()
|
||||
"+ 0o123".toInt()
|
||||
"0o1_2__3___".toInt()
|
||||
"-0o1_2__3___".toInt()
|
||||
"+0o1_2__3___".toInt()
|
||||
"0o0".toInt()
|
||||
"-0o0".toInt()
|
||||
"+0o0".toInt()
|
||||
}
|
||||
|
||||
["toInt() error cases"] {
|
||||
module.catch(() -> "1.2".toInt())
|
||||
module.catch(() -> "9223372036854775808".toInt())
|
||||
module.catch(() -> "-9223372036854775809".toInt())
|
||||
module.catch(() -> "abc".toInt())
|
||||
module.catch(() -> "_1_000".toInt())
|
||||
module.catch(() -> "0p0".toInt())
|
||||
module.catch(() -> "0x-0".toInt())
|
||||
module.catch(() -> "0x+0".toInt())
|
||||
module.catch(() -> "0x0 + 0x1".toInt())
|
||||
module.catch(() -> "".toInt())
|
||||
module.catch(() -> "0o123e4".toInt())
|
||||
}
|
||||
|
||||
["toIntOrNull()"] {
|
||||
@@ -289,6 +351,11 @@ examples {
|
||||
"-9223372036854775809".toIntOrNull()
|
||||
"abc".toIntOrNull()
|
||||
"_1_2__3___".toIntOrNull()
|
||||
"0p0".toIntOrNull()
|
||||
"0x-0".toIntOrNull()
|
||||
"0x+0".toIntOrNull()
|
||||
"0x0 + 0x1".toIntOrNull()
|
||||
"".toIntOrNull()
|
||||
}
|
||||
|
||||
["toFloat()"] {
|
||||
|
||||
+32
-8
@@ -2255,9 +2255,15 @@ alias {
|
||||
displayUri = "https://github.com/apple/pkl/blob/$commitId/stdlib/base.pkl#LXXXX"
|
||||
}
|
||||
docComment = """
|
||||
Parses this string as a signed decimal (base 10) integer.
|
||||
Parses this string as a signed integer.
|
||||
|
||||
Throws if this string cannot be parsed as a signed decimal integer,
|
||||
Supports integer formats supported by Pkl:
|
||||
* Decimal (base 10)
|
||||
* Hexadecimal (base 16) with prefix `0x`
|
||||
* Binary (base 2) with prefix `0b`
|
||||
* Octal (base 8) with prefix `0o`
|
||||
|
||||
Throws if this string cannot be parsed,
|
||||
or if the integer is too large to fit into [Int].
|
||||
"""
|
||||
annotations = List()
|
||||
@@ -2272,9 +2278,15 @@ alias {
|
||||
displayUri = "https://github.com/apple/pkl/blob/$commitId/stdlib/base.pkl#LXXXX"
|
||||
}
|
||||
docComment = """
|
||||
Parses this string as a signed decimal (base 10) integer.
|
||||
Parses this string as a signed integer.
|
||||
|
||||
Returns [null] if this string cannot be parsed as a signed decimal integer,
|
||||
Supports integer formats supported by Pkl:
|
||||
* Decimal (base 10)
|
||||
* Hexadecimal (base 16) with prefix `0x`
|
||||
* Binary (base 2) with prefix `0b`
|
||||
* Octal (base 8) with prefix `0o`
|
||||
|
||||
Returns [null] if this string cannot be parsed,
|
||||
or if the integer is too large to fit into [Int].
|
||||
"""
|
||||
annotations = List()
|
||||
@@ -3192,9 +3204,15 @@ alias {
|
||||
displayUri = "https://github.com/apple/pkl/blob/$commitId/stdlib/base.pkl#LXXXX"
|
||||
}
|
||||
docComment = """
|
||||
Parses this string as a signed decimal (base 10) integer.
|
||||
Parses this string as a signed integer.
|
||||
|
||||
Throws if this string cannot be parsed as a signed decimal integer,
|
||||
Supports integer formats supported by Pkl:
|
||||
* Decimal (base 10)
|
||||
* Hexadecimal (base 16) with prefix `0x`
|
||||
* Binary (base 2) with prefix `0b`
|
||||
* Octal (base 8) with prefix `0o`
|
||||
|
||||
Throws if this string cannot be parsed,
|
||||
or if the integer is too large to fit into [Int].
|
||||
"""
|
||||
annotations = List()
|
||||
@@ -3209,9 +3227,15 @@ alias {
|
||||
displayUri = "https://github.com/apple/pkl/blob/$commitId/stdlib/base.pkl#LXXXX"
|
||||
}
|
||||
docComment = """
|
||||
Parses this string as a signed decimal (base 10) integer.
|
||||
Parses this string as a signed integer.
|
||||
|
||||
Returns [null] if this string cannot be parsed as a signed decimal integer,
|
||||
Supports integer formats supported by Pkl:
|
||||
* Decimal (base 10)
|
||||
* Hexadecimal (base 16) with prefix `0x`
|
||||
* Binary (base 2) with prefix `0b`
|
||||
* Octal (base 8) with prefix `0o`
|
||||
|
||||
Returns [null] if this string cannot be parsed,
|
||||
or if the integer is too large to fit into [Int].
|
||||
"""
|
||||
annotations = List()
|
||||
|
||||
@@ -213,18 +213,76 @@ examples {
|
||||
List()
|
||||
List(97, 98, 99, 100, 101, 102, 103)
|
||||
}
|
||||
["toInt()"] {
|
||||
["toInt() decimal"] {
|
||||
123
|
||||
-123
|
||||
-123
|
||||
123
|
||||
123
|
||||
123
|
||||
-123
|
||||
123
|
||||
-123
|
||||
0
|
||||
0
|
||||
0
|
||||
-9223372036854775808
|
||||
9223372036854775807
|
||||
}
|
||||
["toInt() hex"] {
|
||||
291
|
||||
291
|
||||
-291
|
||||
-291
|
||||
291
|
||||
291
|
||||
291
|
||||
-291
|
||||
291
|
||||
0
|
||||
0
|
||||
0
|
||||
482
|
||||
}
|
||||
["toInt() binary"] {
|
||||
5
|
||||
5
|
||||
-5
|
||||
-5
|
||||
5
|
||||
5
|
||||
5
|
||||
-5
|
||||
5
|
||||
0
|
||||
0
|
||||
0
|
||||
}
|
||||
["toInt() octal"] {
|
||||
83
|
||||
83
|
||||
-83
|
||||
-83
|
||||
83
|
||||
83
|
||||
83
|
||||
-83
|
||||
83
|
||||
0
|
||||
0
|
||||
0
|
||||
}
|
||||
["toInt() error cases"] {
|
||||
"Cannot parse string as `Int`. String: \"1.2\""
|
||||
"Cannot parse string as `Int`. String: \"9223372036854775808\""
|
||||
"Cannot parse string as `Int`. String: \"-9223372036854775809\""
|
||||
"Cannot parse string as `Int`. String: \"abc\""
|
||||
"Cannot parse string as `Int`. String: \"_1_000\""
|
||||
"Cannot parse string as `Int`. String: \"0p0\""
|
||||
"Cannot parse string as `Int`. String: \"0x-0\""
|
||||
"Cannot parse string as `Int`. String: \"0x+0\""
|
||||
"Cannot parse string as `Int`. String: \"0x0 + 0x1\""
|
||||
"Cannot parse string as `Int`. String: \"\""
|
||||
"Cannot parse string as `Int`. String: \"0o123e4\""
|
||||
}
|
||||
["toIntOrNull()"] {
|
||||
123
|
||||
@@ -238,6 +296,11 @@ examples {
|
||||
null
|
||||
null
|
||||
null
|
||||
null
|
||||
null
|
||||
null
|
||||
null
|
||||
null
|
||||
}
|
||||
["toFloat()"] {
|
||||
0.0
|
||||
|
||||
Reference in New Issue
Block a user