Skip to content

Commit f48e547

Browse files
authored
Merge pull request RustPython#2316 from ChJR/feature/format_float
Unify float formatting
2 parents c5fca81 + 3878e8e commit f48e547

File tree

5 files changed

+276
-74
lines changed

5 files changed

+276
-74
lines changed

common/src/float_ops.rs

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,96 @@ pub fn is_integer(v: f64) -> bool {
8383
(v - v.round()).abs() < std::f64::EPSILON
8484
}
8585

86+
#[derive(Debug)]
87+
pub enum Case {
88+
Lower,
89+
Upper,
90+
}
91+
92+
fn format_nan(case: Case) -> String {
93+
let nan = match case {
94+
Case::Lower => "nan",
95+
Case::Upper => "NAN",
96+
};
97+
98+
nan.to_string()
99+
}
100+
101+
fn format_inf(case: Case) -> String {
102+
let inf = match case {
103+
Case::Lower => "inf",
104+
Case::Upper => "INF",
105+
};
106+
107+
inf.to_string()
108+
}
109+
110+
pub fn format_fixed(precision: usize, magnitude: f64, case: Case) -> String {
111+
match magnitude {
112+
magnitude if magnitude.is_finite() => format!("{:.*}", precision, magnitude),
113+
magnitude if magnitude.is_nan() => format_nan(case),
114+
magnitude if magnitude.is_infinite() => format_inf(case),
115+
_ => "".to_string(),
116+
}
117+
}
118+
119+
// Formats floats into Python style exponent notation, by first formatting in Rust style
120+
// exponent notation (`1.0000e0`), then convert to Python style (`1.0000e+00`).
121+
pub fn format_exponent(precision: usize, magnitude: f64, case: Case) -> String {
122+
match magnitude {
123+
magnitude if magnitude.is_finite() => {
124+
let r_exp = format!("{:.*e}", precision, magnitude);
125+
let mut parts = r_exp.splitn(2, 'e');
126+
let base = parts.next().unwrap();
127+
let exponent = parts.next().unwrap().parse::<i64>().unwrap();
128+
let e = match case {
129+
Case::Lower => 'e',
130+
Case::Upper => 'E',
131+
};
132+
format!("{}{}{:+#03}", base, e, exponent)
133+
}
134+
magnitude if magnitude.is_nan() => format_nan(case),
135+
magnitude if magnitude.is_infinite() => format_inf(case),
136+
_ => "".to_string(),
137+
}
138+
}
139+
140+
fn remove_trailing_zeros(s: String) -> String {
141+
let mut s = s;
142+
while s.ends_with('0') || s.ends_with('.') {
143+
s.truncate(s.len() - 1);
144+
}
145+
146+
s
147+
}
148+
149+
pub fn format_general(precision: usize, magnitude: f64, case: Case) -> String {
150+
match magnitude {
151+
magnitude if magnitude.is_finite() => {
152+
let r_exp = format!("{:.*e}", precision.saturating_sub(1), magnitude);
153+
let mut parts = r_exp.splitn(2, 'e');
154+
let base = parts.next().unwrap();
155+
let exponent = parts.next().unwrap().parse::<i64>().unwrap();
156+
if exponent < -4 || exponent >= (precision as i64) {
157+
let e = match case {
158+
Case::Lower => 'e',
159+
Case::Upper => 'E',
160+
};
161+
162+
let base = remove_trailing_zeros(format!("{:.*}", precision + 1, base));
163+
format!("{}{}{:+#03}", base, e, exponent)
164+
} else {
165+
let precision = (precision as i64) - 1 - exponent;
166+
let precision = precision as usize;
167+
remove_trailing_zeros(format!("{:.*}", precision, magnitude))
168+
}
169+
}
170+
magnitude if magnitude.is_nan() => format_nan(case),
171+
magnitude if magnitude.is_infinite() => format_inf(case),
172+
_ => "".to_string(),
173+
}
174+
}
175+
86176
pub fn to_string(value: f64) -> String {
87177
let lit = format!("{:e}", value);
88178
if let Some(position) = lit.find('e') {

extra_tests/snippets/strings.py

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -232,6 +232,73 @@ def __repr__(self):
232232
assert "%e" % 0.1 == '1.000000e-01'
233233
assert "%e" % 10 == '1.000000e+01'
234234
assert "%.10e" % 1.2345678901234567890 == '1.2345678901e+00'
235+
assert '%e' % float('nan') == 'nan'
236+
assert '%e' % float('-nan') == 'nan'
237+
assert '%E' % float('nan') == 'NAN'
238+
assert '%e' % float('inf') == 'inf'
239+
assert '%e' % float('-inf') == '-inf'
240+
assert '%E' % float('inf') == 'INF'
241+
assert "%g" % 123456.78901234567890 == '123457'
242+
assert "%.0g" % 123456.78901234567890 == '1e+05'
243+
assert "%.1g" % 123456.78901234567890 == '1e+05'
244+
assert "%.2g" % 123456.78901234567890 == '1.2e+05'
245+
assert "%g" % 1234567.8901234567890 == '1.23457e+06'
246+
assert "%.0g" % 1234567.8901234567890 == '1e+06'
247+
assert "%.1g" % 1234567.8901234567890 == '1e+06'
248+
assert "%.2g" % 1234567.8901234567890 == '1.2e+06'
249+
assert "%.3g" % 1234567.8901234567890 == '1.23e+06'
250+
assert "%.5g" % 1234567.8901234567890 == '1.2346e+06'
251+
assert "%.6g" % 1234567.8901234567890 == '1.23457e+06'
252+
assert "%.7g" % 1234567.8901234567890 == '1234568'
253+
assert "%.8g" % 1234567.8901234567890 == '1234567.9'
254+
assert "%G" % 123456.78901234567890 == '123457'
255+
assert "%.0G" % 123456.78901234567890 == '1E+05'
256+
assert "%.1G" % 123456.78901234567890 == '1E+05'
257+
assert "%.2G" % 123456.78901234567890 == '1.2E+05'
258+
assert "%G" % 1234567.8901234567890 == '1.23457E+06'
259+
assert "%.0G" % 1234567.8901234567890 == '1E+06'
260+
assert "%.1G" % 1234567.8901234567890 == '1E+06'
261+
assert "%.2G" % 1234567.8901234567890 == '1.2E+06'
262+
assert "%.3G" % 1234567.8901234567890 == '1.23E+06'
263+
assert "%.5G" % 1234567.8901234567890 == '1.2346E+06'
264+
assert "%.6G" % 1234567.8901234567890 == '1.23457E+06'
265+
assert "%.7G" % 1234567.8901234567890 == '1234568'
266+
assert "%.8G" % 1234567.8901234567890 == '1234567.9'
267+
assert '%g' % 0.12345678901234567890 == '0.123457'
268+
assert '%g' % 0.12345678901234567890e-1 == '0.0123457'
269+
assert '%g' % 0.12345678901234567890e-2 == '0.00123457'
270+
assert '%g' % 0.12345678901234567890e-3 == '0.000123457'
271+
assert '%g' % 0.12345678901234567890e-4 == '1.23457e-05'
272+
assert '%g' % 0.12345678901234567890e-5 == '1.23457e-06'
273+
assert '%.6g' % 0.12345678901234567890e-5 == '1.23457e-06'
274+
assert '%.10g' % 0.12345678901234567890e-5 == '1.23456789e-06'
275+
assert '%.20g' % 0.12345678901234567890e-5 == '1.2345678901234567384e-06'
276+
assert '%G' % 0.12345678901234567890 == '0.123457'
277+
assert '%G' % 0.12345678901234567890E-1 == '0.0123457'
278+
assert '%G' % 0.12345678901234567890E-2 == '0.00123457'
279+
assert '%G' % 0.12345678901234567890E-3 == '0.000123457'
280+
assert '%G' % 0.12345678901234567890E-4 == '1.23457E-05'
281+
assert '%G' % 0.12345678901234567890E-5 == '1.23457E-06'
282+
assert '%.6G' % 0.12345678901234567890E-5 == '1.23457E-06'
283+
assert '%.10G' % 0.12345678901234567890E-5 == '1.23456789E-06'
284+
assert '%.20G' % 0.12345678901234567890E-5 == '1.2345678901234567384E-06'
285+
assert '%g' % float('nan') == 'nan'
286+
assert '%g' % float('-nan') == 'nan'
287+
assert '%G' % float('nan') == 'NAN'
288+
assert '%g' % float('inf') == 'inf'
289+
assert '%g' % float('-inf') == '-inf'
290+
assert '%G' % float('inf') == 'INF'
291+
assert "%.0g" % 1.020e-13 == '1e-13'
292+
assert "%.0g" % 1.020e-13 == '1e-13'
293+
assert "%.1g" % 1.020e-13 == '1e-13'
294+
assert "%.2g" % 1.020e-13 == '1e-13'
295+
assert "%.3g" % 1.020e-13 == '1.02e-13'
296+
assert "%.4g" % 1.020e-13 == '1.02e-13'
297+
assert "%.5g" % 1.020e-13 == '1.02e-13'
298+
assert "%.6g" % 1.020e-13 == '1.02e-13'
299+
assert "%.7g" % 1.020e-13 == '1.02e-13'
300+
assert "%g" % 1.020e-13 == '1.02e-13'
301+
assert "%g" % 1.020e-4 == '0.000102'
235302

236303
assert_raises(TypeError, lambda: "My name is %s and I'm %(age)d years old" % ("Foo", 25), _msg='format requires a mapping')
237304
assert_raises(TypeError, lambda: "My name is %(name)s" % "Foo", _msg='format requires a mapping')
@@ -477,6 +544,68 @@ def try_mutate_str():
477544
assert '{:e}'.format(float('-inf')) == '-inf'
478545
assert '{:E}'.format(float('inf')) == 'INF'
479546

547+
# Test g & G formatting
548+
assert '{:g}'.format(123456.78901234567890) == '123457'
549+
assert '{:.0g}'.format(123456.78901234567890) == '1e+05'
550+
assert '{:.1g}'.format(123456.78901234567890) == '1e+05'
551+
assert '{:.2g}'.format(123456.78901234567890) == '1.2e+05'
552+
assert '{:g}'.format(1234567.8901234567890) == '1.23457e+06'
553+
assert '{:.0g}'.format(1234567.8901234567890) == '1e+06'
554+
assert '{:.1g}'.format(1234567.8901234567890) == '1e+06'
555+
assert '{:.2g}'.format(1234567.8901234567890) == '1.2e+06'
556+
assert '{:.3g}'.format(1234567.8901234567890) == '1.23e+06'
557+
assert '{:.5g}'.format(1234567.8901234567890) == '1.2346e+06'
558+
assert '{:.6g}'.format(1234567.8901234567890) == '1.23457e+06'
559+
assert '{:.7g}'.format(1234567.8901234567890) == '1234568'
560+
assert '{:.8g}'.format(1234567.8901234567890) == '1234567.9'
561+
assert '{:G}'.format(123456.78901234567890) == '123457'
562+
assert '{:.0G}'.format(123456.78901234567890) == '1E+05'
563+
assert '{:.1G}'.format(123456.78901234567890) == '1E+05'
564+
assert '{:.2G}'.format(123456.78901234567890) == '1.2E+05'
565+
assert '{:G}'.format(1234567.8901234567890) == '1.23457E+06'
566+
assert '{:.0G}'.format(1234567.8901234567890) == '1E+06'
567+
assert '{:.1G}'.format(1234567.8901234567890) == '1E+06'
568+
assert '{:.2G}'.format(1234567.8901234567890) == '1.2E+06'
569+
assert '{:.3G}'.format(1234567.8901234567890) == '1.23E+06'
570+
assert '{:.5G}'.format(1234567.8901234567890) == '1.2346E+06'
571+
assert '{:.6G}'.format(1234567.8901234567890) == '1.23457E+06'
572+
assert '{:.7G}'.format(1234567.8901234567890) == '1234568'
573+
assert '{:.8G}'.format(1234567.8901234567890) == '1234567.9'
574+
assert '{:g}'.format(0.12345678901234567890) == '0.123457'
575+
assert '{:g}'.format(0.12345678901234567890e-1) == '0.0123457'
576+
assert '{:g}'.format(0.12345678901234567890e-2) == '0.00123457'
577+
assert '{:g}'.format(0.12345678901234567890e-3) == '0.000123457'
578+
assert '{:g}'.format(0.12345678901234567890e-4) == '1.23457e-05'
579+
assert '{:g}'.format(0.12345678901234567890e-5) == '1.23457e-06'
580+
assert '{:.6g}'.format(0.12345678901234567890e-5) == '1.23457e-06'
581+
assert '{:.10g}'.format(0.12345678901234567890e-5) == '1.23456789e-06'
582+
assert '{:.20g}'.format(0.12345678901234567890e-5) == '1.2345678901234567384e-06'
583+
assert '{:G}'.format(0.12345678901234567890) == '0.123457'
584+
assert '{:G}'.format(0.12345678901234567890E-1) == '0.0123457'
585+
assert '{:G}'.format(0.12345678901234567890E-2) == '0.00123457'
586+
assert '{:G}'.format(0.12345678901234567890E-3) == '0.000123457'
587+
assert '{:G}'.format(0.12345678901234567890E-4) == '1.23457E-05'
588+
assert '{:G}'.format(0.12345678901234567890E-5) == '1.23457E-06'
589+
assert '{:.6G}'.format(0.12345678901234567890E-5) == '1.23457E-06'
590+
assert '{:.10G}'.format(0.12345678901234567890E-5) == '1.23456789E-06'
591+
assert '{:.20G}'.format(0.12345678901234567890E-5) == '1.2345678901234567384E-06'
592+
assert '{:g}'.format(float('nan')) == 'nan'
593+
assert '{:g}'.format(float('-nan')) == 'nan'
594+
assert '{:G}'.format(float('nan')) == 'NAN'
595+
assert '{:g}'.format(float('inf')) == 'inf'
596+
assert '{:g}'.format(float('-inf')) == '-inf'
597+
assert '{:G}'.format(float('inf')) == 'INF'
598+
assert '{:.0g}'.format(1.020e-13) == '1e-13'
599+
assert '{:.0g}'.format(1.020e-13) == '1e-13'
600+
assert '{:.1g}'.format(1.020e-13) == '1e-13'
601+
assert '{:.2g}'.format(1.020e-13) == '1e-13'
602+
assert '{:.3g}'.format(1.020e-13) == '1.02e-13'
603+
assert '{:.4g}'.format(1.020e-13) == '1.02e-13'
604+
assert '{:.5g}'.format(1.020e-13) == '1.02e-13'
605+
assert '{:.6g}'.format(1.020e-13) == '1.02e-13'
606+
assert '{:.7g}'.format(1.020e-13) == '1.02e-13'
607+
assert '{:g}'.format(1.020e-13) == '1.02e-13'
608+
assert "{:g}".format(1.020e-4) == '0.000102'
480609

481610
# remove*fix test
482611
def test_removeprefix():

vm/src/builtins/pystr.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -180,7 +180,7 @@ impl PyIter for PyStrReverseIterator {
180180
break;
181181
}
182182
}
183-
start.unwrap_or(end - 4)
183+
start.unwrap_or_else(|| end.saturating_sub(4))
184184
};
185185

