Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion parser/src/fstring.rs
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ impl<'a> FStringParser<'a> {
}))
} else {
spec = Some(Box::new(Constant {
value: spec_expression.trim().to_string(),
value: spec_expression.to_string(),
}))
}
}
Expand Down
33 changes: 33 additions & 0 deletions tests/snippets/strings.py
Original file line number Diff line number Diff line change
Expand Up @@ -398,3 +398,36 @@ def try_mutate_str():
# >>> '{{literal}}'.format_map('foo')
# '{literal}'
assert '{{literal}}'.format_map('foo') == '{literal}'

# test formatting float values
assert f'{5:f}' == '5.000000'
assert f'{-5:f}' == '-5.000000'
assert f'{5.0:f}' == '5.000000'
assert f'{-5.0:f}' == '-5.000000'
assert f'{5:.2f}' == '5.00'
assert f'{5.0:.2f}' == '5.00'
assert f'{-5:.2f}' == '-5.00'
assert f'{-5.0:.2f}' == '-5.00'
assert f'{5.0:04f}' == '5.000000'
assert f'{5.1234:+f}' == '+5.123400'
assert f'{5.1234: f}' == ' 5.123400'
assert f'{5.1234:-f}' == '5.123400'
assert f'{-5.1234:-f}' == '-5.123400'
assert f'{1.0:+}' == '+1.0'
assert f'--{1.0:f>4}--' == '--f1.0--'
assert f'--{1.0:f<4}--' == '--1.0f--'
assert f'--{1.0:d^4}--' == '--1.0d--'
assert f'--{1.0:d^5}--' == '--d1.0d--'
assert f'--{1.1:f>6}--' == '--fff1.1--'
assert '{}'.format(float('nan')) == 'nan'
assert '{:f}'.format(float('nan')) == 'nan'
assert '{:f}'.format(float('-nan')) == 'nan'
assert '{:F}'.format(float('nan')) == 'NAN'
assert '{}'.format(float('inf')) == 'inf'
assert '{:f}'.format(float('inf')) == 'inf'
assert '{:f}'.format(float('-inf')) == '-inf'
assert '{:F}'.format(float('inf')) == 'INF'
assert f'{1234567890.1234:,.2f}' == '1,234,567,890.12'
assert f'{1234567890.1234:_.2f}' == '1_234_567_890.12'
with AssertRaises(ValueError, msg="Unknown format code 'd' for object of type 'float'"):
f'{5.0:04d}'
110 changes: 98 additions & 12 deletions vm/src/format.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
use num_bigint::{BigInt, Sign};
use num_traits::cast::ToPrimitive;
use num_traits::Signed;
use std::cmp;
use std::str::FromStr;
Expand Down Expand Up @@ -281,14 +282,22 @@ impl FormatSpec {
separator: char,
) -> String {
let mut result = String::new();
let mut remaining: usize = magnitude_string.len();
for c in magnitude_string.chars() {

// Don't add separators to the floating decimal point of numbers
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure exactly what the functionality of this is supposed to be, but you might want to do .splitn('.', 2) so that the splitting sort of has right associativity. Then you don't have to .collect() it and you can do let magnitude_integer_string = parts.next().unwrap(); and if let Some(part) = parts.next() { ... }

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is needed to prevent adding an incorrect separator to floats, as this is what it looked like before:

>>>>> '{:,.2f}'.format(1234567890.1234)
'1,234,567,890,.12'

With this change, it's only adding the separator to the integer portion of the number:

>>>>> '{:,.2f}'.format(1234567890.1234)
'1,234,567,890.12'

I'll change it to the way you suggested, as that is cleaner.

let mut parts = magnitude_string.splitn(2, '.');
let magnitude_integer_string = parts.next().unwrap();
let mut remaining: usize = magnitude_integer_string.len();
for c in magnitude_integer_string.chars() {
result.push(c);
remaining -= 1;
if remaining % interval == 0 && remaining > 0 {
result.push(separator);
}
}
if let Some(part) = parts.next() {
result.push('.');
result.push_str(part);
}
result
}

Expand All @@ -300,6 +309,7 @@ impl FormatSpec {
Some(FormatType::HexLower) => 4,
Some(FormatType::HexUpper) => 4,
Some(FormatType::Number) => 3,
Some(FormatType::FixedPointLower) | Some(FormatType::FixedPointUpper) => 3,
None => 3,
_ => panic!("Separators only valid for numbers!"),
}
Expand All @@ -321,8 +331,75 @@ impl FormatSpec {
}
}

