Skip to content

Commit 8c89e4d

Browse files
pierdfacebook-github-bot
authored andcommitted
Implement string interpolation for floats (#39)
Summary: Addresses: #3 Spec: https://github.com/bazelbuild/starlark/blob/689f54426951638ef5b7c41a14d8fc48e65c5f77/spec.md#string-interpolation Changes: - adds `float::write_{decimal,scientific,compact}` functions for various kinds of string interpolation for floats - makes `Display for StarlarkFloat` use `write_compact` (as per spec) - makes string interpolation use `write_*` functions Pull Request resolved: #39 Reviewed By: krallin Differential Revision: D31645493 Pulled By: ndmitchell fbshipit-source-id: d47272d4bf301085544acfcfbaf17f1c2577f692
1 parent a804ccd commit 8c89e4d

File tree

4 files changed

+236
-25
lines changed

4 files changed

+236
-25
lines changed

starlark/src/eval/tests/go.rs

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -107,15 +107,6 @@ fn test_go() {
107107
"int(1e100)",
108108
"1000000 * 1000000 * 1000000",
109109
"int overflow in starlark-rust",
110-
// str for floats doesn't conform to the spec
111-
"assert.eq(str(1.23e45),",
112-
"assert.eq(str(-1.23e-45),",
113-
"assert.eq(str(sorted([inf, neginf, nan, 1e300, -1e300,",
114-
// string interpolation for floats not implemented
115-
"%d",
116-
"%e",
117-
"%f",
118-
"%g",
119110
],
120111
));
121112
assert.conformance(&ignore_bad_lines(

starlark/src/values/interpolation.rs

Lines changed: 49 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ use anyhow::anyhow;
2424
use gazebo::{cast, prelude::*};
2525
use thiserror::Error;
2626

27-
use crate::values::{dict::Dict, tuple::Tuple, Value, ValueError, ValueLike};
27+
use crate::values::{dict::Dict, float, num, tuple::Tuple, Value, ValueError, ValueLike};
2828

2929
/// Operator `%` format or evaluation errors
3030
#[derive(Clone, Dupe, Debug, Error)]
@@ -75,7 +75,19 @@ pub(crate) fn percent(format: &str, value: Value) -> anyhow::Result<String> {
7575
}
7676
}
7777
b'r' => next_value()?.collect_repr(out),
78-
b'd' => write!(out, "{}", next_value()?.to_int()?).unwrap(),
78+
b'd' => {
79+
let value = next_value()?;
80+
if let Some(num::Num::Float(v)) = value.unpack_num() {
81+
match num::Num::Float(v.trunc()).as_int() {
82+
None => {
83+
return ValueError::unsupported(&float::StarlarkFloat(v), "%d");
84+
}
85+
Some(v) => write!(out, "{}", v).unwrap(),
86+
}
87+
} else {
88+
write!(out, "{}", value.to_int()?).unwrap()
89+
}
90+
}
7991
b'o' => {
8092
let v = next_value()?.to_int()?;
8193
write!(
@@ -106,6 +118,41 @@ pub(crate) fn percent(format: &str, value: Value) -> anyhow::Result<String> {
106118
)
107119
.unwrap()
108120
}
121+
b'e' => {
122+
let v = next_value()?
123+
.unpack_num()
124+
.ok_or(ValueError::IncorrectParameterType)?
125+
.as_float();
126+
float::write_scientific(out, v, 'e', false).unwrap()
127+
}
128+
b'E' => {
129+
let v = next_value()?
130+
.unpack_num()
131+
.ok_or(ValueError::IncorrectParameterType)?
132+
.as_float();
133+
float::write_scientific(out, v, 'E', false).unwrap()
134+
}
135+
b'f' | b'F' => {
136+
let v = next_value()?
137+
.unpack_num()
138+
.ok_or(ValueError::IncorrectParameterType)?
139+
.as_float();
140+
float::write_decimal(out, v).unwrap()
141+
}
142+
b'g' => {
143+
let v = next_value()?
144+
.unpack_num()
145+
.ok_or(ValueError::IncorrectParameterType)?
146+
.as_float();
147+
float::write_compact(out, v, 'e').unwrap()
148+
}
149+
b'G' => {
150+
let v = next_value()?
151+
.unpack_num()
152+
.ok_or(ValueError::IncorrectParameterType)?
153+
.as_float();
154+
float::write_compact(out, v, 'E').unwrap()
155+
}
109156
c => {
110157
res.push(b'%');
111158
res.push(c);

starlark/src/values/types/float.rs

Lines changed: 184 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,111 @@ use crate::values::{
2929
StarlarkValue, Value, ValueError,
3030
};
3131

32+
const WRITE_PRECISION: usize = 6;
33+
34+
fn write_non_finite<W: fmt::Write>(output: &mut W, f: f64) -> fmt::Result {
35+
debug_assert!(f.is_nan() || f.is_infinite());
36+
if f.is_nan() {
37+
write!(output, "nan")
38+
} else {
39+
write!(
40+
output,
41+
"{}inf",
42+
if f.is_sign_positive() { "+" } else { "-" }
43+
)
44+
}
45+
}
46+
47+
pub fn write_decimal<W: fmt::Write>(output: &mut W, f: f64) -> fmt::Result {
48+
if !f.is_finite() {
49+
write_non_finite(output, f)
50+
} else {
51+
write!(output, "{:.prec$}", f, prec = WRITE_PRECISION)
52+
}
53+
}
54+
55+
pub fn write_scientific<W: fmt::Write>(
56+
output: &mut W,
57+
f: f64,
58+
exponent_char: char,
59+
strip_trailing_zeros: bool,
60+
) -> fmt::Result {
61+
if !f.is_finite() {
62+
write_non_finite(output, f)
63+
} else {
64+
let abs = f.abs();
65+
let exponent = if f == 0.0 {
66+
0
67+
} else {
68+
abs.log10().floor() as i32
69+
};
70+
let normal = if f == 0.0 {
71+
0.0
72+
} else {
73+
abs / 10f64.powf(exponent as f64)
74+
};
75+
76+
// start with "-" for a negative number
77+
if f.is_sign_negative() {
78+
output.write_char('-')?
79+
}
80+
81+
// use the whole integral part of normal (a single digit)
82+
output.write_fmt(format_args!("{}", normal.trunc()))?;
83+
84+
// calculate the fractional tail for given precision
85+
let mut tail = (normal.fract() * 10f64.powf(WRITE_PRECISION as f64)).round() as u64;
86+
let mut rev_tail = [0u8; WRITE_PRECISION];
87+
let mut rev_tail_len = 0;
88+
let mut removing_trailing_zeros = strip_trailing_zeros;
89+
for _ in 0..WRITE_PRECISION {
90+
let tail_digit = tail % 10;
91+
if tail_digit != 0 || !removing_trailing_zeros {
92+
removing_trailing_zeros = false;
93+
rev_tail[rev_tail_len] = tail_digit as u8;
94+
rev_tail_len += 1;
95+
}
96+
tail /= 10;
97+
}
98+
99+
// write fractional part
100+
if rev_tail_len != 0 {
101+
output.write_char('.')?;
102+
}
103+
for digit in rev_tail[0..rev_tail_len].iter().rev() {
104+
output.write_char((b'0' + digit) as char)?;
105+
}
106+
107+
// add exponent part
108+
output.write_char(exponent_char)?;
109+
output.write_fmt(format_args!("{:+03}", exponent))
110+
}
111+
}
112+
113+
pub fn write_compact<W: fmt::Write>(output: &mut W, f: f64, exponent_char: char) -> fmt::Result {
114+
if !f.is_finite() {
115+
write_non_finite(output, f)
116+
} else {
117+
let abs = f.abs();
118+
let exponent = if f == 0.0 {
119+
0
120+
} else {
121+
abs.log10().floor() as i32
122+
};
123+
124+
if exponent.abs() >= WRITE_PRECISION as i32 {
125+
// use scientific notation if exponent is outside of our precision (but strip 0s)
126+
write_scientific(output, f, exponent_char, true)
127+
} else if f.fract() == 0.0 {
128+
// make sure there's a fractional part even if the number doesn't have it
129+
output.write_fmt(format_args!("{:.1}", f))
130+
} else {
131+
// rely on the built-in formatting otherwise
132+
output.write_fmt(format_args!("{}", f))
133+
}
134+
}
135+
}
136+
32137
#[derive(Clone, Dupe, Copy, Debug, AnyLifetime)]
33138
pub struct StarlarkFloat(pub f64);
34139

@@ -70,19 +175,7 @@ where
70175

71176
impl Display for StarlarkFloat {
72177
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
73-
if self.0.is_nan() {
74-
write!(f, "nan")
75-
} else if self.0.is_infinite() {
76-
if self.0.is_sign_positive() {
77-
write!(f, "+inf")
78-
} else {
79-
write!(f, "-inf")
80-
}
81-
} else if self.0.fract() == 0.0 {
82-
write!(f, "{:.1}", self.0)
83-
} else {
84-
write!(f, "{}", self.0)
85-
}
178+
write_compact(f, self.0, 'e')
86179
}
87180
}
88181

@@ -208,8 +301,86 @@ impl<'v> StarlarkValue<'v> for StarlarkFloat {
208301

209302
#[cfg(test)]
210303
mod tests {
304+
use super::*;
211305
use crate::assert;
212306

307+
fn non_finite(f: f64) -> String {
308+
let mut buf = String::new();
309+
write_non_finite(&mut buf, f).unwrap();
310+
buf
311+
}
312+
313+
#[test]
314+
fn test_write_non_finite() {
315+
assert_eq!(non_finite(f64::NAN), "nan");
316+
assert_eq!(non_finite(f64::INFINITY), "+inf");
317+
assert_eq!(non_finite(f64::NEG_INFINITY), "-inf");
318+
}
319+
320+
#[test]
321+
#[should_panic]
322+
fn test_write_non_finite_only_for_non_finite() {
323+
non_finite(0f64);
324+
}
325+
326+
fn decimal(f: f64) -> String {
327+
let mut buf = String::new();
328+
write_decimal(&mut buf, f).unwrap();
329+
buf
330+
}
331+
332+
#[test]
333+
fn test_write_decimal() {
334+
assert_eq!(decimal(f64::NAN), "nan");
335+
assert_eq!(decimal(f64::INFINITY), "+inf");
336+
assert_eq!(decimal(f64::NEG_INFINITY), "-inf");
337+
338+
assert_eq!(decimal(0f64), "0.000000");
339+
assert_eq!(decimal(std::f64::consts::PI), "3.141593");
340+
assert_eq!(decimal(-std::f64::consts::E), "-2.718282");
341+
assert_eq!(decimal(1e10), "10000000000.000000");
342+
}
343+
344+
fn scientific(f: f64) -> String {
345+
let mut buf = String::new();
346+
write_scientific(&mut buf, f, 'e', false).unwrap();
347+
buf
348+
}
349+
350+
#[test]
351+
fn test_write_scientific() {
352+
assert_eq!(scientific(f64::NAN), "nan");
353+
assert_eq!(scientific(f64::INFINITY), "+inf");
354+
assert_eq!(scientific(f64::NEG_INFINITY), "-inf");
355+
356+
assert_eq!(scientific(0f64), "0.000000e+00");
357+
assert_eq!(scientific(-0f64), "-0.000000e+00");
358+
assert_eq!(scientific(1.23e45), "1.230000e+45");
359+
assert_eq!(scientific(-3.14e-145), "-3.140000e-145");
360+
assert_eq!(scientific(1e300), "1.000000e+300");
361+
}
362+
363+
fn compact(f: f64) -> String {
364+
let mut buf = String::new();
365+
write_compact(&mut buf, f, 'e').unwrap();
366+
buf
367+
}
368+
369+
#[test]
370+
fn test_write_compact() {
371+
assert_eq!(compact(f64::NAN), "nan");
372+
assert_eq!(compact(f64::INFINITY), "+inf");
373+
assert_eq!(compact(f64::NEG_INFINITY), "-inf");
374+
375+
assert_eq!(compact(0f64), "0.0");
376+
assert_eq!(compact(std::f64::consts::PI), "3.141592653589793");
377+
assert_eq!(compact(-std::f64::consts::E), "-2.718281828459045");
378+
assert_eq!(compact(1e10), "1e+10");
379+
assert_eq!(compact(1.23e45), "1.23e+45");
380+
assert_eq!(compact(-3.14e-145), "-3.14e-145");
381+
assert_eq!(compact(1e300), "1e+300");
382+
}
383+
213384
#[test]
214385
fn test_arithmetic_operators() {
215386
assert::all_true(

starlark/testcases/eval/go/float.star

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -237,7 +237,9 @@ assert.true(not (nan < nan))
237237
assert.true(not (nan != nan)) # unlike Python
238238
# Sort is stable: 0.0 and -0.0 are equal, but they are not permuted.
239239
# Similarly 1 and 1.0.
240-
assert.eq(str(sorted([inf, neginf, nan, 1e300, -1e300, 1.0, -1.0, 1, -1, 1e-300, -1e-300, 0, 0.0, negzero, 1e-300, -1e-300])), "[-inf, -1e+300, -1.0, -1, -1e-300, -1e-300, 0, 0.0, -0.0, 1e-300, 1e-300, 1.0, 1, 1e+300, +inf, nan]")
240+
assert.eq(
241+
str(sorted([inf, neginf, nan, 1e300, -1e300, 1.0, -1.0, 1, -1, 1e-300, -1e-300, 0, 0.0, negzero, 1e-300, -1e-300])),
242+
"[-inf, -1e+300, -1.0, -1, -1e-300, -1e-300, 0, 0.0, -0.0, 1e-300, 1e-300, 1.0, 1, 1e+300, +inf, nan]")
241243

242244
# Sort is stable, and its result contains no adjacent x, y such that y > x.
243245
# Note: Python's reverse sort is unstable; see https://bugs.python.org/issue36095.

0 commit comments

Comments
 (0)