186186
let stored = zelf.position.swap(start);

vm/src/cformat.rs

Lines changed: 24 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ use crate::builtins::float::{try_bigint, IntoPyFloat, PyFloat};
44
use crate::builtins::int::{self, PyInt};
55
use crate::builtins::pystr::PyStr;
66
use crate::builtins::{memory::try_buffer_from_object, tuple, PyBytes};
7+
use crate::common::float_ops;
78
use crate::pyobject::{
89
BorrowValue, ItemProtocol, PyObjectRef, PyResult, TryFromObject, TypeProtocol,
910
};
@@ -76,7 +77,7 @@ enum CNumberType {
7677
#[derive(Debug, PartialEq)]
7778
enum CFloatType {
7879
Exponent(CFormatCase),
79-
PointDecimal,
80+
PointDecimal(CFormatCase),
8081
General(CFormatCase),
8182
}
8283

@@ -292,61 +293,43 @@ impl CFormatSpec {
292293
}
293294
}
294295

295-
fn normalize_float(&self, num: f64) -> (f64, i32) {
296-
let mut fraction = num;
297-
let mut exponent = 0;
298-
loop {
299-
if fraction >= 10.0 {
300-
fraction /= 10.0;
301-
exponent += 1;
302-
} else if fraction < 1.0 && fraction > 0.0 {
303-
fraction *= 10.0;
304-
exponent -= 1;
305-
} else {
306-
break;
307-
}
308-
}
309-
310-
(fraction, exponent)
311-
}
312-
313296
pub(crate) fn format_float(&self, num: f64) -> String {
314-
let sign_string = if num.is_sign_positive() {
315-
self.flags.sign_string()
316-
} else {
297+
let sign_string = if num.is_sign_negative() && !num.is_nan() {
317298
"-"
299+
} else {
300+
self.flags.sign_string()
318301
};
302+
319303
let precision = match self.precision {
320304
Some(CFormatQuantity::Amount(p)) => p,
321305
_ => 6,
322306
};
323307

324308
let magnitude_string = match &self.format_type {
325-
CFormatType::Float(CFloatType::PointDecimal) => {
309+
CFormatType::Float(CFloatType::PointDecimal(case)) => {
310+
let case = match case {
311+
CFormatCase::Lowercase => float_ops::Case::Lower,
312+
CFormatCase::Uppercase => float_ops::Case::Upper,
313+
};
326314
let magnitude = num.abs();
327-
format!("{:.*}", precision, magnitude)
315+
float_ops::format_fixed(precision, magnitude, case)
328316
}
329317
CFormatType::Float(CFloatType::Exponent(case)) => {
330-
let (fraction, exponent) = self.normalize_float(num.abs());
331318
let case = match case {
332-
CFormatCase::Lowercase => 'e',
333-
CFormatCase::Uppercase => 'E',
319+
CFormatCase::Lowercase => float_ops::Case::Lower,
320+
CFormatCase::Uppercase => float_ops::Case::Upper,
334321
};
335-
format!("{:.*}{}{:+03}", precision, fraction, case, exponent)
322+
let magnitude = num.abs();
323+
float_ops::format_exponent(precision, magnitude, case)
336324
}
337325
CFormatType::Float(CFloatType::General(case)) => {
338326
let precision = if precision == 0 { 1 } else { precision };
339-
let (fraction, exponent) = self.normalize_float(num.abs());
340-
if exponent < -4 || exponent >= (precision as i32) {
341-
let case = match case {
342-
CFormatCase::Lowercase => 'e',
343-
CFormatCase::Uppercase => 'E',
344-
};
345-
format!("{}{}{:+03}", fraction, case, exponent)
346-
} else {
347-
let magnitude = num.abs();
348-
format!("{}", magnitude)
349-
}
327+
let case = match case {
328+
CFormatCase::Lowercase => float_ops::Case::Lower,
329+
CFormatCase::Uppercase => float_ops::Case::Upper,
330+
};
331+
let magnitude = num.abs();
332+
float_ops::format_general(precision, magnitude, case)
350333
}
351334
_ => unreachable!(),
352335
};
@@ -1053,8 +1036,8 @@ where
10531036
'X' => CFormatType::Number(Hex(Uppercase)),
10541037
'e' => CFormatType::Float(Exponent(Lowercase)),
10551038
'E' => CFormatType::Float(Exponent(Uppercase)),
1056-
'f' => CFormatType::Float(PointDecimal),
1057-
'F' => CFormatType::Float(PointDecimal),
1039+
'f' => CFormatType::Float(PointDecimal(Lowercase)),
1040+
'F' => CFormatType::Float(PointDecimal(Uppercase)),
10581041
'g' => CFormatType::Float(General(Lowercase)),
10591042
'G' => CFormatType::Float(General(Uppercase)),
10601043
'c' => CFormatType::Character,

0 commit comments

Comments
 (0)