Skip to content

Commit f084b43

Browse files
maskri17copybara-github
authored andcommitted
Implement strings.format in CEL string extensions.
PiperOrigin-RevId: 958621592
1 parent 989c8c5 commit f084b43

5 files changed

Lines changed: 505 additions & 7 deletions

File tree

conformance/src/test/java/dev/cel/conformance/BUILD.bazel

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -115,9 +115,6 @@ _TESTS_TO_SKIP_LEGACY = [
115115

116116
# Skip until fixed.
117117
"fields/qualified_identifier_resolution/map_value_repeat_key_heterogeneous",
118-
# TODO: Add strings.format.quote.
119-
"string_ext/format",
120-
"string_ext/format_errors",
121118

122119
# Future features for CEL 1.0
123120
# TODO: Strong typing support for enums, specified but not implemented.
@@ -143,10 +140,6 @@ _TESTS_TO_SKIP_LEGACY = [
143140
]
144141

145142
_TESTS_TO_SKIP_PLANNER = [
146-
# TODO: Add strings.format.
147-
"string_ext/format",
148-
"string_ext/format_errors",
149-
150143
# TODO: This is actually a user experience degradation.
151144
# Not worth fixing until we see a concrete need.
152145
"basic/functions/unbound_is_runtime_error",

extensions/src/main/java/dev/cel/extensions/BUILD.bazel

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,11 @@ java_library(
8989
"//checker:checker_builder",
9090
"//common:compiler_common",
9191
"//common/internal",
92+
"//common/internal:date_time_helpers",
9293
"//common/types",
94+
"//common/types:type_providers",
95+
"//common/values",
96+
"//common/values:cel_byte_string",
9397
"//compiler:compiler_builder",
9498
"//extensions:extension_library",
9599
"//runtime",

extensions/src/main/java/dev/cel/extensions/CelStringExtensions.java

Lines changed: 291 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,27 +17,42 @@
1717
import static com.google.common.collect.ImmutableSet.toImmutableSet;
1818
import static java.lang.Math.max;
1919
import static java.lang.Math.min;
20+
import static java.nio.charset.StandardCharsets.UTF_8;
2021

2122
import com.google.common.base.Ascii;
2223
import com.google.common.base.Joiner;
2324
import com.google.common.base.Splitter;
2425
import com.google.common.collect.ImmutableList;
2526
import com.google.common.collect.ImmutableSet;
27+
import com.google.common.primitives.UnsignedLong;
2628
import com.google.errorprone.annotations.Immutable;
2729
import dev.cel.checker.CelCheckerBuilder;
2830
import dev.cel.common.CelFunctionDecl;
2931
import dev.cel.common.CelOverloadDecl;
3032
import dev.cel.common.internal.CelCodePointArray;
33+
import dev.cel.common.internal.DateTimeHelpers;
34+
import dev.cel.common.types.CelType;
3135
import dev.cel.common.types.ListType;
3236
import dev.cel.common.types.SimpleType;
37+
import dev.cel.common.types.TypeType;
38+
import dev.cel.common.values.CelByteString;
39+
import dev.cel.common.values.NullValue;
3340
import dev.cel.compiler.CelCompilerLibrary;
3441
import dev.cel.runtime.CelEvaluationException;
3542
import dev.cel.runtime.CelEvaluationExceptionBuilder;
3643
import dev.cel.runtime.CelFunctionBinding;
3744
import dev.cel.runtime.CelRuntimeBuilder;
3845
import dev.cel.runtime.CelRuntimeLibrary;
46+
import java.math.BigDecimal;
47+
import java.math.RoundingMode;
48+
import java.time.Duration;
49+
import java.time.Instant;
50+
import java.util.HexFormat;
3951
import java.util.List;
52+
import java.util.Locale;
53+
import java.util.Map;
4054
import java.util.Set;
55+
import java.util.TreeMap;
4156

4257
/** Internal implementation of CEL string extensions. */
4358
@Immutable
@@ -58,6 +73,16 @@ public enum Function {
5873
ImmutableList.of(SimpleType.STRING, SimpleType.INT))),
5974
CelFunctionBinding.from(
6075
"string_char_at_int", String.class, Long.class, CelStringExtensions::charAt)),
76+
FORMAT(
77+
CelFunctionDecl.newFunctionDeclaration(
78+
"format",
79+
CelOverloadDecl.newMemberOverload(
80+
"string_format",
81+
"Formats the string using the provided arguments.",
82+
SimpleType.STRING,
83+
ImmutableList.of(SimpleType.STRING, ListType.create(SimpleType.DYN)))),
84+
CelFunctionBinding.from(
85+
"string_format", String.class, List.class, CelStringExtensions::format)),
6186
INDEX_OF(
6287
CelFunctionDecl.newFunctionDeclaration(
6388
"indexOf",
@@ -404,6 +429,272 @@ private static String join(List<String> stringList, String separator) {
404429
return Joiner.on(separator).join(stringList);
405430
}
406431

432+
private static String format(String formatSpecifier, List<Object> args)
433+
throws CelEvaluationException {
434+
StringBuilder builtStr = new StringBuilder(formatSpecifier.length());
435+
int i = 0;
436+
int argIndex = 0;
437+
while (i < formatSpecifier.length()) {
438+
if (formatSpecifier.charAt(i) == '%') {
439+
if (i + 1 < formatSpecifier.length() && formatSpecifier.charAt(i + 1) == '%') {
440+
builtStr.append('%');
441+
i += 2;
442+
} else {
443+
if (argIndex >= args.size()) {
444+
throw new CelEvaluationException("index " + argIndex + " out of range");
445+
}
446+
Object arg = args.get(argIndex++);
447+
i++; // Skip '%'
448+
449+
int precision = -1;
450+
if (i < formatSpecifier.length() && formatSpecifier.charAt(i) == '.') {
451+
i++;
452+
int start = i;
453+
while (i < formatSpecifier.length() && Character.isDigit(formatSpecifier.charAt(i))) {
454+
i++;
455+
}
456+
if (i == start) {
457+
throw new CelEvaluationException("could not find end of precision specifier");
458+
}
459+
try {
460+
precision = Integer.parseInt(formatSpecifier.substring(start, i));
461+
} catch (NumberFormatException e) {
462+
throw new CelEvaluationException("error while converting precision to integer", e);
463+
}
464+
}
465+
466+
if (i >= formatSpecifier.length()) {
467+
throw new CelEvaluationException("unexpected end of string");
468+
}
469+
char verb = formatSpecifier.charAt(i++);
470+
471+
switch (verb) {
472+
case 's' -> builtStr.append(formatString(arg));
473+
case 'd' -> builtStr.append(formatDecimal(arg));
474+
case 'f' -> builtStr.append(formatFixed(arg, precision));
475+
case 'e' -> builtStr.append(formatScientific(arg, precision));
476+
case 'b' -> builtStr.append(formatBinary(arg));
477+
case 'x', 'X' -> builtStr.append(formatHex(arg, verb == 'X'));
478+
case 'o' -> builtStr.append(formatOctal(arg));
479+
default ->
480+
throw new CelEvaluationException("unrecognized formatting clause \"" + verb + "\"");
481+
}
482+
}
483+
} else {
484+
builtStr.append(formatSpecifier.charAt(i++));
485+
}
486+
}
487+
return builtStr.toString();
488+
}
489+
490+
private static String formatString(Object val) throws CelEvaluationException {
491+
if (val == null) {
492+
return "null";
493+
}
494+
if (val instanceof String s) {
495+
return s;
496+
}
497+
if (val instanceof CelByteString byteString) {
498+
return byteString.toStringUtf8();
499+
}
500+
if (val instanceof Duration duration) {
501+
return DateTimeHelpers.toString(duration);
502+
}
503+
if (val instanceof Instant) {
504+
return val.toString();
505+
}
506+
if (val instanceof Boolean) {
507+
return val.toString();
508+
}
509+
if (val instanceof Long) {
510+
return val.toString();
511+
}
512+
if (val instanceof UnsignedLong) {
513+
return val.toString();
514+
}
515+
if (val instanceof Double d) {
516+
if (d.isNaN()) {
517+
return "NaN";
518+
}
519+
if (d.isInfinite()) {
520+
return d > 0 ? "Infinity" : "-Infinity";
521+
}
522+
return d.toString();
523+
}
524+
if (val instanceof List<?> list) {
525+
return formatList(list);
526+
}
527+
if (val instanceof Map<?, ?> map) {
528+
return formatMap(map);
529+
}
530+
if (val instanceof NullValue) {
531+
return "null";
532+
}
533+
if (val instanceof TypeType typeType) {
534+
return typeType.containingTypeName();
535+
}
536+
if (val instanceof CelType celType) {
537+
return celType.name();
538+
}
539+
throw new CelEvaluationException(
540+
"could not convert argument " + val.getClass().getName() + " to string");
541+
}
542+
543+
private static String formatList(List<?> list) throws CelEvaluationException {
544+
StringBuilder sb = new StringBuilder("[");
545+
for (int i = 0; i < list.size(); i++) {
546+
sb.append(formatString(list.get(i)));
547+
if (i < list.size() - 1) {
548+
sb.append(", ");
549+
}
550+
}
551+
sb.append("]");
552+
return sb.toString();
553+
}
554+
555+
private static String formatMap(Map<?, ?> map) throws CelEvaluationException {
556+
TreeMap<String, Object> sortedMap = new TreeMap<>();
557+
for (Map.Entry<?, ?> entry : map.entrySet()) {
558+
String keyStr = formatString(entry.getKey());
559+
sortedMap.put(keyStr, entry.getValue());
560+
}
561+
StringBuilder sb = new StringBuilder("{");
562+
int i = 0;
563+
for (Map.Entry<String, Object> entry : sortedMap.entrySet()) {
564+
sb.append(entry.getKey()).append(": ").append(formatString(entry.getValue()));
565+
if (i < sortedMap.size() - 1) {
566+
sb.append(", ");
567+
}
568+
i++;
569+
}
570+
sb.append("}");
571+
return sb.toString();
572+
}
573+
574+
private static String formatDecimal(Object arg) throws CelEvaluationException {
575+
if (arg instanceof Long || arg instanceof UnsignedLong) {
576+
return arg.toString();
577+
}
578+
if (arg instanceof Double) {
579+
return formatFixed(arg, -1);
580+
}
581+
throw new CelEvaluationException(
582+
"decimal clause can only be used on numbers, was given " + arg.getClass().getName());
583+
}
584+
585+
private static String formatFixed(Object arg, int precision) throws CelEvaluationException {
586+
if (arg instanceof Double d) {
587+
double val = d;
588+
if (Double.isNaN(val)) {
589+
return "NaN";
590+
}
591+
if (Double.isInfinite(val)) {
592+
return val > 0 ? "Infinity" : "-Infinity";
593+
}
594+
int p = precision >= 0 ? precision : 6;
595+
BigDecimal bd = BigDecimal.valueOf(val);
596+
bd = bd.setScale(p, RoundingMode.HALF_EVEN);
597+
return bd.toPlainString();
598+
}
599+
if (arg instanceof Long l) {
600+
return formatFixed((double) l, precision);
601+
}
602+
if (arg instanceof UnsignedLong ulong) {
603+
return formatFixed(ulong.doubleValue(), precision);
604+
}
605+
throw new CelEvaluationException(
606+
"fixed point clause can only be used on doubles, integers, and unsigned integers, was given"
607+
+ " "
608+
+ arg.getClass().getName());
609+
}
610+
611+
private static String formatScientific(Object arg, int precision) throws CelEvaluationException {
612+
if (arg instanceof Double d) {
613+
double val = d;
614+
if (Double.isNaN(val)) {
615+
return "NaN";
616+
}
617+
if (Double.isInfinite(val)) {
618+
return val > 0 ? "Infinity" : "-Infinity";
619+
}
620+
String fmtStr = precision >= 0 ? "%." + precision + "e" : "%.6e";
621+
return String.format(Locale.ROOT, fmtStr, val);
622+
}
623+
if (arg instanceof Long l) {
624+
return formatScientific((double) l, precision);
625+
}
626+
if (arg instanceof UnsignedLong ulong) {
627+
return formatScientific(ulong.doubleValue(), precision);
628+
}
629+
throw new CelEvaluationException(
630+
"scientific clause can only be used on doubles, integers, and unsigned integers, was given "
631+
+ arg.getClass().getName());
632+
}
633+
634+
private static String formatBinary(Object arg) throws CelEvaluationException {
635+
if (arg instanceof Long val) {
636+
if (val < 0) {
637+
if (val == Long.MIN_VALUE) {
638+
return "-1" + "0".repeat(63);
639+
}
640+
return "-" + Long.toBinaryString(-val);
641+
}
642+
return Long.toBinaryString(val);
643+
}
644+
if (arg instanceof UnsignedLong ulong) {
645+
return ulong.toString(2);
646+
}
647+
if (arg instanceof Boolean b) {
648+
return b ? "1" : "0";
649+
}
650+
throw new CelEvaluationException(
651+
"binary clause can only be used on integers and bools, was given "
652+
+ arg.getClass().getName());
653+
}
654+
655+
private static String formatHex(Object arg, boolean upper) throws CelEvaluationException {
656+
String result;
657+
if (arg instanceof Long val) {
658+
if (val < 0) {
659+
if (val == Long.MIN_VALUE) {
660+
result = "-8000000000000000";
661+
} else {
662+
result = "-" + String.format("%x", -val);
663+
}
664+
} else {
665+
result = String.format("%x", val);
666+
}
667+
} else if (arg instanceof UnsignedLong unsignedLong) {
668+
result = unsignedLong.toString(16);
669+
} else if (arg instanceof CelByteString byteString) {
670+
result = HexFormat.of().formatHex(byteString.toByteArray());
671+
} else if (arg instanceof String str) {
672+
result = HexFormat.of().formatHex(str.getBytes(UTF_8));
673+
} else {
674+
throw new CelEvaluationException(
675+
"hex clause can only be used on integers, byte buffers, and strings, was given "
676+
+ arg.getClass().getName());
677+
}
678+
return upper ? result.toUpperCase(Locale.ROOT) : result;
679+
}
680+
681+
private static String formatOctal(Object arg) throws CelEvaluationException {
682+
if (arg instanceof Long val) {
683+
if (val < 0) {
684+
if (val == Long.MIN_VALUE) {
685+
return "-1000000000000000000000";
686+
}
687+
return "-" + String.format("%o", -val);
688+
}
689+
return String.format("%o", val);
690+
}
691+
if (arg instanceof UnsignedLong ulong) {
692+
return ulong.toString(8);
693+
}
694+
throw new CelEvaluationException(
695+
"octal clause can only be used on integers, was given " + arg.getClass().getName());
696+
}
697+
407698
private static Long lastIndexOf(String str, String substr) throws CelEvaluationException {
408699
CelCodePointArray strCpa = CelCodePointArray.fromString(str);
409700
CelCodePointArray substrCpa = CelCodePointArray.fromString(substr);

extensions/src/test/java/dev/cel/extensions/CelExtensionsTest.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,7 @@ public void getAllFunctionNames() {
164164
"math.bitShiftRight",
165165
"math.sqrt",
166166
"charAt",
167+
"format",
167168
"indexOf",
168169
"join",
169170
"lastIndexOf",

0 commit comments

Comments
 (0)