Skip to content

Commit 410f82f

Browse files
committed
Add support for the % format code for floats.
Contributes to #1656
1 parent 102b91a commit 410f82f

File tree

4 files changed

+74
-23
lines changed

4 files changed

+74
-23
lines changed

tests/snippets/strings.py

+18
Original file line numberDiff line numberDiff line change
@@ -431,3 +431,21 @@ def try_mutate_str():
431431
assert f'{1234567890.1234:_.2f}' == '1_234_567_890.12'
432432
with AssertRaises(ValueError, msg="Unknown format code 'd' for object of type 'float'"):
433433
f'{5.0:04d}'
434+
435+
# Test % formatting
436+
assert f'{10.0:%}' == '1000.000000%'
437+
assert f'{10.0:.2%}' == '1000.00%'
438+
assert f'{10.0:.8%}' == '1000.00000000%'
439+
assert f'{-10.0:%}' == '-1000.000000%'
440+
assert f'{-10.0:.2%}' == '-1000.00%'
441+
assert f'{-10.0:.8%}' == '-1000.00000000%'
442+
assert '{:%}'.format(float('nan')) == 'nan%'
443+
assert '{:%}'.format(float('NaN')) == 'nan%'
444+
assert '{:%}'.format(float('NAN')) == 'nan%'
445+
assert '{:.2%}'.format(float('nan')) == 'nan%'
446+
assert '{:%}'.format(float('inf')) == 'inf%'
447+
assert '{:%}'.format(float('Inf')) == 'inf%'
448+
assert '{:%}'.format(float('INF')) == 'inf%'
449+
assert '{:.2%}'.format(float('inf')) == 'inf%'
450+
with AssertRaises(ValueError, msg='Invalid format specifier'):
451+
f'{10.0:%3}'

vm/src/format.rs

+46-19
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,7 @@ pub enum FormatType {
9898
GeneralFormatUpper,
9999
FixedPointLower,
100100
FixedPointUpper,
101+
Percentage,
101102
}
102103

103104
#[derive(Debug, PartialEq)]
@@ -232,11 +233,12 @@ fn parse_format_type(text: &str) -> (Option<FormatType>, &str) {
232233
Some('g') => (Some(FormatType::GeneralFormatLower), chars.as_str()),
233234
Some('G') => (Some(FormatType::GeneralFormatUpper), chars.as_str()),
234235
Some('n') => (Some(FormatType::Number), chars.as_str()),
236+
Some('%') => (Some(FormatType::Percentage), chars.as_str()),
235237
_ => (None, text),
236238
}
237239
}
238240

