use crate::num::conversion::traits::{ToStringBase, WrappingFrom};
use alloc::string::String;
use alloc::vec::Vec;
use core::cmp::max;
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct GmpConversionSpec {
pub sign: u8,
pub plus: bool,
pub space: bool,
pub alt: bool,
pub left: bool,
pub group: bool,
pub fill: u8,
pub width: i64,
pub prec: Option<i64>,
pub type_chr: u8,
pub type_doubled: bool,
pub rnd_chr: u8,
pub conv: u8,
}
fn read_digits(c: u8, mut fmt: &[u8]) -> Option<(i64, &[u8])> {
let mut n = i64::from(c - b'0');
while let Some((&d, tail)) = fmt.split_first()
&& d.is_ascii_digit()
{
n = n.checked_mul(10)?.checked_add(i64::from(d - b'0'))?;
fmt = tail;
}
if n > const { i32::MAX as i64 } {
return None;
}
Some((n, fmt))
}
pub fn parse_gmp_conversion_spec<'a>(
mut fmt: &'a [u8],
star: &mut dyn FnMut() -> Option<i64>,
) -> Option<(GmpConversionSpec, &'a [u8])> {
let mut spec = GmpConversionSpec {
sign: 0,
plus: false,
space: false,
alt: false,
left: false,
group: false,
fill: b' ',
width: 0,
prec: None,
type_chr: 0,
type_doubled: false,
rnd_chr: 0,
conv: 0,
};
let mut in_width = true;
loop {
let (&c, tail) = fmt.split_first()?;
fmt = tail;
match c {
b'#' => spec.alt = true,
b'\'' => spec.group = true,
b'+' => {
spec.plus = true;
spec.sign = c;
}
b' ' => {
spec.space = true;
spec.sign = c;
}
b'-' => spec.left = true,
b'0' => {
if in_width {
spec.fill = b'0';
} else {
spec.prec = Some(0);
}
}
b'1'..=b'9' => {
let (n, tail) = read_digits(c, fmt)?;
fmt = tail;
if in_width {
spec.width = n;
} else {
spec.prec = Some(n);
}
}
b'.' => {
spec.prec = Some(-1);
in_width = false;
}
b'*' => {
let n = star()?;
if n.unsigned_abs() > const { i32::MAX as u64 } {
return None;
}
if in_width {
spec.width = if n < 0 {
spec.left = true;
-n
} else {
n
};
} else {
spec.prec = Some(max(0, n));
}
}
b'h' | b'l' => {
spec.type_chr = c;
spec.type_doubled = false;
if let Some((&d, tail)) = fmt.split_first()
&& d == c
{
spec.type_doubled = true;
fmt = tail;
}
}
b'j' | b'q' | b't' | b'z' | b'L' | b'Q' | b'M' | b'N' | b'Z' | b'P' => {
spec.type_chr = c;
spec.type_doubled = false;
}
b'R' => {
spec.type_chr = c;
spec.type_doubled = false;
if let Some((&d, tail)) = fmt.split_first() {
match d {
b'N' | b'D' | b'U' | b'Y' | b'Z' => {
spec.rnd_chr = d;
fmt = tail;
}
b'*' => return None,
_ => {}
}
}
}
b'F' => {
if spec.type_chr == b'R' {
spec.conv = c;
return Some((spec, fmt));
}
spec.type_chr = c;
spec.type_doubled = false;
}
b'd' | b'i' | b'u' | b'o' | b'x' | b'X' | b'e' | b'E' | b'f' | b'g' | b'G' | b'a'
| b'A' | b'b' | b'c' | b's' | b'p' | b'n' | b'm' => {
spec.conv = c;
return Some((spec, fmt));
}
_ => return None,
}
}
}
pub trait GmpFormatArg {
fn gmp_format(&self, spec: &GmpConversionSpec) -> Option<String>;
fn printf_int(&self) -> Option<i64> {
None
}
}
fn pad(out: &mut Vec<u8>, fill: u8, n: usize) {
out.resize(out.len() + n, fill);
}
fn justify(body: &[u8], spec: &GmpConversionSpec) -> Option<String> {
let width = usize::try_from(spec.width).unwrap_or(0);
let padding = width.saturating_sub(body.len());
let mut out = Vec::with_capacity(body.len() + padding);
if !spec.left {
pad(&mut out, b' ', padding);
}
out.extend_from_slice(body);
if spec.left {
pad(&mut out, b' ', padding);
}
String::from_utf8(out).ok()
}
const fn is_c_integer_spec(spec: &GmpConversionSpec) -> bool {
matches!(spec.conv, b'd' | b'i' | b'u' | b'o' | b'x' | b'X')
&& matches!(
spec.type_chr,
0 | b'h' | b'l' | b'j' | b'q' | b't' | b'z' | b'L'
)
}
fn format_c_integer(
neg: bool,
to_base: &dyn Fn(u8, bool) -> String,
spec: &GmpConversionSpec,
) -> Option<String> {
if !is_c_integer_spec(spec) {
return None;
}
let digits = match spec.conv {
b'o' => to_base(8, false),
b'x' => to_base(16, false),
b'X' => to_base(16, true),
_ => to_base(10, false),
};
let mut s = digits.as_bytes();
let sign = if neg {
b'-'
} else if spec.plus {
b'+'
} else if spec.space {
b' '
} else {
0
};
let sign_len = usize::from(sign != 0);
let prec = spec.prec.map_or(-1, |p| max(0, p));
if prec == 0 && s == b"0" {
s = b"";
}
let mut showbase: &[u8] = if spec.alt {
match spec.conv {
b'x' => b"0x",
b'X' => b"0X",
b'o' => b"0",
_ => b"",
}
} else {
b""
};
if s.first() == Some(&b'0') {
showbase = b"";
}
let zeros = usize::try_from(max(0, prec - i64::try_from(s.len()).ok()?)).ok()?;
let core = sign_len + showbase.len() + zeros + s.len();
let width = usize::try_from(spec.width).unwrap_or(0);
let padding = width.saturating_sub(core);
let zero_fill = spec.fill == b'0' && !spec.left && spec.prec.is_none();
let mut out = Vec::with_capacity(core + padding);
if !spec.left && !zero_fill {
pad(&mut out, b' ', padding);
}
if sign != 0 {
out.push(sign);
}
out.extend_from_slice(showbase);
if zero_fill {
pad(&mut out, b'0', padding);
}
pad(&mut out, b'0', zeros);
out.extend_from_slice(s);
if spec.left {
pad(&mut out, b' ', padding);
}
String::from_utf8(out).ok()
}
fn format_c_char_of_int(value: u64, spec: &GmpConversionSpec) -> Option<String> {
if spec.conv != b'c' || !matches!(spec.type_chr, 0 | b'h' | b'l') {
return None;
}
justify(&[u8::wrapping_from(value)], spec)
}
macro_rules! impl_gmp_format_arg_unsigned {
($t:ident) => {
impl GmpFormatArg for $t {
fn gmp_format(&self, spec: &GmpConversionSpec) -> Option<String> {
if spec.conv == b'c' {
return format_c_char_of_int(u64::wrapping_from(*self), spec);
}
format_c_integer(
false,
&|base, upper| {
if upper {
self.to_string_base_upper(base)
} else {
self.to_string_base(base)
}
},
spec,
)
}
fn printf_int(&self) -> Option<i64> {
i64::try_from(*self).ok()
}
}
};
}
apply_to_unsigneds!(impl_gmp_format_arg_unsigned);
macro_rules! impl_gmp_format_arg_signed {
($t:ident) => {
impl GmpFormatArg for $t {
fn gmp_format(&self, spec: &GmpConversionSpec) -> Option<String> {
if spec.conv == b'c' {
return format_c_char_of_int(u64::wrapping_from(self.unsigned_abs()), spec);
}
let abs = self.unsigned_abs();
format_c_integer(
*self < 0,
&|base, upper| {
if upper {
abs.to_string_base_upper(base)
} else {
abs.to_string_base(base)
}
},
spec,
)
}
fn printf_int(&self) -> Option<i64> {
i64::try_from(*self).ok()
}
}
};
}
apply_to_signeds!(impl_gmp_format_arg_signed);
impl GmpFormatArg for char {
fn gmp_format(&self, spec: &GmpConversionSpec) -> Option<String> {
if spec.conv != b'c' || spec.type_chr != 0 {
return None;
}
let mut buf = [0; 4];
justify(self.encode_utf8(&mut buf).as_bytes(), spec)
}
}
impl GmpFormatArg for &str {
fn gmp_format(&self, spec: &GmpConversionSpec) -> Option<String> {
if spec.conv != b's' || spec.type_chr != 0 {
return None;
}
let mut s = *self;
if let Some(prec) = spec.prec {
let prec = usize::try_from(max(0, prec)).ok()?;
if prec < s.len() {
if !s.is_char_boundary(prec) {
return None;
}
s = &s[..prec];
}
}
justify(s.as_bytes(), spec)
}
}
impl GmpFormatArg for String {
#[inline]
fn gmp_format(&self, spec: &GmpConversionSpec) -> Option<String> {
(&**self).gmp_format(spec)
}
}
pub fn gmp_format(fmt: &str, args: &[&dyn GmpFormatArg]) -> Option<String> {
let bytes = fmt.as_bytes();
let mut out = Vec::new();
let mut i = 0;
let mut next = 0;
while i < bytes.len() {
if bytes[i] == b'%' {
if bytes.get(i + 1) == Some(&b'%') {
out.push(b'%');
i += 2;
continue;
}
let (spec, rest) = {
let mut star = || {
let arg = args.get(next)?;
next += 1;
arg.printf_int()
};
parse_gmp_conversion_spec(&bytes[i + 1..], &mut star)?
};
let arg = args.get(next)?;
next += 1;
out.extend_from_slice(arg.gmp_format(&spec)?.as_bytes());
i = bytes.len() - rest.len();
} else {
out.push(bytes[i]);
i += 1;
}
}
String::from_utf8(out).ok()
}
#[macro_export]
macro_rules! gmp_format {
($fmt:expr $(, $args:expr)* $(,)?) => {
$crate::strings::gmp_format::gmp_format(
$fmt,
&[$(&$args as &dyn $crate::strings::gmp_format::GmpFormatArg),*],
)
};
}