Implement lone-dot semantics for %-style format strings

This commit is contained in:
Charlie Marsh
2023-01-19 20:23:12 -05:00
parent ff90fe52ee
commit 115357ddd1
3 changed files with 136 additions and 38 deletions

View File

@@ -170,8 +170,6 @@ class EnUSNumberFormatting(BaseFormattingTest):
self._test_format("%f", -42, grouping=1, out='-42.000000')
self._test_format("%+f", -42, grouping=1, out='-42.000000')
# TODO: RUSTPYTHON
@unittest.expectedFailure
def test_grouping_and_padding(self):
self._test_format("%20.f", -42, grouping=1, out='-42'.rjust(20))
if self.sep:
@@ -197,8 +195,6 @@ class EnUSNumberFormatting(BaseFormattingTest):
self._test_format("%f", -42, grouping=0, out='-42.000000')
self._test_format("%+f", -42, grouping=0, out='-42.000000')
# TODO: RUSTPYTHON
@unittest.expectedFailure
def test_padding(self):
self._test_format("%20.f", -42, grouping=0, out='-42'.rjust(20))
self._test_format("%+10.f", -4200, grouping=0, out='-4200'.rjust(10))

View File

@@ -34,7 +34,7 @@ impl fmt::Display for CFormatError {
use CFormatErrorType::*;
match self.typ {
UnmatchedKeyParentheses => write!(f, "incomplete format key"),
CFormatErrorType::IncompleteFormat => write!(f, "incomplete format"),
IncompleteFormat => write!(f, "incomplete format"),
UnsupportedFormatChar(c) => write!(
f,
"unsupported format character '{}' ({:#x}) at index {}",
@@ -76,6 +76,18 @@ pub enum CFormatType {
String(CFormatPreconversor),
}
#[derive(Debug, PartialEq)]
pub enum CFormatPrecision {
Quantity(CFormatQuantity),
Dot,
}
impl From<CFormatQuantity> for CFormatPrecision {
fn from(quantity: CFormatQuantity) -> Self {
CFormatPrecision::Quantity(quantity)
}
}
bitflags! {
pub struct CConversionFlags: u32 {
const ALTERNATE_FORM = 0b0000_0001;
@@ -110,7 +122,7 @@ pub struct CFormatSpec {
pub mapping_key: Option<String>,
pub flags: CConversionFlags,
pub min_field_width: Option<CFormatQuantity>,
pub precision: Option<CFormatQuantity>,
pub precision: Option<CFormatPrecision>,
pub format_type: CFormatType,
pub format_char: char,
// chars_consumed: usize,
@@ -143,10 +155,6 @@ impl CFormatSpec {
let precision = parse_precision(iter)?;
consume_length(iter);
let (format_type, format_char) = parse_format_type(iter)?;
let precision = precision.or(match format_type {
CFormatType::Float(_) => Some(CFormatQuantity::Amount(6)),
_ => None,
});
Ok(CFormatSpec {
mapping_key,
@@ -169,20 +177,14 @@ impl CFormatSpec {
string: String,
fill_char: char,
num_prefix_chars: Option<usize>,
fill_with_precision: bool,
) -> String {
let target_width = if fill_with_precision {
&self.precision
} else {
&self.min_field_width
};
let mut num_chars = string.chars().count();
if let Some(num_prefix_chars) = num_prefix_chars {
num_chars += num_prefix_chars;
}
let num_chars = num_chars;
let width = match target_width {
let width = match &self.min_field_width {
Some(CFormatQuantity::Amount(width)) => cmp::max(width, &num_chars),
_ => &num_chars,
};
@@ -190,10 +192,7 @@ impl CFormatSpec {
let fill_string = CFormatSpec::compute_fill_string(fill_char, fill_chars_needed);
if !fill_string.is_empty() {
// Don't left-adjust if precision-filling: that will always be prepending 0s to %d
// arguments, the LEFT_ADJUST flag will be used by a later call to fill_string with
// the 0-filled string as the string param.
if !fill_with_precision && self.flags.contains(CConversionFlags::LEFT_ADJUST) {
if self.flags.contains(CConversionFlags::LEFT_ADJUST) {
format!("{string}{fill_string}")
} else {
format!("{fill_string}{string}")
@@ -203,19 +202,47 @@ impl CFormatSpec {
}
}
fn fill_string_with_precision(&self, string: String, fill_char: char) -> String {
let num_chars = string.chars().count();
let width = match &self.precision {
Some(CFormatPrecision::Quantity(CFormatQuantity::Amount(width))) => {
cmp::max(width, &num_chars)
}
_ => &num_chars,
};
let fill_chars_needed = width.saturating_sub(num_chars);
let fill_string = CFormatSpec::compute_fill_string(fill_char, fill_chars_needed);
if !fill_string.is_empty() {
// Don't left-adjust if precision-filling: that will always be prepending 0s to %d
// arguments, the LEFT_ADJUST flag will be used by a later call to fill_string with
// the 0-filled string as the string param.
format!("{fill_string}{string}")
} else {
string
}
}
fn format_string_with_precision(
&self,
string: String,
precision: Option<&CFormatQuantity>,
precision: Option<&CFormatPrecision>,
) -> String {
// truncate if needed
let string = match precision {
Some(CFormatQuantity::Amount(precision)) if string.chars().count() > *precision => {
Some(CFormatPrecision::Quantity(CFormatQuantity::Amount(precision)))
if string.chars().count() > *precision =>
{
string.chars().take(*precision).collect::<String>()
}
Some(CFormatPrecision::Dot) => {
// truncate to 0
String::new()
}
_ => string,
};
self.fill_string(string, ' ', None, false)
self.fill_string(string, ' ', None)
}
#[inline]
@@ -225,11 +252,16 @@ impl CFormatSpec {
#[inline]
pub fn format_char(&self, ch: char) -> String {
self.format_string_with_precision(ch.to_string(), Some(&CFormatQuantity::Amount(1)))
self.format_string_with_precision(
ch.to_string(),
Some(&(CFormatQuantity::Amount(1).into())),
)
}
pub fn format_bytes(&self, bytes: &[u8]) -> Vec<u8> {
let bytes = if let Some(CFormatQuantity::Amount(precision)) = self.precision {
let bytes = if let Some(CFormatPrecision::Quantity(CFormatQuantity::Amount(precision))) =
self.precision
{
&bytes[..cmp::min(bytes.len(), precision)]
} else {
bytes
@@ -282,7 +314,7 @@ impl CFormatSpec {
_ => self.flags.sign_string(),
};
let padded_magnitude_string = self.fill_string(magnitude_string, '0', None, true);
let padded_magnitude_string = self.fill_string_with_precision(magnitude_string, '0');
if self.flags.contains(CConversionFlags::ZERO_PAD) {
let fill_char = if !self.flags.contains(CConversionFlags::LEFT_ADJUST) {
@@ -298,7 +330,6 @@ impl CFormatSpec {
padded_magnitude_string,
fill_char,
Some(signed_prefix.chars().count()),
false
),
)
} else {
@@ -306,7 +337,6 @@ impl CFormatSpec {
format!("{sign_string}{prefix}{padded_magnitude_string}"),
' ',
None,
false,
)
}
}
@@ -318,9 +348,13 @@ impl CFormatSpec {
self.flags.sign_string()
};
let precision = match self.precision {
Some(CFormatQuantity::Amount(p)) => p,
_ => 6,
let precision = match &self.precision {
Some(CFormatPrecision::Quantity(quantity)) => match quantity {
CFormatQuantity::Amount(amount) => *amount,
CFormatQuantity::FromValuesTuple => 6,
},
Some(CFormatPrecision::Dot) => 0,
None => 6,
};
let magnitude_string = match &self.format_type {
@@ -381,11 +415,10 @@ impl CFormatSpec {
magnitude_string,
fill_char,
Some(sign_string.chars().count()),
false
)
)
} else {
self.fill_string(format!("{sign_string}{magnitude_string}"), ' ', None, false)
self.fill_string(format!("{sign_string}{magnitude_string}"), ' ', None)
}
}
}
@@ -510,7 +543,7 @@ where
Ok(None)
}
fn parse_precision<T, I>(iter: &mut ParseIter<I>) -> Result<Option<CFormatQuantity>, ParsingError>
fn parse_precision<T, I>(iter: &mut ParseIter<I>) -> Result<Option<CFormatPrecision>, ParsingError>
where
T: Into<char> + Copy,
I: Iterator<Item = T>,
@@ -518,7 +551,11 @@ where
if let Some(&(_, c)) = iter.peek() {
if c.into() == '.' {
iter.next().unwrap();
return parse_quantity(iter);
return Ok(Some(
parse_quantity(iter)?
.map(CFormatPrecision::Quantity)
.unwrap_or(CFormatPrecision::Dot),
));
}
}
Ok(None)
@@ -848,6 +885,20 @@ mod tests {
.format_string("Hello, World!".to_owned()),
"Hell ".to_owned()
);
assert_eq!(
"%.s"
.parse::<CFormatSpec>()
.unwrap()
.format_string("Hello, World!".to_owned()),
"".to_owned()
);
assert_eq!(
"%5.s"
.parse::<CFormatSpec>()
.unwrap()
.format_string("Hello, World!".to_owned()),
" ".to_owned()
);
}
#[test]
@@ -863,6 +914,13 @@ mod tests {
#[test]
fn test_parse_and_format_number() {
assert_eq!(
"%5d"
.parse::<CFormatSpec>()
.unwrap()
.format_number(&BigInt::from(27)),
" 27".to_owned()
);
assert_eq!(
"%05d"
.parse::<CFormatSpec>()
@@ -870,6 +928,13 @@ mod tests {
.format_number(&BigInt::from(27)),
"00027".to_owned()
);
assert_eq!(
"%.5d"
.parse::<CFormatSpec>()
.unwrap()
.format_number(&BigInt::from(27)),
"00027".to_owned()
);
assert_eq!(
"%+05d"
.parse::<CFormatSpec>()
@@ -927,6 +992,18 @@ mod tests {
"%f".parse::<CFormatSpec>().unwrap().format_float(1.2345),
"1.234500"
);
assert_eq!(
"%.2f".parse::<CFormatSpec>().unwrap().format_float(1.2345),
"1.23"
);
assert_eq!(
"%.f".parse::<CFormatSpec>().unwrap().format_float(1.2345),
"1"
);
assert_eq!(
"%+.f".parse::<CFormatSpec>().unwrap().format_float(1.2345),
"+1"
);
assert_eq!(
"%+f".parse::<CFormatSpec>().unwrap().format_float(1.2345),
"+1.234500"

View File

@@ -223,6 +223,31 @@ fn try_update_quantity_from_tuple<'a, I: Iterator<Item = &'a PyObjectRef>>(
}
}
fn try_update_precision_from_tuple<'a, I: Iterator<Item = &'a PyObjectRef>>(
vm: &VirtualMachine,
elements: &mut I,
p: &mut Option<CFormatPrecision>,
) -> PyResult<()> {
match p {
Some(CFormatPrecision::Quantity(CFormatQuantity::FromValuesTuple)) => match elements.next()
{
Some(width_obj) => {
if let Some(i) = width_obj.payload::<PyInt>() {
let i = i.try_to_primitive::<i32>(vm)?.unsigned_abs();
*p = Some(CFormatPrecision::Quantity(CFormatQuantity::Amount(
i as usize,
)));
Ok(())
} else {
Err(vm.new_type_error("* wants int".to_owned()))
}
}
None => Err(vm.new_type_error("not enough arguments for format string".to_owned())),
},
_ => Ok(()),
}
}
fn specifier_error(vm: &VirtualMachine) -> PyBaseExceptionRef {
vm.new_type_error("format requires a mapping".to_owned())
}
@@ -299,7 +324,7 @@ pub(crate) fn cformat_bytes(
CFormatPart::Literal(literal) => result.append(literal),
CFormatPart::Spec(spec) => {
try_update_quantity_from_tuple(vm, &mut value_iter, &mut spec.min_field_width)?;
try_update_quantity_from_tuple(vm, &mut value_iter, &mut spec.precision)?;
try_update_precision_from_tuple(vm, &mut value_iter, &mut spec.precision)?;
let value = match value_iter.next() {
Some(obj) => Ok(obj.clone()),
@@ -393,7 +418,7 @@ pub(crate) fn cformat_string(
CFormatPart::Literal(literal) => result.push_str(literal),
CFormatPart::Spec(spec) => {
try_update_quantity_from_tuple(vm, &mut value_iter, &mut spec.min_field_width)?;
try_update_quantity_from_tuple(vm, &mut value_iter, &mut spec.precision)?;
try_update_precision_from_tuple(vm, &mut value_iter, &mut spec.precision)?;
let value = match value_iter.next() {
Some(obj) => Ok(obj.clone()),