use std::{fmt, fmt::Write, iter, iter::Peekable, str, str::FromStr};
pub use monty_types::FormatFloat;
use monty_types::ResourceTracker;
use crate::{
bytecode::VM,
defer_drop,
exception_private::{ExcType, RunError, SimpleException},
expressions::ExprLoc,
heap::HeapData,
intern::StringId,
resource_checks::check_repeat_size,
types::{LongInt, PyTrait, Type, long_int::check_bits_str_digits_limit},
value::Value,
};
#[derive(Debug, Clone, Copy, Default, PartialEq, serde::Serialize, serde::Deserialize)]
pub enum ConversionFlag {
#[default]
None,
Str,
Repr,
Ascii,
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub enum FStringPart {
Literal(StringId),
Interpolation {
expr: Box<ExprLoc>,
conversion: ConversionFlag,
format_spec: Option<FormatSpec>,
debug_prefix: Option<StringId>,
},
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub enum FormatSpec {
Static(i64),
Dynamic(Vec<FStringPart>),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub enum Align {
Left,
Right,
Center,
SignAware,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub enum Sign {
Plus,
Minus,
Space,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub enum TypeChar {
B,
C,
D,
E,
EUpper,
F,
FUpper,
G,
GUpper,
N,
O,
S,
X,
XUpper,
Percent,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub enum Grouping {
Comma,
Underscore,
}
impl Grouping {
fn separator(self) -> char {
match self {
Self::Comma => ',',
Self::Underscore => '_',
}
}
}
impl Align {
pub fn from_char(c: char) -> Option<Self> {
match c {
'<' => Some(Self::Left),
'>' => Some(Self::Right),
'^' => Some(Self::Center),
'=' => Some(Self::SignAware),
_ => None,
}
}
}
impl Sign {
pub fn from_char(c: char) -> Option<Self> {
match c {
'+' => Some(Self::Plus),
'-' => Some(Self::Minus),
' ' => Some(Self::Space),
_ => None,
}
}
}
impl TypeChar {
pub fn from_char(c: char) -> Option<Self> {
match c {
'b' => Some(Self::B),
'c' => Some(Self::C),
'd' => Some(Self::D),
'e' => Some(Self::E),
'E' => Some(Self::EUpper),
'f' => Some(Self::F),
'F' => Some(Self::FUpper),
'g' => Some(Self::G),
'G' => Some(Self::GUpper),
'n' => Some(Self::N),
'o' => Some(Self::O),
's' => Some(Self::S),
'x' => Some(Self::X),
'X' => Some(Self::XUpper),
'%' => Some(Self::Percent),
_ => None,
}
}
pub fn as_char(self) -> char {
match self {
Self::B => 'b',
Self::C => 'c',
Self::D => 'd',
Self::E => 'e',
Self::EUpper => 'E',
Self::F => 'f',
Self::FUpper => 'F',
Self::G => 'g',
Self::GUpper => 'G',
Self::N => 'n',
Self::O => 'o',
Self::S => 's',
Self::X => 'x',
Self::XUpper => 'X',
Self::Percent => '%',
}
}
}
#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
pub struct ParsedFormatSpec {
pub fill: char,
pub align: Option<Align>,
pub sign: Option<Sign>,
pub alternate: bool,
pub z: bool,
pub zero_pad: bool,
pub width: usize,
pub grouping: Option<Grouping>,
pub frac_grouping: Option<Grouping>,
pub precision: Option<usize>,
pub type_char: Option<TypeChar>,
}
#[derive(Debug, Clone)]
pub enum ParseFormatSpecReason {
Malformed,
NumberOverflow,
MissingPrecision,
UnknownFormatCode(char),
GroupingConflict(String),
}
impl ParseFormatSpecReason {
fn grouping_conflict(g: Grouping, c: char) -> Self {
let msg = if (c == ',' || c == '_') && c != g.separator() {
"Cannot specify both ',' and '_'.".to_owned()
} else {
format!("Cannot specify '{}' with '{c}'.", g.separator())
};
Self::GroupingConflict(msg)
}
}
#[derive(Debug, Clone)]
pub struct ParseFormatSpecError {
pub spec: String,
pub reason: ParseFormatSpecReason,
}
impl ParseFormatSpecError {
fn new(spec: &str, reason: ParseFormatSpecReason) -> Self {
Self {
spec: spec.to_owned(),
reason,
}
}
pub fn needs_type_suffix(&self) -> bool {
matches!(
self.reason,
ParseFormatSpecReason::Malformed
| ParseFormatSpecReason::NumberOverflow
| ParseFormatSpecReason::UnknownFormatCode(_)
)
}
pub fn defer_to_runtime(&self) -> bool {
matches!(
self.reason,
ParseFormatSpecReason::MissingPrecision
| ParseFormatSpecReason::UnknownFormatCode(_)
| ParseFormatSpecReason::GroupingConflict(_)
)
}
}
impl fmt::Display for ParseFormatSpecError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match &self.reason {
ParseFormatSpecReason::Malformed => write!(f, "Invalid format specifier '{}'", self.spec),
ParseFormatSpecReason::NumberOverflow => {
write!(
f,
"Invalid format specifier '{}': width or precision overflows usize",
self.spec
)
}
ParseFormatSpecReason::MissingPrecision => f.write_str("Format specifier missing precision"),
ParseFormatSpecReason::UnknownFormatCode(c) => write!(f, "Unknown format code '{c}'"),
ParseFormatSpecReason::GroupingConflict(msg) => f.write_str(msg),
}
}
}
impl FromStr for ParsedFormatSpec {
type Err = ParseFormatSpecError;
fn from_str(spec: &str) -> Result<Self, Self::Err> {
if spec.is_empty() {
return Ok(Self {
fill: ' ',
..Default::default()
});
}
let mut result = Self {
fill: ' ',
..Default::default()
};
let mut chars = spec.chars().peekable();
let mut fill_specified = false;
if let Some(align) = spec.chars().nth(1).and_then(Align::from_char) {
result.fill = chars.next().unwrap_or(' ');
fill_specified = true;
chars.next();
result.align = Some(align);
} else {
result.align = chars.next_if_map(|c| Align::from_char(c).ok_or(c));
}
result.sign = chars.next_if_map(|c| Sign::from_char(c).ok_or(c));
result.z = chars.next_if_eq(&'z').is_some();
result.alternate = chars.next_if_eq(&'#').is_some();
if chars.next_if_eq(&'0').is_some() {
if !fill_specified {
result.fill = '0';
}
if result.align.is_none() {
result.zero_pad = true;
}
}
result.width = consume_decimal_usize(&mut chars)
.map_err(|()| ParseFormatSpecError::new(spec, ParseFormatSpecReason::NumberOverflow))?
.unwrap_or(0);
result.grouping = chars.next_if(|c| matches!(c, ',' | '_')).map(|c| match c {
',' => Grouping::Comma,
_ => Grouping::Underscore,
});
if chars.next_if_eq(&'.').is_some() {
result.precision = consume_decimal_usize(&mut chars)
.map_err(|()| ParseFormatSpecError::new(spec, ParseFormatSpecReason::NumberOverflow))?;
result.frac_grouping = chars.next_if(|c| matches!(c, ',' | '_')).map(|c| match c {
',' => Grouping::Comma,
_ => Grouping::Underscore,
});
if result.precision.is_none() && result.frac_grouping.is_none() {
return Err(ParseFormatSpecError::new(spec, ParseFormatSpecReason::MissingPrecision));
}
}
let type_pos = chars.next();
if chars.peek().is_some() {
return Err(ParseFormatSpecError::new(spec, ParseFormatSpecReason::Malformed));
}
if let Some(c) = type_pos {
if let Some(tc) = TypeChar::from_char(c) {
result.type_char = Some(tc);
} else {
let reason = match result.grouping {
Some(g) => ParseFormatSpecReason::grouping_conflict(g, c),
None => ParseFormatSpecReason::UnknownFormatCode(c),
};
return Err(ParseFormatSpecError::new(spec, reason));
}
}
Ok(result)
}
}
#[derive(Debug, Clone)]
pub enum FormatError {
InvalidAlignment(String),
Overflow(String),
ValueError(String),
}
impl fmt::Display for FormatError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::InvalidAlignment(msg) | Self::Overflow(msg) | Self::ValueError(msg) => {
write!(f, "{msg}")
}
}
}
}
pub fn format_with_spec(value: &Value, spec: &ParsedFormatSpec, vm: &mut VM<'_>) -> Result<String, RunError> {
let value_type = value.py_type(vm);
let coerced_bool;
let value = if let Value::Bool(b) = value
&& spec_has_directives(spec)
{
coerced_bool = Value::Int(i64::from(*b));
&coerced_bool
} else {
value
};
check_repeat_size(spec.fill.len_utf8(), spec.width, vm.heap.tracker())?;
let precision_scales_output = matches!(
spec.type_char,
Some(TypeChar::F | TypeChar::FUpper | TypeChar::E | TypeChar::EUpper | TypeChar::Percent)
) || (spec.alternate
&& matches!(
spec.type_char,
None | Some(TypeChar::G | TypeChar::GUpper | TypeChar::N)
));
if let Some(precision) = spec.precision
&& precision_scales_output
{
let numeric_finite = match value {
Value::Int(_) => true,
Value::Float(f) => f.is_finite(),
Value::Ref(id) => matches!(vm.heap.get(*id), HeapData::LongInt(_)),
_ => false,
};
if numeric_finite {
let separators = if spec.frac_grouping.is_some() { precision / 3 } else { 0 };
check_repeat_size(precision.saturating_add(separators), 1, vm.heap.tracker())?;
}
}
if value_type == Type::Str {
validate_string_spec(spec)?;
return Ok(format_string(value.to_str(vm)?, spec)?);
}
if let Some(grouping) = spec.grouping {
validate_grouping(grouping, spec.type_char, value_type)?;
}
if spec.precision.is_some() && formats_as_integer(spec.type_char, value_type) {
return Err(SimpleException::new_msg(
ExcType::ValueError,
"Precision not allowed in integer format specifier".to_owned(),
)
.into());
}
if let Some(c) = spec.type_char
&& !type_valid_for_value(spec.type_char, value_type)
{
return Err(SimpleException::new_msg(
ExcType::ValueError,
format!(
"Unknown format code '{}' for object of type '{}'",
c.as_char(),
value_type.name(vm.heap, vm.interns)
),
)
.into());
}
if spec.z && !formats_as_float(spec.type_char, value_type) {
let kind = if formats_as_integer(spec.type_char, value_type) {
"integer"
} else {
"string"
};
return Err(SimpleException::new_msg(
ExcType::ValueError,
format!("Negative zero coercion (z) not allowed in {kind} format specifier"),
)
.into());
}
if spec.sign.is_some() && spec.type_char == Some(TypeChar::C) && matches!(value_type, Type::Int | Type::Bool) {
return Err(SimpleException::new_msg(
ExcType::ValueError,
"Sign not allowed with integer format specifier 'c'".to_owned(),
)
.into());
}
if spec.alternate {
validate_alternate(spec.type_char, value_type)?;
}
if let Value::Ref(id) = value
&& let HeapData::LongInt(li) = vm.heap.get(*id)
{
return format_long_int(li, &value_type.name(vm.heap, vm.interns), spec, vm.heap.tracker());
}
match (value, spec.type_char) {
(Value::Int(n), None | Some(TypeChar::D | TypeChar::N)) => Ok(format_int(*n, spec)),
(Value::Int(n), Some(TypeChar::B)) => Ok(format_int_base(*n, 2, false, spec)?),
(Value::Int(n), Some(TypeChar::O)) => Ok(format_int_base(*n, 8, false, spec)?),
(Value::Int(n), Some(TypeChar::X)) => Ok(format_int_base(*n, 16, false, spec)?),
(Value::Int(n), Some(TypeChar::XUpper)) => Ok(format_int_base(*n, 16, true, spec)?),
(Value::Int(n), Some(TypeChar::C)) => Ok(format_char(*n, spec)?),
(Value::Float(f), None) if spec.precision.is_none() => Ok(format_float_default(*f, spec)),
(Value::Float(f), None | Some(TypeChar::G | TypeChar::GUpper | TypeChar::N)) => Ok(format_float_g(*f, spec)),
(Value::Float(f), Some(TypeChar::F | TypeChar::FUpper)) => Ok(format_float_f(*f, spec)),
(Value::Float(f), Some(TypeChar::E)) => Ok(format_float_e(*f, spec, false)),
(Value::Float(f), Some(TypeChar::EUpper)) => Ok(format_float_e(*f, spec, true)),
(Value::Float(f), Some(TypeChar::Percent)) => Ok(format_float_percent(*f, spec)),
(Value::Int(n), Some(TypeChar::F | TypeChar::FUpper)) => Ok(format_float_f(*n as f64, spec)),
(Value::Int(n), Some(TypeChar::E)) => Ok(format_float_e(*n as f64, spec, false)),
(Value::Int(n), Some(TypeChar::EUpper)) => Ok(format_float_e(*n as f64, spec, true)),
(Value::Int(n), Some(TypeChar::G | TypeChar::GUpper)) => Ok(format_float_g(*n as f64, spec)),
(Value::Int(n), Some(TypeChar::Percent)) => Ok(format_float_percent(*n as f64, spec)),
(_, None) => {
let s = value.py_str(vm)?;
defer_drop!(s, vm);
Ok(format_string(s.to_str(vm)?, spec)?)
}
(_, Some(c)) => Err(SimpleException::new_msg(
ExcType::ValueError,
format!(
"Unknown format code '{}' for object of type '{}'",
c.as_char(),
value_type.name(vm.heap, vm.interns)
),
)
.into()),
}
}
pub fn validate_string_spec(spec: &ParsedFormatSpec) -> Result<(), RunError> {
if let Some(grouping) = spec.grouping {
validate_grouping(grouping, spec.type_char, Type::Str)?;
}
if let Some(c) = spec.type_char
&& c != TypeChar::S
{
return Err(SimpleException::new_msg(
ExcType::ValueError,
format!("Unknown format code '{}' for object of type 'str'", c.as_char()),
)
.into());
}
if let Some(sign) = spec.sign {
let msg = match sign {
Sign::Space => "Space not allowed in string format specifier",
Sign::Plus | Sign::Minus => "Sign not allowed in string format specifier",
};
return Err(SimpleException::new_msg(ExcType::ValueError, msg.to_owned()).into());
}
if spec.z {
return Err(SimpleException::new_msg(
ExcType::ValueError,
"Negative zero coercion (z) not allowed in string format specifier".to_owned(),
)
.into());
}
if spec.alternate {
validate_alternate(spec.type_char, Type::Str)?;
}
if spec.align == Some(Align::SignAware) {
return Err(SimpleException::new_msg(
ExcType::ValueError,
"'=' alignment not allowed in string format specifier".to_owned(),
)
.into());
}
Ok(())
}
fn validate_grouping(grouping: Grouping, type_char: Option<TypeChar>, value_type: Type) -> Result<(), RunError> {
let allowed = match type_char {
None => matches!(value_type, Type::Int | Type::Bool | Type::Float),
Some(TypeChar::B | TypeChar::O | TypeChar::X | TypeChar::XUpper) => grouping == Grouping::Underscore,
Some(TypeChar::C | TypeChar::S | TypeChar::N) => false,
Some(_) => true,
};
if allowed {
Ok(())
} else {
let presentation = type_char.map_or('s', TypeChar::as_char);
Err(SimpleException::new_msg(
ExcType::ValueError,
format!("Cannot specify '{}' with '{presentation}'.", grouping.separator()),
)
.into())
}
}
fn type_valid_for_value(type_char: Option<TypeChar>, value_type: Type) -> bool {
let is_int = matches!(value_type, Type::Int | Type::Bool);
let is_num = is_int || value_type == Type::Float;
match type_char {
None => true,
Some(TypeChar::D | TypeChar::B | TypeChar::O | TypeChar::X | TypeChar::XUpper | TypeChar::C) => is_int,
Some(
TypeChar::E
| TypeChar::EUpper
| TypeChar::F
| TypeChar::FUpper
| TypeChar::G
| TypeChar::GUpper
| TypeChar::Percent
| TypeChar::N,
) => is_num,
Some(TypeChar::S) => value_type == Type::Str,
}
}
fn formats_as_integer(type_char: Option<TypeChar>, value_type: Type) -> bool {
let int_value = matches!(value_type, Type::Int | Type::Bool);
match type_char {
Some(TypeChar::D | TypeChar::B | TypeChar::O | TypeChar::X | TypeChar::XUpper | TypeChar::C | TypeChar::N) => {
int_value
}
None => int_value,
_ => false,
}
}
fn formats_as_float(type_char: Option<TypeChar>, value_type: Type) -> bool {
match type_char {
Some(
TypeChar::E
| TypeChar::EUpper
| TypeChar::F
| TypeChar::FUpper
| TypeChar::G
| TypeChar::GUpper
| TypeChar::Percent,
) => true,
Some(TypeChar::N) | None => value_type == Type::Float,
_ => false,
}
}
fn spec_has_directives(spec: &ParsedFormatSpec) -> bool {
spec.align.is_some()
|| spec.sign.is_some()
|| spec.alternate
|| spec.z
|| spec.zero_pad
|| spec.width != 0
|| spec.grouping.is_some()
|| spec.precision.is_some()
|| spec.type_char.is_some()
}
fn validate_alternate(type_char: Option<TypeChar>, value_type: Type) -> Result<(), RunError> {
let message = match type_char {
Some(TypeChar::C) => Some("Alternate form (#) not allowed with integer format specifier 'c'"),
Some(TypeChar::S) => Some("Alternate form (#) not allowed in string format specifier"),
None if !matches!(value_type, Type::Int | Type::Bool | Type::Float) => {
Some("Alternate form (#) not allowed in string format specifier")
}
_ => None,
};
match message {
Some(msg) => Err(SimpleException::new_msg(ExcType::ValueError, msg.to_owned()).into()),
None => Ok(()),
}
}
pub const MAX_ENCODED_FILL: u32 = 0xFF;
pub const MAX_ENCODED_WIDTH: usize = (1 << 20) - 1;
pub const MAX_ENCODED_PRECISION: usize = (1 << 21) - 2;
pub fn encode_format_spec(spec: &ParsedFormatSpec) -> Option<i64> {
if spec.frac_grouping.is_some() || spec.z {
return None;
}
let fill_code = u32::from(spec.fill);
if fill_code > MAX_ENCODED_FILL {
return None;
}
if spec.width > MAX_ENCODED_WIDTH {
return None;
}
if let Some(p) = spec.precision
&& p > MAX_ENCODED_PRECISION
{
return None;
}
let fill = i64::from(fill_code);
let align: i64 = spec.align.map_or(0, |a| match a {
Align::Left => 1,
Align::Right => 2,
Align::Center => 3,
Align::SignAware => 4,
});
let sign: i64 = spec.sign.map_or(0, |s| match s {
Sign::Plus => 1,
Sign::Minus => 2,
Sign::Space => 3,
});
let zero_pad = i64::from(spec.zero_pad);
let width = i64::try_from(spec.width).expect("width bounds-checked by MAX_ENCODED_WIDTH");
let precision: i64 = spec.precision.map_or(0, |p| {
i64::try_from(p).expect("precision bounds-checked by MAX_ENCODED_PRECISION") + 1
});
let type_char: i64 = spec.type_char.map_or(0, |c| match c {
TypeChar::B => 1,
TypeChar::C => 2,
TypeChar::D => 3,
TypeChar::E => 4,
TypeChar::EUpper => 5,
TypeChar::F => 6,
TypeChar::FUpper => 7,
TypeChar::G => 8,
TypeChar::GUpper => 9,
TypeChar::N => 10,
TypeChar::O => 11,
TypeChar::S => 12,
TypeChar::X => 13,
TypeChar::XUpper => 14,
TypeChar::Percent => 15,
});
let grouping: i64 = spec.grouping.map_or(0, |g| match g {
Grouping::Comma => 1,
Grouping::Underscore => 2,
});
let alternate = i64::from(spec.alternate);
Some(
fill | (align << 8)
| (sign << 11)
| (zero_pad << 13)
| (width << 14)
| (precision << 34)
| (type_char << 55)
| (grouping << 60)
| (alternate << 62),
)
}
pub fn decode_format_spec(encoded: i64) -> ParsedFormatSpec {
let encoded = encoded.cast_unsigned();
let fill = (encoded & 0xFF) as u8 as char;
let align_bits = (encoded >> 8) & 0x07;
let sign_bits = (encoded >> 11) & 0x03;
let zero_pad = ((encoded >> 13) & 0x01) != 0;
let width = ((encoded >> 14) & 0xF_FFFF) as usize;
let precision_raw = ((encoded >> 34) & 0x1F_FFFF) as usize;
let type_bits = ((encoded >> 55) & 0x1F) as u8;
let grouping_bits = (encoded >> 60) & 0x03;
let alternate = ((encoded >> 62) & 0x01) != 0;
let align = match align_bits {
1 => Some(Align::Left),
2 => Some(Align::Right),
3 => Some(Align::Center),
4 => Some(Align::SignAware),
_ => None,
};
let sign = match sign_bits {
1 => Some(Sign::Plus),
2 => Some(Sign::Minus),
3 => Some(Sign::Space),
_ => None,
};
let precision = if precision_raw == 0 {
None
} else {
Some(precision_raw - 1)
};
let type_char = match type_bits {
1 => Some(TypeChar::B),
2 => Some(TypeChar::C),
3 => Some(TypeChar::D),
4 => Some(TypeChar::E),
5 => Some(TypeChar::EUpper),
6 => Some(TypeChar::F),
7 => Some(TypeChar::FUpper),
8 => Some(TypeChar::G),
9 => Some(TypeChar::GUpper),
10 => Some(TypeChar::N),
11 => Some(TypeChar::O),
12 => Some(TypeChar::S),
13 => Some(TypeChar::X),
14 => Some(TypeChar::XUpper),
15 => Some(TypeChar::Percent),
_ => None,
};
let grouping = match grouping_bits {
1 => Some(Grouping::Comma),
2 => Some(Grouping::Underscore),
_ => None,
};
ParsedFormatSpec {
fill,
align,
sign,
alternate,
z: false,
zero_pad,
width,
grouping,
frac_grouping: None,
precision,
type_char,
}
}
pub fn format_string(value: &str, spec: &ParsedFormatSpec) -> Result<String, FormatError> {
let value = if let Some(prec) = spec.precision {
value.chars().take(prec).collect::<String>()
} else {
value.to_owned()
};
if spec.align == Some(Align::SignAware) {
return Err(FormatError::InvalidAlignment(
"'=' alignment not allowed in string format specifier".to_owned(),
));
}
let align = spec.align.unwrap_or(Align::Left);
Ok(pad_string(&value, spec.width, align, spec.fill))
}
pub fn format_int(n: i64, spec: &ParsedFormatSpec) -> String {
let is_negative = n < 0;
let abs_str = n.unsigned_abs().to_string();
let sign = numeric_sign(is_negative, &abs_str, spec);
pad_signed_numeric(sign, "", &abs_str, spec)
}
pub fn format_int_base(n: i64, base: u32, uppercase: bool, spec: &ParsedFormatSpec) -> Result<String, FormatError> {
let is_negative = n < 0;
let abs_val = n.unsigned_abs();
let (abs_str, base_prefix) = match (base, uppercase) {
(2, _) => (format!("{abs_val:b}"), "0b"),
(8, _) => (format!("{abs_val:o}"), "0o"),
(16, false) => (format!("{abs_val:x}"), "0x"),
(16, true) => (format!("{abs_val:X}"), "0X"),
_ => return Err(FormatError::ValueError("Invalid base".to_owned())),
};
let prefix = if spec.alternate { base_prefix } else { "" };
let sign = numeric_sign(is_negative, &abs_str, spec);
Ok(pad_signed_numeric(sign, prefix, &abs_str, spec))
}
fn format_long_int(
li: &LongInt,
value_type: &str,
spec: &ParsedFormatSpec,
tracker: &ResourceTracker,
) -> Result<String, RunError> {
let sign = if li.is_negative() {
"-"
} else {
positive_sign_prefix(spec.sign)
};
let magnitude = li.abs();
let radix = |base: u32, base_prefix: &'static str, uppercase: bool| -> Result<String, RunError> {
let max_digits = li.bits() / u64::from(base.trailing_zeros().max(1));
check_repeat_size(
1,
usize::try_from(max_digits).unwrap_or(usize::MAX).saturating_add(2),
tracker,
)?;
let mut digits = magnitude.inner().to_str_radix(base);
let prefix = if !spec.alternate {
""
} else if uppercase {
"0X"
} else {
base_prefix
};
if uppercase {
digits.make_ascii_uppercase();
}
Ok(pad_signed_numeric(sign, prefix, &digits, spec))
};
let as_float = || match li.to_f64() {
Some(f) if f.is_finite() => Ok(f),
_ => Err(RunError::from(SimpleException::new_msg(
ExcType::OverflowError,
"int too large to convert to float".to_owned(),
))),
};
match spec.type_char {
None | Some(TypeChar::D | TypeChar::N) => {
check_bits_str_digits_limit(li.bits())?;
radix(10, "", false)
}
Some(TypeChar::B) => radix(2, "0b", false),
Some(TypeChar::O) => radix(8, "0o", false),
Some(TypeChar::X) => radix(16, "0x", false),
Some(TypeChar::XUpper) => radix(16, "0x", true),
Some(TypeChar::F | TypeChar::FUpper) => Ok(format_float_f(as_float()?, spec)),
Some(TypeChar::E) => Ok(format_float_e(as_float()?, spec, false)),
Some(TypeChar::EUpper) => Ok(format_float_e(as_float()?, spec, true)),
Some(TypeChar::G | TypeChar::GUpper) => Ok(format_float_g(as_float()?, spec)),
Some(TypeChar::Percent) => Ok(format_float_percent(as_float()?, spec)),
Some(TypeChar::C) => Err(SimpleException::new_msg(
ExcType::OverflowError,
"Python int too large to convert to C long".to_owned(),
)
.into()),
Some(TypeChar::S) => Err(SimpleException::new_msg(
ExcType::ValueError,
format!("Unknown format code 's' for object of type '{value_type}'"),
)
.into()),
}
}
pub fn format_char(n: i64, spec: &ParsedFormatSpec) -> Result<String, FormatError> {
if !(0..=0x0010_FFFF).contains(&n) {
return Err(FormatError::Overflow("%c arg not in range(0x110000)".to_owned()));
}
let n_u32 = u32::try_from(n).expect("format_char n validated in 0..=0x10FFFF range");
let c = char::from_u32(n_u32).ok_or_else(|| FormatError::ValueError("Invalid Unicode code point".to_owned()))?;
let value = c.to_string();
let align = match spec.align.unwrap_or(Align::Right) {
Align::SignAware => Align::Right,
other => other,
};
Ok(pad_string(&value, spec.width, align, spec.fill))
}
pub fn format_float_f(f: f64, spec: &ParsedFormatSpec) -> String {
let is_negative = f.is_sign_negative() && !f.is_nan();
let uppercase = spec.type_char == Some(TypeChar::FUpper);
let abs_str = if let Some(word) = non_finite_repr(f, uppercase) {
word.to_owned()
} else {
let abs_val = f.abs();
let abs_str = fmt_float_fixed(abs_val, spec.precision.unwrap_or(6));
maybe_alternate_point(abs_str, abs_val, spec)
};
let sign = numeric_sign(is_negative, &abs_str, spec);
pad_signed_numeric(sign, "", &abs_str, spec)
}
pub fn format_float_e(f: f64, spec: &ParsedFormatSpec, uppercase: bool) -> String {
let is_negative = f.is_sign_negative() && !f.is_nan();
let abs_str = if let Some(word) = non_finite_repr(f, uppercase) {
word.to_owned()
} else {
let abs_val = f.abs();
let abs_str = fmt_float_exp(abs_val, spec.precision.unwrap_or(6), uppercase);
let abs_str = fix_exp_format(&abs_str);
maybe_alternate_point(abs_str, abs_val, spec)
};
let sign = numeric_sign(is_negative, &abs_str, spec);
pad_signed_numeric(sign, "", &abs_str, spec)
}
pub fn format_float_g(f: f64, spec: &ParsedFormatSpec) -> String {
let is_negative = f.is_sign_negative() && !f.is_nan();
let uppercase = spec.type_char == Some(TypeChar::GUpper);
if let Some(word) = non_finite_repr(f, uppercase) {
let sign = numeric_sign(is_negative, word, spec);
return pad_signed_numeric(sign, "", word, spec);
}
let precision = spec.precision.unwrap_or(6).max(1);
let abs_val = f.abs();
let exp = if abs_val == 0.0 {
0
} else {
let mantissa_digits = precision.saturating_sub(1).min(MAX_FMT_PRECISION_EXP);
let sci = format!("{abs_val:.mantissa_digits$e}");
sci[sci.find('e').map_or(sci.len(), |i| i + 1)..]
.parse::<i32>()
.unwrap_or(0)
};
let prec_i32 = i32::try_from(precision).unwrap_or(i32::MAX);
let is_default = spec.type_char.is_none();
let sci_threshold = if is_default { prec_i32 - 1 } else { prec_i32 };
let alternate_g = spec.alternate;
let abs_str = if exp < -4 || exp >= sci_threshold {
let exp_prec = precision.saturating_sub(1);
if alternate_g {
fix_exp_format(&fmt_float_exp(abs_val, exp_prec, uppercase))
} else {
strip_trailing_zeros_exp(&fmt_float_exp(abs_val, exp_prec.min(MAX_FMT_PRECISION_EXP), uppercase))
}
} else {
let sig_digits_i32 = (prec_i32 - exp - 1).max(0);
let sig_digits = usize::try_from(sig_digits_i32).expect("sig_digits guaranteed non-negative");
if alternate_g {
fmt_float_fixed(abs_val, sig_digits)
} else {
let cap = sig_digits.min(MAX_FMT_PRECISION);
strip_trailing_zeros(&format!("{abs_val:.cap$}"))
}
};
let abs_str = if alternate_g {
maybe_alternate_point(abs_str, abs_val, spec)
} else {
abs_str
};
let abs_str = if is_default && !abs_str.contains(['.', 'e', 'E']) {
format!("{abs_str}.0")
} else {
abs_str
};
let sign = numeric_sign(is_negative, &abs_str, spec);
pad_signed_numeric(sign, "", &abs_str, spec)
}
fn format_float_default(f: f64, spec: &ParsedFormatSpec) -> String {
let is_negative = f.is_sign_negative() && !f.is_nan();
let abs_val = f.abs();
let abs_str = maybe_alternate_point(FormatFloat(abs_val).to_string(), abs_val, spec);
let sign = numeric_sign(is_negative, &abs_str, spec);
pad_signed_numeric(sign, "", &abs_str, spec)
}
pub fn ascii_escape(s: &str) -> String {
let mut result = String::new();
for c in s.chars() {
if c.is_ascii() {
result.push(c);
} else {
let code = c as u32;
if code <= 0xFF {
write!(result, "\\x{code:02x}")
} else if code <= 0xFFFF {
write!(result, "\\u{code:04x}")
} else {
write!(result, "\\U{code:08x}")
}
.expect("string write should be infallible");
}
}
result
}
pub fn format_float_percent(f: f64, spec: &ParsedFormatSpec) -> String {
let percent_val = f * 100.0;
let is_negative = percent_val.is_sign_negative() && !percent_val.is_nan();
let abs_str = if let Some(word) = non_finite_repr(percent_val, false) {
format!("{word}%")
} else {
let abs_val = percent_val.abs();
let abs_str = format!("{}%", fmt_float_fixed(abs_val, spec.precision.unwrap_or(6)));
maybe_alternate_point(abs_str, abs_val, spec)
};
let sign = numeric_sign(is_negative, &abs_str, spec);
pad_signed_numeric(sign, "", &abs_str, spec)
}
fn non_finite_repr(value: f64, uppercase: bool) -> Option<&'static str> {
if value.is_nan() {
Some(if uppercase { "NAN" } else { "nan" })
} else if value.is_infinite() {
Some(if uppercase { "INF" } else { "inf" })
} else {
None
}
}
fn positive_sign_prefix(sign: Option<Sign>) -> &'static str {
match sign {
Some(Sign::Plus) => "+",
Some(Sign::Space) => " ",
None | Some(Sign::Minus) => "",
}
}
fn numeric_sign(is_negative: bool, abs_str: &str, spec: &ParsedFormatSpec) -> &'static str {
if is_negative && !(spec.z && is_rounded_zero(abs_str)) {
"-"
} else {
positive_sign_prefix(spec.sign)
}
}
fn is_rounded_zero(abs_str: &str) -> bool {
let mut saw_digit = false;
for b in abs_str.bytes() {
if b.is_ascii_digit() {
saw_digit = true;
if b != b'0' {
return false;
}
}
}
saw_digit
}
fn maybe_alternate_point(abs_str: String, abs_val: f64, spec: &ParsedFormatSpec) -> String {
if !spec.alternate || !abs_val.is_finite() {
return abs_str;
}
let marker = abs_str.find(['e', 'E', '%']).unwrap_or(abs_str.len());
if abs_str[..marker].contains('.') {
abs_str
} else {
format!("{}.{}", &abs_str[..marker], &abs_str[marker..])
}
}
fn pad_signed_numeric(sign: &str, prefix: &str, abs_str: &str, spec: &ParsedFormatSpec) -> String {
let frac_grouped;
let abs_str = if let Some(g) = spec.frac_grouping {
frac_grouped = insert_frac_grouping(abs_str, g.separator());
frac_grouped.as_str()
} else {
abs_str
};
let align = spec.align.unwrap_or(Align::Right);
match spec.grouping {
None => pad_signed_ungrouped(sign, prefix, abs_str, align, spec),
Some(grouping) => pad_signed_grouped(sign, prefix, abs_str, align, grouping, spec),
}
}
fn insert_frac_grouping(s: &str, sep: char) -> String {
let Some(dot) = s.find('.') else {
return s.to_owned();
};
let after = dot + 1;
let frac_len = s[after..]
.find(|c: char| !c.is_ascii_digit())
.unwrap_or(s.len() - after);
let mut out = String::with_capacity(s.len() + frac_len / 3);
out.push_str(&s[..after]);
for (i, c) in s[after..after + frac_len].chars().enumerate() {
if i > 0 && i.is_multiple_of(3) {
out.push(sep);
}
out.push(c);
}
out.push_str(&s[after + frac_len..]);
out
}
fn pad_signed_ungrouped(sign: &str, prefix: &str, abs_str: &str, align: Align, spec: &ParsedFormatSpec) -> String {
if spec.zero_pad || align == Align::SignAware {
let fill = if spec.zero_pad { '0' } else { spec.fill };
let total_len = sign.len() + prefix.len() + abs_str.len();
if spec.width > total_len {
let padding = spec.width - total_len;
let pad_str: String = iter::repeat_n(fill, padding).collect();
format!("{sign}{prefix}{pad_str}{abs_str}")
} else {
format!("{sign}{prefix}{abs_str}")
}
} else {
let value = format!("{sign}{prefix}{abs_str}");
pad_string(&value, spec.width, align, spec.fill)
}
}
fn pad_signed_grouped(
sign: &str,
prefix: &str,
abs_str: &str,
align: Align,
grouping: Grouping,
spec: &ParsedFormatSpec,
) -> String {
let sep = grouping.separator();
let is_base = matches!(
spec.type_char,
Some(TypeChar::B | TypeChar::O | TypeChar::X | TypeChar::XUpper)
);
let group_size = if is_base { 4 } else { 3 };
let (int_digits, suffix) = if is_base {
(abs_str, "")
} else {
let end = abs_str.find(|c: char| !c.is_ascii_digit()).unwrap_or(abs_str.len());
abs_str.split_at(end)
};
if int_digits.is_empty() {
return pad_signed_ungrouped(sign, prefix, abs_str, align, spec);
}
if spec.zero_pad {
let reserved = sign.len() + prefix.len() + suffix.len();
let min_int_width = spec.width.saturating_sub(reserved);
let grouped = insert_grouping(int_digits, group_size, sep, min_int_width);
format!("{sign}{prefix}{grouped}{suffix}")
} else if align == Align::SignAware {
let body = format!("{}{suffix}", insert_grouping(int_digits, group_size, sep, 0));
let total_len = sign.len() + prefix.len() + body.len();
if spec.width > total_len {
let pad_str: String = iter::repeat_n(spec.fill, spec.width - total_len).collect();
format!("{sign}{prefix}{pad_str}{body}")
} else {
format!("{sign}{prefix}{body}")
}
} else {
let grouped = insert_grouping(int_digits, group_size, sep, 0);
let value = format!("{sign}{prefix}{grouped}{suffix}");
pad_string(&value, spec.width, align, spec.fill)
}
}
fn insert_grouping(digits: &str, group_size: usize, sep: char, min_width: usize) -> String {
let ndigits = digits.len();
let mut total = ndigits;
while total + total.saturating_sub(1) / group_size < min_width {
total += 1;
}
let zeros = total - ndigits;
let mut out = String::with_capacity(total + total.saturating_sub(1) / group_size);
let mut digit_chars = digits.chars();
for i in 0..total {
if i > 0 && (total - i).is_multiple_of(group_size) {
out.push(sep);
}
out.push(if i < zeros {
'0'
} else {
digit_chars.next().expect("digit_chars yields exactly ndigits items")
});
}
out
}
fn consume_decimal_usize(chars: &mut Peekable<impl Iterator<Item = char>>) -> Result<Option<usize>, ()> {
let mut value: Option<usize> = None;
while let Some(c) = chars.next_if(char::is_ascii_digit) {
let digit = c.to_digit(10).expect("char::is_ascii_digit guarantees a 0-9 digit") as usize;
let next = value
.unwrap_or(0)
.checked_mul(10)
.and_then(|n| n.checked_add(digit))
.ok_or(())?;
value = Some(next);
}
Ok(value)
}
const MAX_FMT_PRECISION: usize = u16::MAX as usize;
const MAX_FMT_PRECISION_EXP: usize = (u16::MAX as usize) - 1;
fn fmt_float_fixed(abs_val: f64, precision: usize) -> String {
if precision <= MAX_FMT_PRECISION || !abs_val.is_finite() {
return format!("{abs_val:.precision$}");
}
let mut s = format!("{abs_val:.MAX_FMT_PRECISION$}");
s.extend(iter::repeat_n('0', precision - MAX_FMT_PRECISION));
s
}
fn fmt_float_exp(abs_val: f64, precision: usize, uppercase: bool) -> String {
if precision <= MAX_FMT_PRECISION_EXP || !abs_val.is_finite() {
return if uppercase {
format!("{abs_val:.precision$E}")
} else {
format!("{abs_val:.precision$e}")
};
}
let base = if uppercase {
format!("{abs_val:.MAX_FMT_PRECISION_EXP$E}")
} else {
format!("{abs_val:.MAX_FMT_PRECISION_EXP$e}")
};
let extra = precision - MAX_FMT_PRECISION_EXP;
if let Some(e_pos) = base.find(['e', 'E']) {
let (mantissa, exp_part) = base.split_at(e_pos);
let zeros: String = iter::repeat_n('0', extra).collect();
format!("{mantissa}{zeros}{exp_part}")
} else {
base
}
}
fn pad_string(value: &str, width: usize, align: Align, fill: char) -> String {
debug_assert!(
align != Align::SignAware,
"pad_string received Align::SignAware; callers must handle `=` themselves \
(numeric formatters via pad_signed_numeric, format_char by mapping to Right)"
);
let value_len = value.chars().count();
if width <= value_len {
return value.to_owned();
}
let padding = width - value_len;
match align {
Align::Left => {
let mut s = value.to_owned();
for _ in 0..padding {
s.push(fill);
}
s
}
Align::Right => {
let mut s = String::new();
for _ in 0..padding {
s.push(fill);
}
s.push_str(value);
s
}
Align::Center => {
let left_pad = padding / 2;
let right_pad = padding - left_pad;
let mut s = String::new();
for _ in 0..left_pad {
s.push(fill);
}
s.push_str(value);
for _ in 0..right_pad {
s.push(fill);
}
s
}
Align::SignAware => value.to_owned(),
}
}
fn strip_trailing_zeros(s: &str) -> String {
if !s.contains('.') {
return s.to_owned();
}
let trimmed = s.trim_end_matches('0');
if let Some(stripped) = trimmed.strip_suffix('.') {
stripped.to_owned()
} else {
trimmed.to_owned()
}
}
fn strip_trailing_zeros_exp(s: &str) -> String {
if let Some(e_pos) = s.find(['e', 'E']) {
let (mantissa, exp_part) = s.split_at(e_pos);
let trimmed_mantissa = strip_trailing_zeros(mantissa);
let fixed_exp = fix_exp_format(exp_part);
format!("{trimmed_mantissa}{fixed_exp}")
} else {
strip_trailing_zeros(s)
}
}
fn fix_exp_format(s: &str) -> String {
let Some(e_pos) = s.find(['e', 'E']) else {
return s.to_owned();
};
let (before_e, e_and_rest) = s.split_at(e_pos);
let e_char = e_and_rest.chars().next().unwrap();
let exp_part = &e_and_rest[1..];
let (sign, digits) = if let Some(stripped) = exp_part.strip_prefix('-') {
('-', stripped)
} else if let Some(stripped) = exp_part.strip_prefix('+') {
('+', stripped)
} else {
('+', exp_part)
};
let padded_digits = if digits.len() < 2 {
format!("{digits:0>2}")
} else {
digits.to_owned()
};
format!("{before_e}{e_char}{sign}{padded_digits}")
}