pub fn format_float(&self, num: f64) -> Result<String, &'static str> {
let precision = self.precision.unwrap_or(6);
let magnitude = num.abs();
let raw_magnitude_string_result: Result<String, &'static str> = match self.format_type {
Some(FormatType::FixedPointUpper) => match magnitude {
magnitude if magnitude.is_nan() => Ok("NAN".to_string()),
magnitude if magnitude.is_infinite() => Ok("INF".to_string()),
_ => Ok(format!("{:.*}", precision, magnitude)),
},
Some(FormatType::FixedPointLower) => match magnitude {
magnitude if magnitude.is_nan() => Ok("nan".to_string()),
magnitude if magnitude.is_infinite() => Ok("inf".to_string()),
_ => Ok(format!("{:.*}", precision, magnitude)),
},
Some(FormatType::Decimal) => Err("Unknown format code 'd' for object of type 'float'"),
Some(FormatType::Binary) => Err("Unknown format code 'b' for object of type 'float'"),
Some(FormatType::Octal) => Err("Unknown format code 'o' for object of type 'float'"),
Some(FormatType::HexLower) => Err("Unknown format code 'x' for object of type 'float'"),
Some(FormatType::HexUpper) => Err("Unknown format code 'X' for object of type 'float'"),
Some(FormatType::String) => Err("Unknown format code 's' for object of type 'float'"),
Some(FormatType::Character) => {
Err("Unknown format code 'c' for object of type 'float'")
}
Some(FormatType::Number) => {
Err("Format code 'n' for object of type 'float' not implemented yet")
}
Some(FormatType::GeneralFormatUpper) => {
Err("Format code 'G' for object of type 'float' not implemented yet")
}
Some(FormatType::GeneralFormatLower) => {
Err("Format code 'g' for object of type 'float' not implemented yet")
}
Some(FormatType::ExponentUpper) => {
Err("Format code 'E' for object of type 'float' not implemented yet")
}
Some(FormatType::ExponentLower) => {
Err("Format code 'e' for object of type 'float' not implemented yet")
}
None => {
match magnitude {
magnitude if magnitude.is_nan() => Ok("nan".to_string()),
magnitude if magnitude.is_infinite() => Ok("inf".to_string()),
// Using the Debug format here to prevent the automatic conversion of floats
// ending in .0 to their integer representation (e.g., 1.0 -> 1)
_ => Ok(format!("{:?}", magnitude)),
}
}
};

if raw_magnitude_string_result.is_err() {
return raw_magnitude_string_result;
}

let magnitude_string = self.add_magnitude_separators(raw_magnitude_string_result.unwrap());
let format_sign = self.sign.unwrap_or(FormatSign::Minus);
let sign_str = if num.is_sign_negative() && !num.is_nan() {
"-"
} else {
match format_sign {
FormatSign::Plus => "+",
FormatSign::Minus => "",
FormatSign::MinusOrSpace => " ",
}
};

self.format_sign_and_align(magnitude_string, sign_str)
}

pub fn format_int(&self, num: &BigInt) -> Result<String, &'static str> {
let fill_char = self.fill.unwrap_or(' ');
let magnitude = num.abs();
let prefix = if self.alternate_form {
match self.format_type {
Expand Down Expand Up @@ -360,11 +437,11 @@ impl FormatSpec {
Some(FormatType::ExponentLower) => {
Err("Unknown format code 'e' for object of type 'int'")
}
Some(FormatType::FixedPointUpper) => {
Err("Unknown format code 'F' for object of type 'int'")
}
Some(FormatType::FixedPointLower) => {
Err("Unknown format code 'f' for object of type 'int'")
Some(FormatType::FixedPointUpper) | Some(FormatType::FixedPointLower) => {
match num.to_f64() {
Some(float) => return self.format_float(float),
_ => Err("Unable to convert int to float"),
}
}
None => Ok(magnitude.to_str_radix(10)),
};
Expand All @@ -376,10 +453,6 @@ impl FormatSpec {
prefix,
self.add_magnitude_separators(raw_magnitude_string_result.unwrap())
);
let align = self.align.unwrap_or(FormatAlign::Right);

// Use the byte length as the string length since we're in ascii
let num_chars = magnitude_string.len();

let format_sign = self.sign.unwrap_or(FormatSign::Minus);
let sign_str = match num.sign() {
Expand All @@ -391,6 +464,19 @@ impl FormatSpec {
},
};

self.format_sign_and_align(magnitude_string, sign_str)
}

fn format_sign_and_align(
&self,
magnitude_string: String,
sign_str: &str,
) -> Result<String, &'static str> {
let align = self.align.unwrap_or(FormatAlign::Right);

// Use the byte length as the string length since we're in ascii
let num_chars = magnitude_string.len();
let fill_char = self.fill.unwrap_or(' ');
let fill_chars_needed: i32 = self.width.map_or(0, |w| {
cmp::max(0, (w as i32) - (num_chars as i32) - (sign_str.len() as i32))
});
Expand Down
10 changes: 10 additions & 0 deletions vm/src/obj/objfloat.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ use super::objint::{self, PyInt, PyIntRef};
use super::objstr::{PyString, PyStringRef};
use super::objtype::{self, PyClassRef};
use crate::exceptions::PyBaseExceptionRef;
use crate::format::FormatSpec;
use crate::function::{OptionalArg, OptionalOption};
use crate::pyhash;
use crate::pyobject::{
Expand Down Expand Up @@ -210,6 +211,15 @@ impl PyFloat {
vm.ctx.new_bool(result)
}

#[pymethod(name = "__format__")]
fn format(&self, spec: PyStringRef, vm: &VirtualMachine) -> PyResult<String> {
let format_spec = FormatSpec::parse(spec.as_str());
match format_spec.format_float(self.value) {
Ok(string) => Ok(string),
Err(err) => Err(vm.new_value_error(err.to_string())),
}
}

#[pymethod(name = "__eq__")]
fn eq(&self, other: PyObjectRef, vm: &VirtualMachine) -> PyObjectRef {
self.cmp(other, |a, b| a == b, |a, b| int_eq(a, b), vm)
Expand Down