Skip to content

Commit 261e335

Browse files
authored
Fix and document string.format (#436)
Signed-off-by: Justin King <jcking@google.com>
1 parent bfe4f8b commit 261e335

File tree

2 files changed

+150
-62
lines changed

2 files changed

+150
-62
lines changed

doc/extensions/strings.md

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
<!--
2+
Copyright 2025 Google LLC
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
https://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
-->
16+
17+
# Strings
18+
19+
## string.format(list) -> string
20+
21+
### Format
22+
23+
`%[.precision]conversion`
24+
25+
### Precision
26+
27+
Optional. In the form of a period `.` followed by a required positive decimal digit sequence. The default precision is `6`. Not all conversions support precision.
28+
29+
### Conversion
30+
31+
| Character | Precision | Description |
32+
| --- | --- | --- |
33+
| `s` | N | <table><tbody><tr><td><code>bool</code></td><td>The value is foramtted as <code>true</code> or <code>false</code>.</td></tr><tr><td><code>int</code></td><td>The value is formatted in base 10 with a preceding <code>-</code> if the value is negative. No insignificant <code>0</code>s must be included.</td></tr><tr><td><code>uint</code></td><td>The value is formatted in base 10. No insignificant <code>0</code>s must be included.</td></tr><tr><td><code>double</code></td><td>The value is formatted in base 10. No insignificant <code>0</code>s must be included. If there are no significant digits after the <code>.</code> then it must be excluded.</td></tr><tr><td><code>bytes</code></td><td>The value is formatted as if `string(value)` was performed and any invalid UTF-8 sequences are replaced with <code>\ufffd</code>. Multiple adjacent invalid UTF-8 sequences must be replaced with a single <code>\ufffd</code>.</td></tr><tr><td><code>string</code></td><td>The value is included as is.</td></tr><tr><td><code>duration</code></td><td>The value is formatted as decimal seconds as if the value was converted to <code>double</code> and then formatted as <code>%ds</code>.</td></tr><tr><td><code>timestamp</code></td><td>The value is formatted according to RFC 3339 and is always in UTC.</td></tr><tr><td><code>null_type</code></td><td>The value is formatted as <code>null</code>.</td></tr><tr><td><code>type</code></td><td>The value is formatted as a string.</td></tr><tr><td><code>list</code></td><td>The value is formatted as if each element was formatted as <code>"%s".format([element])</code>, joined together with <code>, </code> and enclosed with <code>[</code> and <code>]</code>.</td></tr><tr><td><code>map</code></td><td>The value is formatted as if each entry was formatted as <code>"%s: %s".format([key, value])</code>, sorted by the formatted keys in ascending order, joined together with <code>, </code>, and enclosed with <code>{</code> and <code>}</code>.</td></tr></tbody></table> |
34+
| `d` | N | <table><tbody><tr><td><code>int</code></td><td>The value is formatted in base 10 with a preceding <code>-</code> if the value is negative. No insignificant <code>0</code>s must be included.</td></tr><tr><td><code>uint</code></td><td>The value is formatted in base 10. No insignificant <code>0</code>s must be included.</td></tr><tr><td><code>double</code></td><td>The value is formatted in base 10. No insignificant <code>0</code>s must be included. If there are no significant digits after the <code>.</code> then it must be excluded.</td></tr></tbody></table> |
35+
| `f` | Y | `int` `uint` `double`: The value is converted to the style `[-]dddddd.dddddd` where there is at least one digit before the decimal and exactly `precision` digits after the decimal. If `precision` is 0, then the decimal is excluded. |
36+
| `e` | Y | `int` `uint` `double`: The value is converted to the style `[-]d.dddddde±dd` where there is one digit before the decimal and `precision` digits after the decimal followed by `e`, then the plus or minus, and then two digits. |
37+
| `x` `X` | N | Values are formatted in base 16. For `x` lowercase letters are used. For `X` uppercase letters are used.<table><tbody><tr><td><code>int</code> <code>uint</code></td><td>The value is formatted in base 16 with no insignificant digits. If the value was negative <code>-</code> is prepended.</td></tr><tr><td><code>string</code></td><td>The value is formatted as if `bytes(value)` was used to convert the <code>string</code> to <code>bytes</code> and then each byte is formatted in base 16 with exactly 2 digits.</td></tr><tr><td><code>bytes</code></td><td>The value is formatted as if each byte is formatted in base 16 with exactly 2 digits.</td></tr></tbody></table> |
38+
| `o` | N | `int` `uint`: The value is converted to base 8 with no insignificant digits. If the value was negative `-` is prepended. |
39+
| `b` | N | `int` `uint` `bool`: The value is converted to base 2 with no insignificant digits. If the value was negative `-` is prepended. |
40+
41+
> In all cases where `double` is accepted: if the value is NaN the result is `NaN`, if the value is infinity the result is `[-]Infinity`.
42+
43+
### Examples
44+
45+
```
46+
"%s".format(["foo"]) // foo
47+
"%s".format([b"foo"]) // foo
48+
"%d".format([1]) // 1
49+
"%d".format([1u]) // 1
50+
"%d".format([3.14]) // 3.14
51+
"%f".format([1]) // 1.000000
52+
"%f".format([1u]) // 1.000000
53+
"%f".format([3.14]) // 3.140000
54+
"%.1f".format([3.14]) // 3.1
55+
"%e".format([1]) // 1.000000e+00
56+
"%e".format([1u]) // 1.000000e+00
57+
"%e".format([3.14]) // 3.140000e+00
58+
"%.1e".format([3.14]) // 3.1e+00
59+
"%.1e".format([-3.14]) // -3.1e+00
60+
```

tests/simple/testdata/string_ext.textproto

Lines changed: 90 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -524,7 +524,7 @@ section: {
524524
name: "scientific notation formatting clause"
525525
expr: '"%.6e".format([1052.032911275])'
526526
value: {
527-
string_value: '1.052033×10⁰³',
527+
string_value: '1.052033e+03',
528528
}
529529
}
530530
test: {
@@ -538,14 +538,49 @@ section: {
538538
name: "default precision for scientific notation"
539539
expr: '"%e".format([2.71828])'
540540
value: {
541-
string_value: '2.718280×10⁰⁰',
541+
string_value: '2.718280e+00',
542542
}
543543
}
544544
test: {
545-
name: "unicode output for scientific notation"
546-
expr: '"unescaped unicode: %e, escaped unicode: %e".format([2.71828, 2.71828])'
545+
name: "NaN support for scientific notation"
546+
expr: '"%e".format(["NaN"])'
547547
value: {
548-
string_value: 'unescaped unicode: 2.718280×10⁰⁰, escaped unicode: 2.718280\u00d710\u2070\u2070',
548+
string_value: 'NaN',
549+
}
550+
}
551+
test: {
552+
name: "positive infinity support for scientific notation"
553+
expr: '"%e".format([double("Infinity")])'
554+
value: {
555+
string_value: 'Infinity',
556+
}
557+
}
558+
test: {
559+
name: "negative infinity support for scientific notation"
560+
expr: '"%e".format([double("-Infinity")])'
561+
value: {
562+
string_value: '-Infinity',
563+
}
564+
}
565+
test: {
566+
name: "NaN support for decimal"
567+
expr: '"%d".format(["NaN"])'
568+
value: {
569+
string_value: 'NaN',
570+
}
571+
}
572+
test: {
573+
name: "positive infinity support for decimal"
574+
expr: '"%d".format([double("Infinity")])'
575+
value: {
576+
string_value: 'Infinity',
577+
}
578+
}
579+
test: {
580+
name: "negative infinity support for decimal"
581+
expr: '"%d".format([double("-Infinity")])'
582+
value: {
583+
string_value: '-Infinity',
549584
}
550585
}
551586
test: {
@@ -557,16 +592,16 @@ section: {
557592
}
558593
test: {
559594
name: "positive infinity support for fixed-point"
560-
expr: '"%f".format(["Infinity"])'
595+
expr: '"%f".format([double("Infinity")])'
561596
value: {
562-
string_value: '',
597+
string_value: 'Infinity',
563598
}
564599
}
565600
test: {
566601
name: "negative infinity support for fixed-point"
567-
expr: '"%f".format(["-Infinity"])'
602+
expr: '"%f".format([double("-Infinity")])'
568603
value: {
569-
string_value: '-',
604+
string_value: '-Infinity',
570605
}
571606
}
572607
test: {
@@ -578,9 +613,9 @@ section: {
578613
}
579614
test: {
580615
name: "null support for string"
581-
expr: '"null: %s".format([null])'
616+
expr: '"%s".format([null])'
582617
value: {
583-
string_value: 'null: null',
618+
string_value: 'null',
584619
}
585620
}
586621
test: {
@@ -592,16 +627,16 @@ section: {
592627
}
593628
test: {
594629
name: "bytes support for string"
595-
expr: '"some bytes: %s".format([b"xyz"])'
630+
expr: '"%s".format([b"xyz"])'
596631
value: {
597-
string_value: 'some bytes: xyz',
632+
string_value: 'xyz',
598633
}
599634
}
600635
test: {
601636
name: "type() support for string"
602-
expr: '"type is %s".format([type("test string")])'
637+
expr: '"%s".format([type("test string")])'
603638
value: {
604-
string_value: 'type is string',
639+
string_value: 'string',
605640
}
606641
}
607642
test: {
@@ -620,135 +655,128 @@ section: {
620655
}
621656
test: {
622657
name: "list support for string"
623-
expr: '"%s".format([["abc", 3.14, null, [9, 8, 7, 6], timestamp("2023-02-03T23:31:20Z")]])'
658+
expr: '"%s".format([[abc, 3.14, null, [9, 8, 7, 6], 2023-02-03T23:31:20Z]])'
624659
value: {
625-
string_value: '["abc", 3.14, null, [9, 8, 7, 6], timestamp("2023-02-03T23:31:20Z")]',
660+
string_value: '[abc, 3.14, null, [9, 8, 7, 6], 2023-02-03T23:31:20Z]',
626661
}
627662
}
628663
test: {
629664
name: "map support for string"
630665
expr: '"%s".format([{"key1": b"xyz", "key5": null, "key2": duration("2h"), "key4": true, "key3": 2.71828}])'
631666
value: {
632-
string_value: '{"key1":b"xyz", "key2":duration("7200s"), "key3":2.71828, "key4":true, "key5":null}',
667+
string_value: '{key1: xyz, key2: 7200s, key3: 2.71828, key4: true, key5: null}',
633668
}
634669
}
635670
test: {
636671
name: "map support (all key types)"
637-
expr: '"map with multiple key types: %s".format([{1: "value1", uint(2): "value2", true: double("NaN")}])'
672+
expr: '"%s".format([{1: "value1", uint(2): "value2", true: double("NaN")}])'
638673
value: {
639-
string_value: 'map with multiple key types: {1:"value1", 2:"value2", true:"NaN"}',
674+
string_value: '{1: value1, 2: value2, true: NaN}',
640675
}
641676
}
642677
test: {
643678
name: "boolean support for %s"
644-
expr: '"true bool: %s, false bool: %s".format([true, false])'
679+
expr: '"%s, %s".format([true, false])'
645680
value: {
646-
string_value: 'true bool: true, false bool: false',
681+
string_value: 'true, false',
647682
}
648683
}
649684
test: {
650685
name: "dyntype support for string formatting clause"
651-
expr: '"dynamic string: %s".format([dyn("a string")])'
686+
expr: '"%s".format([dyn("a string")])'
652687
value: {
653-
string_value: 'dynamic string: a string',
688+
string_value: 'a string',
654689
}
655690
}
656691
test: {
657692
name: "dyntype support for numbers with string formatting clause"
658-
expr: '"dynIntStr: %s dynDoubleStr: %s".format([dyn(32), dyn(56.8)])'
693+
expr: '"%s, %s".format([dyn(32), dyn(56.8)])'
659694
value: {
660-
string_value: 'dynIntStr: 32 dynDoubleStr: 56.8',
695+
string_value: '32, 56.8',
661696
}
662697
}
663698
test: {
664699
name: "dyntype support for integer formatting clause"
665-
expr: '"dynamic int: %d".format([dyn(128)])'
700+
expr: '"%d".format([dyn(128)])'
666701
value: {
667-
string_value: 'dynamic int: 128',
702+
string_value: '128',
668703
}
669704
}
670705
test: {
671706
name: "dyntype support for integer formatting clause (unsigned)"
672-
expr: '"dynamic unsigned int: %d".format([dyn(256u)])'
707+
expr: '"%d".format([dyn(256u)])'
673708
value: {
674-
string_value: 'dynamic unsigned int: 256',
709+
string_value: '256',
675710
}
676711
}
677712
test: {
678713
name: "dyntype support for hex formatting clause"
679-
expr: '"dynamic hex int: %x".format([dyn(22)])'
714+
expr: '"%x".format([dyn(22)])'
680715
value: {
681-
string_value: 'dynamic hex int: 16',
716+
string_value: '16',
682717
}
683718
}
684719
test: {
685720
name: "dyntype support for hex formatting clause (uppercase)"
686-
expr: '"dynamic hex int: %X (uppercase)".format([dyn(26)])'
721+
expr: '"%X".format([dyn(26)])'
687722
value: {
688-
string_value: 'dynamic hex int: 1A (uppercase)',
723+
string_value: '1A',
689724
}
690725
}
691726
test: {
692727
name: "dyntype support for unsigned hex formatting clause"
693-
expr: '"dynamic hex int: %x (unsigned)".format([dyn(500u)])'
728+
expr: '"%x".format([dyn(500u)])'
694729
value: {
695-
string_value: 'dynamic hex int: 1f4 (unsigned)',
730+
string_value: '1f4',
696731
}
697732
}
698733
test: {
699734
name: "dyntype support for fixed-point formatting clause"
700-
expr: '"dynamic double: %.3f".format([dyn(4.5)])'
735+
expr: '"%.3f".format([dyn(4.5)])'
701736
value: {
702-
string_value: 'dynamic double: 4.500',
737+
string_value: '4.500',
703738
}
704739
}
705740
test: {
706741
name: "dyntype support for scientific notation"
707-
expr: '"(dyntype) e: %e".format([dyn(2.71828)])'
742+
expr: '"%e".format([dyn(2.71828)])'
708743
value: {
709-
string_value: '(dyntype) e: 2.718280×10⁰⁰',
744+
string_value: '2.718280e+00',
710745
}
711746
}
712747
test: {
713748
name: "dyntype NaN/infinity support for fixed-point"
714-
expr: '"NaN: %f, infinity: %f".format([dyn("NaN"), dyn("Infinity")])'
749+
expr: '"NaN: %f, infinity: %f".format([double("NaN"), double("Infinity"), double("-Infinity")])'
715750
value: {
716-
string_value: 'NaN: NaN, infinity: ∞',
751+
string_value: 'NaN, Infinity, -Infinity',
717752
}
718753
}
719754
test: {
720755
name: "dyntype support for timestamp"
721-
expr: '"dyntype timestamp: %s".format([dyn(timestamp("2009-11-10T23:00:00Z"))])'
756+
expr: '"%s".format([dyn(timestamp("2009-11-10T23:00:00Z"))])'
722757
value: {
723-
string_value: 'dyntype timestamp: 2009-11-10T23:00:00Z',
758+
string_value: '2009-11-10T23:00:00Z',
724759
}
725760
}
726761
test: {
727762
name: "dyntype support for duration"
728-
expr: '"dyntype duration: %s".format([dyn(duration("8747s"))])'
763+
expr: '"%s".format([dyn(duration("8747s"))])'
729764
value: {
730-
string_value: 'dyntype duration: 8747s',
765+
string_value: '8747s',
731766
}
732767
}
733768
test: {
734769
name: "dyntype support for lists"
735-
expr: '"dyntype list: %s".format([dyn([6, 4.2, "a string"])])'
770+
expr: '"%s".format([dyn([6, 4.2, "a string"])])'
736771
value: {
737-
string_value: 'dyntype list: [6, 4.2, "a string"]',
772+
string_value: '[6, 4.2, a string]',
738773
}
739774
}
740775
test: {
741776
name: "dyntype support for maps"
742-
expr: '"dyntype map: %s".format([{"strKey":"x", 6:duration("422s"), true:42}])'
777+
expr: '"%s".format([{"strKey":"x", 6:duration("422s"), true:42}])'
743778
value: {
744-
string_value: 'dyntype map: {"strKey":"x", 6:duration("422s"), true:42}',
745-
}
746-
}
747-
test: {
748-
name: "message field support"
749-
expr: '"message field msg.single_int32: %d, msg.single_double: %.1f".format([2, 1.0])'
750-
value: {
751-
string_value: 'message field msg.single_int32: 2, msg.single_double: 1.0',
779+
string_value: '{strKey: x, 6: 422s, true: 42}',
752780
}
753781
}
754782
test: {
@@ -760,10 +788,10 @@ section: {
760788
}
761789
bindings: {
762790
key: "str_var"
763-
value: { value: { string_value: "str is %s and some more" } }
791+
value: { value: { string_value: "%s" } }
764792
}
765793
value: {
766-
string_value: 'str is filler and some more',
794+
string_value: 'filler',
767795
}
768796
}
769797
test: {
@@ -820,10 +848,10 @@ section: {
820848
}
821849
bindings: {
822850
key: "str_var"
823-
value: { value: { string_value: "this is 5 in binary: %b" } }
851+
value: { value: { string_value: "%b" } }
824852
}
825853
value: {
826-
string_value: 'this is 5 in binary: 101',
854+
string_value: '101',
827855
}
828856
}
829857
test: {
@@ -838,7 +866,7 @@ section: {
838866
value: { value: { string_value: "%.6e" } }
839867
}
840868
value: {
841-
string_value: '1.052033×10⁰³',
869+
string_value: '1.052033e+03',
842870
}
843871
}
844872
test: {

0 commit comments

Comments
 (0)