239-
fn parse_format_spec(text: &str) -> FormatSpec {
241+
fn parse_format_spec(text: &str) -> Result<FormatSpec, &'static str> {
240242
let (preconversor, after_preconversor) = parse_preconversor(text);
241243
let (mut fill, mut align, after_align) = parse_fill_and_align(after_preconversor);
242244
let (sign, after_sign) = parse_sign(after_align);
@@ -245,14 +247,17 @@ fn parse_format_spec(text: &str) -> FormatSpec {
245247
let (width, after_width) = parse_number(after_zero);
246248
let (grouping_option, after_grouping_option) = parse_grouping_option(after_width);
247249
let (precision, after_precision) = parse_precision(after_grouping_option);
248-
let (format_type, _) = parse_format_type(after_precision);
250+
let (format_type, after_format_type) = parse_format_type(after_precision);
251+
if !after_format_type.is_empty() {
252+
return Err("Invalid format spec");
253+
}
249254

250255
if zero && fill.is_none() {
251256
fill.replace('0');
252257
align = align.or(Some(FormatAlign::AfterSign));
253258
}
254259

255-
FormatSpec {
260+
Ok(FormatSpec {
256261
preconversor,
257262
fill,
258263
align,
@@ -262,11 +267,11 @@ fn parse_format_spec(text: &str) -> FormatSpec {
262267
grouping_option,
263268
precision,
264269
format_type,
265-
}
270+
})
266271
}
267272

268273
impl FormatSpec {
269-
pub fn parse(text: &str) -> FormatSpec {
274+
pub fn parse(text: &str) -> Result<FormatSpec, &'static str> {
270275
parse_format_spec(text)
271276
}
272277

@@ -369,6 +374,11 @@ impl FormatSpec {
369374
Some(FormatType::ExponentLower) => {
370375
Err("Format code 'e' for object of type 'float' not implemented yet")
371376
}
377+
Some(FormatType::Percentage) => match magnitude {
378+
magnitude if magnitude.is_nan() => Ok("nan%".to_string()),
379+
magnitude if magnitude.is_infinite() => Ok("inf%".to_string()),
380+
_ => Ok(format!("{:.*}%", precision, magnitude * 100.0)),
381+
},
372382
None => {
373383
match magnitude {
374384
magnitude if magnitude.is_nan() => Ok("nan".to_string()),
@@ -443,6 +453,9 @@ impl FormatSpec {
443453
_ => Err("Unable to convert int to float"),
444454
}
445455
}
456+
Some(FormatType::Percentage) => {
457+
Err("Format code '%' for object of type 'int' not implemented yet")
458+
}
446459
None => Ok(magnitude.to_str_radix(10)),
447460
};
448461
if raw_magnitude_string_result.is_err() {
@@ -525,7 +538,7 @@ pub enum FormatParseError {
525538
impl FromStr for FormatSpec {
526539
type Err = &'static str;
527540
fn from_str(s: &str) -> Result<Self, Self::Err> {
528-
Ok(FormatSpec::parse(s))
541+
FormatSpec::parse(s)
529542
}
530543
}
531544

@@ -702,7 +715,7 @@ mod tests {
702715

703716
#[test]
704717
fn test_width_only() {
705-
let expected = FormatSpec {
718+
let expected = Ok(FormatSpec {
706719
preconversor: None,
707720
fill: None,
708721
align: None,
@@ -712,13 +725,13 @@ mod tests {
712725
grouping_option: None,
713726
precision: None,
714727
format_type: None,
715-
};
728+
});
716729
assert_eq!(parse_format_spec("33"), expected);
717730
}
718731

719732
#[test]
720733
fn test_fill_and_width() {
721-
let expected = FormatSpec {
734+
let expected = Ok(FormatSpec {
722735
preconversor: None,
723736
fill: Some('<'),
724737
align: Some(FormatAlign::Right),
@@ -728,13 +741,13 @@ mod tests {
728741
grouping_option: None,
729742
precision: None,
730743
format_type: None,
731-
};
744+
});
732745
assert_eq!(parse_format_spec("<>33"), expected);
733746
}
734747

735748
#[test]
736749
fn test_all() {
737-
let expected = FormatSpec {
750+
let expected = Ok(FormatSpec {
738751
preconversor: None,
739752
fill: Some('<'),
740753
align: Some(FormatAlign::Right),
@@ -744,38 +757,52 @@ mod tests {
744757
grouping_option: Some(FormatGrouping::Comma),
745758
precision: Some(11),
746759
format_type: Some(FormatType::Binary),
747-
};
760+
});
748761
assert_eq!(parse_format_spec("<>-#23,.11b"), expected);
749762
}
750763

751764
#[test]
752765
fn test_format_int() {
753766
assert_eq!(
754-
parse_format_spec("d").format_int(&BigInt::from_bytes_be(Sign::Plus, b"\x10")),
767+
parse_format_spec("d")
768+
.unwrap()
769+
.format_int(&BigInt::from_bytes_be(Sign::Plus, b"\x10")),
755770
Ok("16".to_string())
756771
);
757772
assert_eq!(
758-
parse_format_spec("x").format_int(&BigInt::from_bytes_be(Sign::Plus, b"\x10")),
773+
parse_format_spec("x")
774+
.unwrap()
775+
.format_int(&BigInt::from_bytes_be(Sign::Plus, b"\x10")),
759776
Ok("10".to_string())
760777
);
761778
assert_eq!(
762-
parse_format_spec("b").format_int(&BigInt::from_bytes_be(Sign::Plus, b"\x10")),
779+
parse_format_spec("b")
780+
.unwrap()
781+
.format_int(&BigInt::from_bytes_be(Sign::Plus, b"\x10")),
763782
Ok("10000".to_string())
764783
);
765784
assert_eq!(
766-
parse_format_spec("o").format_int(&BigInt::from_bytes_be(Sign::Plus, b"\x10")),
785+
parse_format_spec("o")
786+
.unwrap()
787+
.format_int(&BigInt::from_bytes_be(Sign::Plus, b"\x10")),
767788
Ok("20".to_string())
768789
);
769790
assert_eq!(
770-
parse_format_spec("+d").format_int(&BigInt::from_bytes_be(Sign::Plus, b"\x10")),
791+
parse_format_spec("+d")
792+
.unwrap()
793+
.format_int(&BigInt::from_bytes_be(Sign::Plus, b"\x10")),
771794
Ok("+16".to_string())
772795
);
773796
assert_eq!(
774-
parse_format_spec("^ 5d").format_int(&BigInt::from_bytes_be(Sign::Minus, b"\x10")),
797+
parse_format_spec("^ 5d")
798+
.unwrap()
799+
.format_int(&BigInt::from_bytes_be(Sign::Minus, b"\x10")),
775800
Ok(" -16 ".to_string())
776801
);
777802
assert_eq!(
778-
parse_format_spec("0>+#10x").format_int(&BigInt::from_bytes_be(Sign::Plus, b"\x10")),
803+
parse_format_spec("0>+#10x")
804+
.unwrap()
805+
.format_int(&BigInt::from_bytes_be(Sign::Plus, b"\x10")),
779806
Ok("00000+0x10".to_string())
780807
);
781808
}

vm/src/obj/objfloat.rs

+5-2
Original file line numberDiff line numberDiff line change
@@ -213,8 +213,11 @@ impl PyFloat {
213213

214214
#[pymethod(name = "__format__")]
215215
fn format(&self, spec: PyStringRef, vm: &VirtualMachine) -> PyResult<String> {
216-
let format_spec = FormatSpec::parse(spec.as_str());
217-
match format_spec.format_float(self.value) {
216+
let try_format = || {
217+
let format_spec = FormatSpec::parse(spec.as_str())?;
218+
format_spec.format_float(self.value)
219+
};
220+
match try_format() {
218221
Ok(string) => Ok(string),
219222
Err(err) => Err(vm.new_value_error(err.to_string())),
220223
}

vm/src/obj/objint.rs

+5-2
Original file line numberDiff line numberDiff line change
@@ -506,8 +506,11 @@ impl PyInt {
506506

507507
#[pymethod(name = "__format__")]
508508
fn format(&self, spec: PyStringRef, vm: &VirtualMachine) -> PyResult<String> {
509-
let format_spec = FormatSpec::parse(spec.as_str());
510-
match format_spec.format_int(&self.value) {
509+
let try_format = || {
510+
let format_spec = FormatSpec::parse(spec.as_str())?;
511+
format_spec.format_int(&self.value)
512+
};
513+
match try_format() {
511514
Ok(string) => Ok(string),
512515
Err(err) => Err(vm.new_value_error(err.to_string())),
513516
}

0 commit comments

Comments
 (0)