From 69196cf35838e5157d6a262819923014e060c808 Mon Sep 17 00:00:00 2001 From: qjerome Date: Thu, 3 Jul 2025 09:20:45 +0200 Subject: [PATCH 1/5] =?UTF-8?q?add:=C2=A0first=20implementation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 1 + Cargo.lock | 206 ++++++++++++++++++++++++++++++++++ Cargo.toml | 9 ++ src/lib.rs | 273 ++++++++++++++++++++++++++++++++++++++++++++++ src/rust_fmt.pest | 28 +++++ 5 files changed, 517 insertions(+) create mode 100644 .gitignore create mode 100644 Cargo.lock create mode 100644 Cargo.toml create mode 100644 src/lib.rs create mode 100644 src/rust_fmt.pest diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ea8c4bf --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/target diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..2c9b94f --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,206 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "cfg-if" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9555578bc9e57714c812a1f84e4fc5b4d21fcb063490c624de019f7464c91268" + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + +[[package]] +name = "dyf" +version = "0.1.0" +dependencies = [ + "pest", + "pest_derive", + "thiserror", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "libc" +version = "0.2.174" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1171693293099992e19cddea4e8b849964e9846f4acee11b3948bcc337be8776" + +[[package]] +name = "memchr" +version = "2.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0" + +[[package]] +name = "pest" +version = "2.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1db05f56d34358a8b1066f67cbb203ee3e7ed2ba674a6263a1d5ec6db2204323" +dependencies = [ + "memchr", + "thiserror", + "ucd-trie", +] + +[[package]] +name = "pest_derive" +version = "2.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb056d9e8ea77922845ec74a1c4e8fb17e7c218cc4fc11a15c5d25e189aa40bc" +dependencies = [ + "pest", + "pest_generator", +] + +[[package]] +name = "pest_generator" +version = "2.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87e404e638f781eb3202dc82db6760c8ae8a1eeef7fb3fa8264b2ef280504966" +dependencies = [ + "pest", + "pest_meta", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "pest_meta" +version = "2.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edd1101f170f5903fde0914f899bb503d9ff5271d7ba76bbb70bea63690cc0d5" +dependencies = [ + "pest", + "sha2", +] + +[[package]] +name = "proc-macro2" +version = "1.0.95" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "syn" +version = "2.0.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17b6f705963418cdb9927482fa304bc562ece2fdd4f616084c50b7023b435a40" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "thiserror" +version = "2.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "typenum" +version = "1.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f" + +[[package]] +name = "ucd-trie" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971" + +[[package]] +name = "unicode-ident" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..52d7277 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "dyf" +version = "0.1.0" +edition = "2024" + +[dependencies] +pest = "2.8.1" +pest_derive = "2.8.1" +thiserror = "2.0.12" diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..ee9f68a --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,273 @@ +use std::{ + borrow::Cow, + fmt::{Debug, Display}, +}; + +use pest::{Parser, iterators::Pair}; +use pest_derive::Parser; +use thiserror::Error; + +#[derive(Parser)] +#[grammar = "rust_fmt.pest"] // relative to src +struct FmtParser; + +#[derive(Debug)] +pub enum FmtType { + Unsupported, + Default, + LowerHex, + UpperHex, + Octal, + Ptr, + Bin, + LowExp, + UpperExp, +} + +#[derive(Debug)] +struct Format<'s> { + fmt: Cow<'s, str>, + ty: FmtType, +} + +#[derive(Debug, Error)] +pub enum Error { + #[error("unsupported format")] + UnsupportedFormat, + #[error("format strings don't match with number of arguments")] + FormatVsArgs, +} + +pub trait DynDisplay: Display + Debug { + fn fmt(&self, t: &FmtType) -> Result; +} + +macro_rules! impl_dyn_display_int { + ($ty: ty) => { + impl DynDisplay for $ty { + #[inline] + fn fmt(&self, t: &FmtType) -> Result { + match t { + FmtType::Default => Ok(format!("{}", self)), + FmtType::LowerHex => Ok(format!("{:x}", self)), + FmtType::UpperHex => Ok(format!("{:X}", self)), + FmtType::Bin => Ok(format!("{:b}", self)), + FmtType::Ptr => Ok(format!("{:p}", self)), + FmtType::Octal => Ok(format!("{:o}", self)), + FmtType::LowExp => Ok(format!("{:e}", self)), + FmtType::UpperExp => Ok(format!("{:e}", self)), + _ => Err(Error::UnsupportedFormat), + } + } + } + }; +} + +impl DynDisplay for Box { + fn fmt(&self, t: &FmtType) -> Result { + DynDisplay::fmt(self.as_ref(), t) + } +} + +// Signed integers +impl_dyn_display_int!(i8); +impl_dyn_display_int!(i16); +impl_dyn_display_int!(i32); +impl_dyn_display_int!(i64); +impl_dyn_display_int!(i128); +impl_dyn_display_int!(isize); + +// Unsigned integers +impl_dyn_display_int!(u8); +impl_dyn_display_int!(u16); +impl_dyn_display_int!(u32); +impl_dyn_display_int!(u64); +impl_dyn_display_int!(u128); +impl_dyn_display_int!(usize); + +impl<'s> Format<'s> { + fn from_pair(pairs: Pair<'s, Rule>) -> Self { + let ty = match pairs.as_str() { + "{}" => FmtType::Default, + "{:x}" => FmtType::LowerHex, + "{:X}" => FmtType::UpperHex, + "{:o}" => FmtType::Octal, + "{:b}" => FmtType::Bin, + "{:p}" => FmtType::Ptr, + "{:e}" => FmtType::LowExp, + "{:E}" => FmtType::UpperExp, + + _ => FmtType::Unsupported, + }; + + Self { + fmt: Cow::Borrowed(pairs.as_str()), + ty, + } + } + + fn fmt(&self, v: &D) -> Result { + DynDisplay::fmt(v, &self.ty) + } +} + +#[derive(Debug)] +pub struct FormatString<'s> { + s: Cow<'s, str>, + fmts: Vec>, +} + +impl<'s> FormatString<'s> { + pub fn parse(s: &'s str) -> Self { + //FIXME: remove unwrap + let pairs = FmtParser::parse(Rule::format_string, s) + .inspect_err(|e| println!("{}", e)) + .unwrap(); + + let mut fmts = vec![]; + + for p in pairs { + match p.as_rule() { + Rule::format => fmts.push(Format::from_pair(p)), + Rule::EOI => {} + _ => { + //FIXME: return Error instead + unimplemented!() + } + } + } + + Self { + s: Cow::Borrowed(s), + fmts, + } + } + + pub fn is_format_string(&self) -> bool { + !self.fmts.is_empty() + } +} + +pub struct Formatter<'s> { + i: usize, + i_arg: usize, + format_string: &'s FormatString<'s>, + out: String, +} + +impl<'s> From<&'s FormatString<'s>> for Formatter<'s> { + fn from(value: &'s FormatString<'s>) -> Self { + Self { + i: 0, + i_arg: 0, + format_string: value, + out: String::new(), + } + } +} + +impl Formatter<'_> { + pub fn format_arg(&mut self, arg: &A) -> &mut Self { + let slice = &self.format_string.s[self.i..]; + let arg_fmt = &self.format_string.fmts[self.i_arg]; + let arg_i = slice.find(arg_fmt.fmt.as_ref()).unwrap(); + self.out.push_str(&slice[..arg_i]); + self.out.push_str(&arg_fmt.fmt(arg).unwrap()); + self.i += arg_i + arg_fmt.fmt.len(); + self.i_arg += 1; + self + } + + pub fn to_string_lossy(&self) -> Cow<'_, str> { + Cow::Borrowed(&self.out) + } +} + +#[macro_export] +macro_rules! dformat { + (&$fmt: expr, $($arg: expr),*) => { + { + let mut fs = $crate::Formatter::from(&$fmt); + $( + fs.format_arg(&$arg); + )* + fs.to_string_lossy().to_string() + } + }; + ($fmt: expr, $($arg: expr),*) => { + dformat!(&$fmt, $($arg),*) + }; +} + +#[cfg(test)] +mod tests { + use pest::Parser; + + use super::*; + + #[test] + fn test_rule_format() { + for f in [ + "{}", "{0}", "{name}", "{:>5}", "{:<5}", "{:^5}", "{:05}", "{:+}", "{:-}", "{: }", + "{:#b}", "{:#o}", "{:#x}", "{:.2}", "{:08.2}", "{:x}", "{:X}", "{:o}", "{:b}", "{:e}", + "{:E}", "{:?}", "{:#?}", "{:p}", + ] { + FmtParser::parse(Rule::format, f) + .inspect_err(|e| println!("{}", e)) + .unwrap(); + } + } + + #[test] + fn test_rule_format_string() { + for f in [ + "Default format: {}", + "Positional args: {0}, {1}, {2}", + "Named args: {num}, {float}", + "Right-aligned: {:>5}", + "Left-aligned: {:<5}", + "Centered: {:^5}", + "Zero-padded: {:05}", + "Always show sign: {:+}", + "Show negative sign only: {:-}", + "Show space for positive numbers: {: }", + "Binary with prefix: {:#b}", + "Octal with prefix: {:#o}", + "Hex with prefix: {:#x}", + "Floating-point precision: {:.2}", + "Zero-padded floating-point: {:08.2}", + "Lowercase hex: {:x}", + "Uppercase hex: {:X}", + "Octal: {:o}", + "Binary: {:b}", + "Scientific notation (e): {:e}", + "Scientific notation (E): {:E}", + "Debug format: {:?}", + "Pretty-print debug format: {:#?}", + "Pointer address: {:p}", + ] + .iter() + { + let mut pairs = FmtParser::parse(Rule::format_string, f) + .inspect_err(|e| println!("{}", e)) + .unwrap(); + println!("{:?}", pairs.next().unwrap().as_rule()); + println!("{:?}", FormatString::parse(f)); + } + } + + #[test] + fn test_dyn_display() { + let f = FormatString::parse("this is a test: 0x{:x}"); + let mut fs = Formatter::from(&f); + fs.format_arg(&b'A'); + println!("{}", fs.to_string_lossy()); + } + + #[test] + fn test_macro() { + let s = FormatString::parse("test 0x{:x} {} {:x}"); + assert_eq!(dformat!(&s, 0x42, 43, b'A'), "test 0x42 43 41"); + assert_eq!(dformat!(s, 0x42, 43, b'A'), "test 0x42 43 41"); + } +} diff --git a/src/rust_fmt.pest b/src/rust_fmt.pest new file mode 100644 index 0000000..f96d4b0 --- /dev/null +++ b/src/rust_fmt.pest @@ -0,0 +1,28 @@ +format_string = _{ SOI ~ (maybe_format | ANY)* ~ EOI } +maybe_format = _{ ("{" ~ "{" | "}" ~ "}") | format } +format = { "{" ~ argument? ~ (":" ~ format_spec)? ~ WHITESPACE* ~ "}" } +format_spec = { (fill? ~ align)? ~ sign? ~ "#"? ~ "0"? ~ width? ~ ("." ~ precision)? ~ type? } +fill = { ASCII_ALPHANUMERIC } +align = @{ "<" | "^" | ">" } +sign = @{ "+" | "-" } +argument = { (integer | identifier) } +parameter = { argument ~ "$" } +count = { integer | parameter } +precision = { count | "*" } +width = { count } + +type_debug = @{ "?" } +type_debug_low_hex = @{ "x?" } +type_debug_up_hex = @{ "X?" } +type_oct = @{ "o" } +type_low_hex = @{ "x" } +type_up_hex = @{ "X" } +type_ptr = @{ "p" } +type_bin = @{ "b" } +type_low_exp = @{ "e" } +type_upper_exp = @{ "E" } +type = { (type_debug | type_debug_low_hex | type_debug_up_hex | type_oct | type_low_hex | type_up_hex | type_ptr | type_bin | type_low_exp | type_upper_exp | identifier) } +integer = { ASCII_DIGIT+ } +identifier = { ASCII_ALPHA ~ (ASCII_ALPHANUMERIC | "_")+ } + +WHITESPACE = _{ " " | "\t" } From def2f2a716c6d32564c5078cb8076febb689c954 Mon Sep 17 00:00:00 2001 From: qjerome Date: Fri, 4 Jul 2025 15:29:17 +0200 Subject: [PATCH 2/5] =?UTF-8?q?enhance:=C2=A0improve=20API=20+=20implement?= =?UTF-8?q?ations?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/lib.rs | 808 +++++++++++++++++++++++++++++++++++++--------- src/rust_fmt.pest | 8 +- 2 files changed, 660 insertions(+), 156 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index ee9f68a..c0cb2fe 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -7,14 +7,18 @@ use pest::{Parser, iterators::Pair}; use pest_derive::Parser; use thiserror::Error; +mod imp; + #[derive(Parser)] #[grammar = "rust_fmt.pest"] // relative to src struct FmtParser; #[derive(Debug)] pub enum FmtType { - Unsupported, Default, + Debug, + DebugLowHex, + DebugUpHex, LowerHex, UpperHex, Octal, @@ -24,139 +28,327 @@ pub enum FmtType { UpperExp, } +impl Display for FmtType { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + FmtType::Default => Ok(()), + FmtType::Debug => write!(f, "?"), + FmtType::DebugLowHex => write!(f, "x?"), + FmtType::DebugUpHex => write!(f, "X?"), + FmtType::LowerHex => write!(f, "x"), + FmtType::UpperHex => write!(f, "X"), + FmtType::Octal => write!(f, "o"), + FmtType::Ptr => write!(f, "p"), + FmtType::Bin => write!(f, "b"), + FmtType::LowExp => write!(f, "e"), + FmtType::UpperExp => write!(f, "E"), + } + } +} + +#[derive(Debug, Clone, Copy)] +pub enum Align { + Left, + Center, + Right, +} + +impl Display for Align { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Align::Center => write!(f, "^"), + Align::Left => write!(f, "<"), + Align::Right => write!(f, ">"), + } + } +} + +#[derive(Debug, Clone, Copy)] +pub enum Sign { + Positive, + Negative, +} + +impl Display for Sign { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Sign::Positive => write!(f, "+"), + Sign::Negative => write!(f, "-"), + } + } +} + #[derive(Debug)] -struct Format<'s> { - fmt: Cow<'s, str>, - ty: FmtType, +pub struct FormatSpec { + pub fill: Option, + pub align: Option, + pub sign: Option, + pub alternate: bool, + pub zero: bool, + pub width: Option, + pub precision: Option, + pub ty: FmtType, } -#[derive(Debug, Error)] -pub enum Error { - #[error("unsupported format")] - UnsupportedFormat, - #[error("format strings don't match with number of arguments")] - FormatVsArgs, +impl Display for FormatSpec { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + if let Some(c) = self.fill { + write!(f, "{}", c)?; + } + if let Some(a) = self.align { + write!(f, "{}", a)?; + } + if let Some(s) = self.sign { + write!(f, "{}", s)?; + } + if self.alternate { + write!(f, "#")?; + } + if self.zero { + write!(f, "0")?; + } + if let Some(w) = self.width { + write!(f, "{}", w)?; + } + if let Some(p) = self.precision { + write!(f, ".{}", p)?; + } + write!(f, "{}", self.ty) + } } -pub trait DynDisplay: Display + Debug { - fn fmt(&self, t: &FmtType) -> Result; +impl Default for FormatSpec { + fn default() -> Self { + Self { + fill: None, + align: None, + sign: None, + alternate: false, + zero: false, + width: None, + precision: None, + ty: FmtType::Default, + } + } } -macro_rules! impl_dyn_display_int { - ($ty: ty) => { - impl DynDisplay for $ty { - #[inline] - fn fmt(&self, t: &FmtType) -> Result { - match t { - FmtType::Default => Ok(format!("{}", self)), - FmtType::LowerHex => Ok(format!("{:x}", self)), - FmtType::UpperHex => Ok(format!("{:X}", self)), - FmtType::Bin => Ok(format!("{:b}", self)), - FmtType::Ptr => Ok(format!("{:p}", self)), - FmtType::Octal => Ok(format!("{:o}", self)), - FmtType::LowExp => Ok(format!("{:e}", self)), - FmtType::UpperExp => Ok(format!("{:e}", self)), - _ => Err(Error::UnsupportedFormat), +impl FormatSpec { + fn from_pair(p: Pair<'_, Rule>) -> Self { + let mut out = Self::default(); + for p in p.into_inner() { + match p.as_rule() { + Rule::fill => out.fill = p.as_str().chars().nth(0), + Rule::align => { + out.align = match p.as_str() { + "^" => Some(Align::Center), + "<" => Some(Align::Left), + ">" => Some(Align::Right), + _ => None, + } + } + Rule::sign => { + out.sign = match p.as_str() { + "-" => Some(Sign::Negative), + "+" => Some(Sign::Positive), + _ => None, + } + } + Rule::alternate => out.alternate = true, + Rule::zero_pad => out.zero = true, + Rule::width => out.width = p.as_str().parse::().ok(), + Rule::precision => out.precision = p.as_str().parse::().ok(), + Rule::type_fmt => { + if let Some(type_fmt) = p.into_inner().next() { + match type_fmt.as_rule() { + Rule::type_debug => out.ty = FmtType::Debug, + Rule::type_debug_low_hex => out.ty = FmtType::DebugLowHex, + Rule::type_debug_up_hex => out.ty = FmtType::DebugUpHex, + Rule::type_oct => out.ty = FmtType::Octal, + Rule::type_low_hex => out.ty = FmtType::LowerHex, + Rule::type_up_hex => out.ty = FmtType::UpperHex, + Rule::type_ptr => out.ty = FmtType::Ptr, + Rule::type_bin => out.ty = FmtType::Bin, + Rule::type_low_exp => out.ty = FmtType::LowExp, + Rule::type_upper_exp => out.ty = FmtType::UpperExp, + _ => {} + } + } + } + + _ => {} + } + } + out + } + + fn is_empty(&self) -> bool { + self.fill.is_none() + && self.align.is_none() + && self.sign.is_none() + && !self.alternate + && !self.zero + && self.width.is_none() + && self.precision.is_none() + && matches!(self.ty, FmtType::Default) + } + + pub fn fill_and_align(&self, s: String, default_align: Align) -> String { + let width = self.width.unwrap_or(s.len()); + if width <= s.len() { + s + } else { + let mut out = String::new(); + let pad = width.saturating_sub(s.len()); + let align = self.align.unwrap_or(default_align); + match align { + Align::Left => { + out.push_str(&s); + (0..pad).for_each(|_| out.push(self.fill.unwrap_or(' '))); + } + Align::Center => { + if pad > 0 { + let r_pad = pad / 2; + let l_pad = pad.div_ceil(2); + (0..r_pad).for_each(|_| out.push(self.fill.unwrap_or(' '))); + out.push_str(&s); + (0..l_pad).for_each(|_| out.push(self.fill.unwrap_or(' '))); + } + } + Align::Right => { + (0..pad).for_each(|_| out.push(self.fill.unwrap_or(' '))); + out.push_str(&s); } } + out } - }; + } } -impl DynDisplay for Box { - fn fmt(&self, t: &FmtType) -> Result { - DynDisplay::fmt(self.as_ref(), t) +#[derive(Debug)] +struct Format { + start: usize, + end: usize, + arg: Option, + spec: FormatSpec, +} + +impl Display for Format { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{{")?; + if let Some(a) = self.arg.as_ref() { + write!(f, "{}", a)?; + } + + if !self.spec.is_empty() { + write!(f, ":")?; + } + write!(f, "{}", self.spec)?; + write!(f, "}}") } } -// Signed integers -impl_dyn_display_int!(i8); -impl_dyn_display_int!(i16); -impl_dyn_display_int!(i32); -impl_dyn_display_int!(i64); -impl_dyn_display_int!(i128); -impl_dyn_display_int!(isize); - -// Unsigned integers -impl_dyn_display_int!(u8); -impl_dyn_display_int!(u16); -impl_dyn_display_int!(u32); -impl_dyn_display_int!(u64); -impl_dyn_display_int!(u128); -impl_dyn_display_int!(usize); - -impl<'s> Format<'s> { - fn from_pair(pairs: Pair<'s, Rule>) -> Self { - let ty = match pairs.as_str() { - "{}" => FmtType::Default, - "{:x}" => FmtType::LowerHex, - "{:X}" => FmtType::UpperHex, - "{:o}" => FmtType::Octal, - "{:b}" => FmtType::Bin, - "{:p}" => FmtType::Ptr, - "{:e}" => FmtType::LowExp, - "{:E}" => FmtType::UpperExp, - - _ => FmtType::Unsupported, - }; +#[derive(Debug, Error)] +pub enum Error { + #[error("unsupported format spec")] + UnsupportedSpec, + #[error("format strings don't match with number of arguments")] + FormatVsArgs, + #[error("unknown parsing rule={0}")] + UnknownParsingRule(String), + #[error("format parsing error: {0}")] + Parse(#[from] Box>), +} + +pub trait DynDisplay { + fn dyn_fmt(&self, f: &FormatSpec) -> Result; +} + +impl Format { + fn from_pair(pairs: Pair<'_, Rule>) -> Self { + let start = pairs.as_span().start(); + let end = pairs.as_span().end(); + let mut spec = None; + let mut arg = None; + + for p in pairs.into_inner() { + match p.as_rule() { + Rule::argument => { + // we ignore it for the moment + arg = Some(p.as_str().to_string()) + } + Rule::format_spec => spec = Some(FormatSpec::from_pair(p)), + _ => {} + } + } Self { - fmt: Cow::Borrowed(pairs.as_str()), - ty, + start, + end, + arg, + spec: spec.unwrap_or_default(), } } - fn fmt(&self, v: &D) -> Result { - DynDisplay::fmt(v, &self.ty) + fn dyn_fmt_arg(&self, arg: &D) -> Result { + DynDisplay::dyn_fmt(arg, &self.spec) } } #[derive(Debug)] -pub struct FormatString<'s> { - s: Cow<'s, str>, - fmts: Vec>, +pub struct FormatString { + s: String, + fmts: Vec, } -impl<'s> FormatString<'s> { - pub fn parse(s: &'s str) -> Self { - //FIXME: remove unwrap - let pairs = FmtParser::parse(Rule::format_string, s) - .inspect_err(|e| println!("{}", e)) - .unwrap(); +impl FormatString { + #[inline] + fn new_from_str>(s: S) -> Result { + let pairs = FmtParser::parse(Rule::format_string, s.as_ref()) + .map_err(|e| Error::from(Box::new(e)))?; let mut fmts = vec![]; for p in pairs { - match p.as_rule() { - Rule::format => fmts.push(Format::from_pair(p)), - Rule::EOI => {} - _ => { - //FIXME: return Error instead - unimplemented!() - } + // WARNING: here anything else than Rule::format is ignored + if p.as_rule() == Rule::format { + fmts.push(Format::from_pair(p)) } } - Self { - s: Cow::Borrowed(s), + Ok(Self { + s: s.as_ref().to_string(), fmts, - } + }) + } + + pub fn from_string(s: String) -> Result { + Self::new_from_str(s) + } + + pub fn into_string(self) -> String { + self.s } - pub fn is_format_string(&self) -> bool { + pub fn to_string_lossy(&self) -> Cow<'_, str> { + Cow::Borrowed(&self.s) + } + + pub fn contains_format(&self) -> bool { !self.fmts.is_empty() } } pub struct Formatter<'s> { + /// index in the format string i: usize, + /// index of of the format string argument i_arg: usize, - format_string: &'s FormatString<'s>, + format_string: &'s FormatString, out: String, } -impl<'s> From<&'s FormatString<'s>> for Formatter<'s> { - fn from(value: &'s FormatString<'s>) -> Self { +impl<'s> From<&'s FormatString> for Formatter<'s> { + fn from(value: &'s FormatString) -> Self { Self { i: 0, i_arg: 0, @@ -167,107 +359,417 @@ impl<'s> From<&'s FormatString<'s>> for Formatter<'s> { } impl Formatter<'_> { - pub fn format_arg(&mut self, arg: &A) -> &mut Self { - let slice = &self.format_string.s[self.i..]; - let arg_fmt = &self.format_string.fmts[self.i_arg]; - let arg_i = slice.find(arg_fmt.fmt.as_ref()).unwrap(); - self.out.push_str(&slice[..arg_i]); - self.out.push_str(&arg_fmt.fmt(arg).unwrap()); - self.i += arg_i + arg_fmt.fmt.len(); + pub fn format_arg(&mut self, arg: &A) -> Result<&mut Self, Error> { + let Some(arg_fmt) = &self.format_string.fmts.get(self.i_arg) else { + return Ok(self); + }; + + let slice = &self.format_string.s.as_str()[self.i..]; + self.out.push_str(&slice[..arg_fmt.start - self.i]); + self.out.push_str(&arg_fmt.dyn_fmt_arg(arg)?); + self.i = arg_fmt.end; self.i_arg += 1; - self + + Ok(self) } pub fn to_string_lossy(&self) -> Cow<'_, str> { Cow::Borrowed(&self.out) } + + pub fn into_string(self) -> String { + self.out + } } #[macro_export] macro_rules! dformat { - (&$fmt: expr, $($arg: expr),*) => { + ($fmt: expr, $($arg: expr),*) => { { - let mut fs = $crate::Formatter::from(&$fmt); + let mut last_err = None; + let mut fs = $crate::Formatter::from($fmt); $( - fs.format_arg(&$arg); + if let Err(e)=fs.format_arg(&$arg) { + last_err = Some(e); + } )* - fs.to_string_lossy().to_string() + match last_err { + Some(e) => Err(e), + None => Ok(fs.into_string()), + } } }; - ($fmt: expr, $($arg: expr),*) => { - dformat!(&$fmt, $($arg),*) - }; } #[cfg(test)] mod tests { + use std::{ + ffi::{OsStr, OsString}, + net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr, SocketAddrV4, SocketAddrV6}, + path::{Path, PathBuf}, + rc::Rc, + sync::Arc, + time::{Duration, Instant, SystemTime}, + }; + use pest::Parser; use super::*; + macro_rules! dformat_lit { + ($fmt: literal, $($arg: expr),*) => { + { + let fs = FormatString::new_from_str($fmt).unwrap(); + dformat!(&fs, $($arg),*) + } + }; + } + #[test] fn test_rule_format() { for f in [ - "{}", "{0}", "{name}", "{:>5}", "{:<5}", "{:^5}", "{:05}", "{:+}", "{:-}", "{: }", - "{:#b}", "{:#o}", "{:#x}", "{:.2}", "{:08.2}", "{:x}", "{:X}", "{:o}", "{:b}", "{:e}", - "{:E}", "{:?}", "{:#?}", "{:p}", + "{}", "{0}", "{name}", "{:>5}", "{:<5}", "{:^5}", "{:05}", "{:+}", "{:-}", "{:#b}", + "{:#o}", "{:#x}", "{:.2}", "{:08.2}", "{:x}", "{:X}", "{:o}", "{:b}", "{:e}", "{:E}", + "{:?}", "{:#?}", "{:p}", ] { - FmtParser::parse(Rule::format, f) - .inspect_err(|e| println!("{}", e)) - .unwrap(); + let fmt = Format::from_pair( + FmtParser::parse(Rule::format, f) + .inspect_err(|e| println!("{}", e)) + .unwrap() + .next() + .unwrap(), + ); + + assert_eq!(f, format!("{}", fmt)); } } #[test] - fn test_rule_format_string() { - for f in [ - "Default format: {}", - "Positional args: {0}, {1}, {2}", - "Named args: {num}, {float}", - "Right-aligned: {:>5}", - "Left-aligned: {:<5}", - "Centered: {:^5}", - "Zero-padded: {:05}", - "Always show sign: {:+}", - "Show negative sign only: {:-}", - "Show space for positive numbers: {: }", - "Binary with prefix: {:#b}", - "Octal with prefix: {:#o}", - "Hex with prefix: {:#x}", - "Floating-point precision: {:.2}", - "Zero-padded floating-point: {:08.2}", - "Lowercase hex: {:x}", - "Uppercase hex: {:X}", - "Octal: {:o}", - "Binary: {:b}", - "Scientific notation (e): {:e}", - "Scientific notation (E): {:E}", - "Debug format: {:?}", - "Pretty-print debug format: {:#?}", - "Pointer address: {:p}", - ] - .iter() - { - let mut pairs = FmtParser::parse(Rule::format_string, f) - .inspect_err(|e| println!("{}", e)) - .unwrap(); - println!("{:?}", pairs.next().unwrap().as_rule()); - println!("{:?}", FormatString::parse(f)); - } + fn test_integer_formatting() { + // Decimal formatting + assert_eq!(format!("{}", 42), dformat_lit!("{}", 42).unwrap()); + assert_eq!(format!("{:5}", 42), dformat_lit!("{:5}", 42).unwrap()); + assert_eq!(format!("{:05}", 42), dformat_lit!("{:05}", 42).unwrap()); + assert_eq!(format!("{:+}", 42), dformat_lit!("{:+}", 42).unwrap()); + assert_eq!(format!("{: }", 42), dformat_lit!("{: }", 42).unwrap()); + assert_eq!(format!("{:#}", 42), dformat_lit!("{:#}", 42).unwrap()); + + // Hexadecimal formatting + assert_eq!(format!("{:x}", 42), dformat_lit!("{:x}", 42).unwrap()); + assert_eq!(format!("{:X}", 42), dformat_lit!("{:X}", 42).unwrap()); + assert_eq!(format!("{:#x}", 42), dformat_lit!("{:#x}", 42).unwrap()); + assert_eq!(format!("{:#X}", 42), dformat_lit!("{:#X}", 42).unwrap()); + + // Octal formatting + assert_eq!(format!("{:o}", 42), dformat_lit!("{:o}", 42).unwrap()); + assert_eq!(format!("{:#o}", 42), dformat_lit!("{:#o}", 42).unwrap()); + + // Binary formatting + assert_eq!(format!("{:b}", 42), dformat_lit!("{:b}", 42).unwrap()); + assert_eq!(format!("{:#b}", 42), dformat_lit!("{:#b}", 42).unwrap()); + + // Width and alignment + assert_eq!(format!("{:5}", 42), dformat_lit!("{:5}", 42).unwrap()); + assert_eq!(format!("{:<5}", 42), dformat_lit!("{:<5}", 42).unwrap()); + assert_eq!(format!("{:>5}", 42), dformat_lit!("{:>5}", 42).unwrap()); + assert_eq!(format!("{:^5}", 42), dformat_lit!("{:^5}", 42).unwrap()); + assert_eq!(format!("{:05}", 42), dformat_lit!("{:05}", 42).unwrap()); + assert_eq!(format!("{:+<5}", 42), dformat_lit!("{:+<5}", 42).unwrap()); + assert_eq!(format!("{:->5}", 42), dformat_lit!("{:->5}", 42).unwrap()); + assert_eq!(format!("{:+^5}", 42), dformat_lit!("{:+^5}", 42).unwrap()); + } + + #[test] + fn test_float_formatting() { + // Float formatting + assert_eq!(format!("{}", 42.0), dformat_lit!("{}", 42.0).unwrap()); + assert_eq!(format!("{:e}", 42.0), dformat_lit!("{:e}", 42.0).unwrap()); + assert_eq!(format!("{:E}", 42.0), dformat_lit!("{:E}", 42.0).unwrap()); + assert_eq!(format!("{:.2}", 42.0), dformat_lit!("{:.2}", 42.0).unwrap()); + assert_eq!( + format!("{:.2}", 42.1234), + dformat_lit!("{:.2}", 42.1234).unwrap() + ); + assert_eq!( + format!("{:10.2}", 42.1234), + dformat_lit!("{:10.2}", 42.1234).unwrap() + ); + assert_eq!( + format!("{:<10.2}", 42.1234), + dformat_lit!("{:<10.2}", 42.1234).unwrap() + ); + assert_eq!( + format!("{:>10.2}", 42.1234), + dformat_lit!("{:>10.2}", 42.1234).unwrap() + ); + assert_eq!( + format!("{:^10.2}", 42.1234), + dformat_lit!("{:^10.2}", 42.1234).unwrap() + ); + assert_eq!(format!("{:+}", 42.0), dformat_lit!("{:+}", 42.0).unwrap()); + assert_eq!( + format!("{:+<10.2}", 42.1234), + dformat_lit!("{:+<10.2}", 42.1234).unwrap() + ); + assert_eq!( + format!("{:->10.2}", 42.1234), + dformat_lit!("{:->10.2}", 42.1234).unwrap() + ); + assert_eq!( + format!("{:+^10.2}", 42.1234), + dformat_lit!("{:+^10.2}", 42.1234).unwrap() + ); } #[test] - fn test_dyn_display() { - let f = FormatString::parse("this is a test: 0x{:x}"); - let mut fs = Formatter::from(&f); - fs.format_arg(&b'A'); - println!("{}", fs.to_string_lossy()); + fn test_string_formatting() { + // String formatting + assert_eq!(format!("{}", "hello"), dformat_lit!("{}", "hello").unwrap()); + assert_eq!( + format!("{:10}", "hello"), + dformat_lit!("{:10}", "hello").unwrap() + ); + assert_eq!( + format!("{:<10}", "hello"), + dformat_lit!("{:<10}", "hello").unwrap() + ); + assert_eq!( + format!("{:>10}", "hello"), + dformat_lit!("{:>10}", "hello").unwrap() + ); + assert_eq!( + format!("{:^10}", "hello"), + dformat_lit!("{:^10}", "hello").unwrap() + ); + + // String precision + assert_eq!( + format!("{:.3}", "hello"), + dformat_lit!("{:.3}", "hello").unwrap() + ); + assert_eq!( + format!("{:10.3}", "hello"), + dformat_lit!("{:10.3}", "hello").unwrap() + ); + assert_eq!( + format!("{:<10.3}", "hello"), + dformat_lit!("{:<10.3}", "hello").unwrap() + ); + assert_eq!( + format!("{:>10.3}", "hello"), + dformat_lit!("{:>10.3}", "hello").unwrap() + ); + assert_eq!( + format!("{:^10.3}", "hello"), + dformat_lit!("{:^10.3}", "hello").unwrap() + ); + } + + #[test] + fn test_char_formatting() { + // Character formatting + assert_eq!(format!("{}", 'A'), dformat_lit!("{}", 'A').unwrap()); + assert_eq!(format!("{:5}", 'A'), dformat_lit!("{:5}", 'A').unwrap()); + assert_eq!(format!("{:<5}", 'A'), dformat_lit!("{:<5}", 'A').unwrap()); + assert_eq!(format!("{:>5}", 'A'), dformat_lit!("{:>5}", 'A').unwrap()); + assert_eq!(format!("{:^5}", 'A'), dformat_lit!("{:^5}", 'A').unwrap()); + } + + #[test] + fn test_bool_formatting() { + // Boolean formatting + assert_eq!(format!("{}", true), dformat_lit!("{}", true).unwrap()); + assert_eq!(format!("{}", false), dformat_lit!("{}", false).unwrap()); + assert_eq!(format!("{:5}", true), dformat_lit!("{:5}", true).unwrap()); + assert_eq!(format!("{:<5}", true), dformat_lit!("{:<5}", true).unwrap()); + assert_eq!(format!("{:>5}", true), dformat_lit!("{:>5}", true).unwrap()); + assert_eq!(format!("{:^5}", true), dformat_lit!("{:^5}", true).unwrap()); + } + + #[test] + fn test_pointer_formatting() { + // Pointer formatting + let x = 42; + let ptr = &x as *const i32; + assert_eq!(format!("{:p}", ptr), dformat_lit!("{:p}", ptr).unwrap()); + assert_eq!(format!("{:10p}", ptr), dformat_lit!("{:10p}", ptr).unwrap()); + assert_eq!( + format!("{:<10p}", ptr), + dformat_lit!("{:<10p}", ptr).unwrap() + ); + assert_eq!( + format!("{:>10p}", ptr), + dformat_lit!("{:>10p}", ptr).unwrap() + ); + assert_eq!( + format!("{:^10p}", ptr), + dformat_lit!("{:^10p}", ptr).unwrap() + ); } #[test] - fn test_macro() { - let s = FormatString::parse("test 0x{:x} {} {:x}"); - assert_eq!(dformat!(&s, 0x42, 43, b'A'), "test 0x42 43 41"); - assert_eq!(dformat!(s, 0x42, 43, b'A'), "test 0x42 43 41"); + fn test_multiple_arguments() { + // Multiple arguments + assert_eq!( + format!("{} {}", "hello", 42), + dformat_lit!("{} {}", "hello", 42).unwrap() + ); + assert_eq!( + format!("{:5} {:<10.2}", 42, 42.1234), + dformat_lit!("{:5} {:<10.2}", 42, 42.1234).unwrap() + ); + assert_eq!( + format!("{:>5} {:^10.2}", 42, 42.1234), + dformat_lit!("{:>5} {:^10.2}", 42, 42.1234).unwrap() + ); + } + + #[test] + fn test_complex_formatting() { + // Complex formatting + assert_eq!( + format!("{:05} {:<10.2} {:^10}", 42, 42.1234, "hello"), + dformat_lit!("{:05} {:<10.2} {:^10}", 42, 42.1234, "hello").unwrap() + ); + assert_eq!( + format!("{:+<5} {:^10.2} {:>10}", 42, 42.1234, "hello"), + dformat_lit!("{:+<5} {:^10.2} {:>10}", 42, 42.1234, "hello").unwrap() + ); + } + + #[test] + fn test_network_types() { + // IpAddr, Ipv4Addr, Ipv6Addr + let ipv4_addr = Ipv4Addr::new(127, 0, 0, 1); + let ipv6_addr = Ipv6Addr::new(0, 0, 0, 0, 0, 0, 0, 1); + let ip_addr_v4 = IpAddr::V4(ipv4_addr); + let ip_addr_v6 = IpAddr::V6(ipv6_addr); + + assert_eq!( + format!("{}", ipv4_addr), + dformat_lit!("{}", ipv4_addr).unwrap() + ); + assert_eq!( + format!("{:?}", ipv4_addr), + dformat_lit!("{:?}", ipv4_addr).unwrap() + ); + assert_eq!( + format!("{}", ipv6_addr), + dformat_lit!("{}", ipv6_addr).unwrap() + ); + assert_eq!( + format!("{:?}", ipv6_addr), + dformat_lit!("{:?}", ipv6_addr).unwrap() + ); + assert_eq!( + format!("{}", ip_addr_v4), + dformat_lit!("{}", ip_addr_v4).unwrap() + ); + assert_eq!( + format!("{:?}", ip_addr_v4), + dformat_lit!("{:?}", ip_addr_v4).unwrap() + ); + assert_eq!( + format!("{}", ip_addr_v6), + dformat_lit!("{}", ip_addr_v6).unwrap() + ); + assert_eq!( + format!("{:?}", ip_addr_v6), + dformat_lit!("{:?}", ip_addr_v6).unwrap() + ); + + // SocketAddr, SocketAddrV4, SocketAddrV6 + let socket_addr_v4 = SocketAddrV4::new(ipv4_addr, 8080); + let socket_addr_v6 = SocketAddrV6::new(ipv6_addr, 8080, 0, 0); + let socket_addr = SocketAddr::V4(socket_addr_v4); + + assert_eq!( + format!("{}", socket_addr_v4), + dformat_lit!("{}", socket_addr_v4).unwrap() + ); + assert_eq!( + format!("{:?}", socket_addr_v4), + dformat_lit!("{:?}", socket_addr_v4).unwrap() + ); + assert_eq!( + format!("{}", socket_addr_v6), + dformat_lit!("{}", socket_addr_v6).unwrap() + ); + assert_eq!( + format!("{:?}", socket_addr_v6), + dformat_lit!("{:?}", socket_addr_v6).unwrap() + ); + assert_eq!( + format!("{}", socket_addr), + dformat_lit!("{}", socket_addr).unwrap() + ); + assert_eq!( + format!("{:?}", socket_addr), + dformat_lit!("{:?}", socket_addr).unwrap() + ); + } + + #[test] + fn test_time_types() { + // Duration, SystemTime, Instant + let duration = Duration::from_secs(3600); + let system_time = SystemTime::now(); + let instant = Instant::now(); + + assert_eq!( + format!("{:?}", duration), + dformat_lit!("{:?}", duration).unwrap() + ); + assert_eq!( + format!("{:?}", system_time), + dformat_lit!("{:?}", system_time).unwrap() + ); + assert_eq!( + format!("{:?}", instant), + dformat_lit!("{:?}", instant).unwrap() + ); + } + + #[test] + fn test_path_types() { + // Path, PathBuf + let path = Path::new("/path/to/file"); + let path_buf = PathBuf::from("/path/to/file"); + + assert_eq!(format!("{:?}", path), dformat_lit!("{:?}", path).unwrap()); + assert_eq!( + format!("{:?}", path_buf), + dformat_lit!("{:?}", path_buf).unwrap() + ); + } + + #[test] + fn test_ffi_types() { + // OsString, OsStr + let os_string = OsString::from("OS String"); + let os_str: &OsStr = os_string.as_os_str(); + + assert_eq!( + format!("{:?}", os_string), + dformat_lit!("{:?}", os_string).unwrap() + ); + assert_eq!( + format!("{:?}", os_str), + dformat_lit!("{:?}", os_str).unwrap() + ); + } + + #[test] + fn test_smart_pointers() { + // Box, Rc, Arc, Cow + let boxed = Box::new(42); + let rc = Rc::new(42); + let arc = Arc::new(42); + let cow_str: Cow<'_, str> = Cow::Borrowed("Hello"); + + assert_eq!(format!("{}", boxed), dformat_lit!("{}", boxed).unwrap()); + assert_eq!(format!("{}", rc), dformat_lit!("{}", rc).unwrap()); + assert_eq!(format!("{}", arc), dformat_lit!("{}", arc).unwrap()); + assert_eq!(format!("{}", cow_str), dformat_lit!("{}", cow_str).unwrap()); } } diff --git a/src/rust_fmt.pest b/src/rust_fmt.pest index f96d4b0..7594c85 100644 --- a/src/rust_fmt.pest +++ b/src/rust_fmt.pest @@ -1,10 +1,12 @@ format_string = _{ SOI ~ (maybe_format | ANY)* ~ EOI } maybe_format = _{ ("{" ~ "{" | "}" ~ "}") | format } format = { "{" ~ argument? ~ (":" ~ format_spec)? ~ WHITESPACE* ~ "}" } -format_spec = { (fill? ~ align)? ~ sign? ~ "#"? ~ "0"? ~ width? ~ ("." ~ precision)? ~ type? } -fill = { ASCII_ALPHANUMERIC } +format_spec = { (fill? ~ align)? ~ sign? ~ alternate? ~ zero_pad? ~ width? ~ ("." ~ precision)? ~ type_fmt? } +fill = { (ASCII_ALPHANUMERIC | "+" | "-") } align = @{ "<" | "^" | ">" } sign = @{ "+" | "-" } +alternate = @{ "#" } +zero_pad = @{ "0" } argument = { (integer | identifier) } parameter = { argument ~ "$" } count = { integer | parameter } @@ -21,7 +23,7 @@ type_ptr = @{ "p" } type_bin = @{ "b" } type_low_exp = @{ "e" } type_upper_exp = @{ "E" } -type = { (type_debug | type_debug_low_hex | type_debug_up_hex | type_oct | type_low_hex | type_up_hex | type_ptr | type_bin | type_low_exp | type_upper_exp | identifier) } +type_fmt = { (type_debug | type_debug_low_hex | type_debug_up_hex | type_oct | type_low_hex | type_up_hex | type_ptr | type_bin | type_low_exp | type_upper_exp | identifier) } integer = { ASCII_DIGIT+ } identifier = { ASCII_ALPHA ~ (ASCII_ALPHANUMERIC | "_")+ } From 1dc36542a1f8f42ba4a36e9a47dd3707c4b8336e Mon Sep 17 00:00:00 2001 From: qjerome Date: Fri, 4 Jul 2025 15:29:26 +0200 Subject: [PATCH 3/5] =?UTF-8?q?enhance:=C2=A0improve=20API=20+=20implement?= =?UTF-8?q?ations?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/imp.rs | 439 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 439 insertions(+) create mode 100644 src/imp.rs diff --git a/src/imp.rs b/src/imp.rs new file mode 100644 index 0000000..6627b55 --- /dev/null +++ b/src/imp.rs @@ -0,0 +1,439 @@ +use std::{ + borrow::Cow, + ffi::{OsStr, OsString}, + net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr, SocketAddrV4, SocketAddrV6}, + path::{Path, PathBuf}, + rc::Rc, + sync::Arc, + time::{Duration, Instant, SystemTime}, +}; + +use crate::{Align, DynDisplay, Error, FmtType, FormatSpec, Sign}; + +macro_rules! impl_debug { + ($ty:ty) => { + impl DynDisplay for $ty { + fn dyn_fmt(&self, f: &FormatSpec) -> Result { + match f.ty { + FmtType::Debug => { + if f.alternate { + Ok(format!("{:#?}", self)) + } else { + Ok(format!("{:?}", self)) + } + } + FmtType::Default + | FmtType::DebugLowHex + | FmtType::DebugUpHex + | FmtType::LowerHex + | FmtType::UpperHex + | FmtType::Octal + | FmtType::Ptr + | FmtType::Bin + | FmtType::LowExp + | FmtType::UpperExp => Err(Error::UnsupportedSpec), + } + .map(|s| f.fill_and_align(s, Align::Left)) + } + } + }; +} + +macro_rules! impl_debug_display { + ($ty:ty) => { + impl DynDisplay for $ty { + fn dyn_fmt(&self, f: &FormatSpec) -> Result { + match f.ty { + FmtType::Debug => { + if f.alternate { + Ok(format!("{:#?}", self)) + } else { + Ok(format!("{:?}", self)) + } + } + FmtType::Default => Ok(format!("{}", self)), + FmtType::DebugLowHex + | FmtType::DebugUpHex + | FmtType::LowerHex + | FmtType::UpperHex + | FmtType::Octal + | FmtType::Ptr + | FmtType::Bin + | FmtType::LowExp + | FmtType::UpperExp => Err(Error::UnsupportedSpec), + } + .map(|s| f.fill_and_align(s, Align::Left)) + } + } + }; +} + +impl DynDisplay for char { + fn dyn_fmt(&self, f: &FormatSpec) -> Result { + match f.ty { + FmtType::Debug => { + if f.alternate { + Ok(format!("{:#?}", self)) + } else { + Ok(format!("{:?}", self)) + } + } + FmtType::Default => Ok(format!("{}", self)), + FmtType::DebugLowHex + | FmtType::DebugUpHex + | FmtType::LowerHex + | FmtType::UpperHex + | FmtType::Octal + | FmtType::Ptr + | FmtType::Bin + | FmtType::LowExp + | FmtType::UpperExp => Err(Error::UnsupportedSpec), + } + .map(|s| f.fill_and_align(s, Align::Left)) + } +} + +impl_debug_display!(bool); + +impl DynDisplay for *const T { + fn dyn_fmt(&self, f: &FormatSpec) -> Result { + let ptr = *self; + match f.ty { + FmtType::Debug => { + if f.alternate { + Ok(format!("{:#?}", ptr)) + } else { + Ok(format!("{:?}", ptr)) + } + } + FmtType::Ptr => Ok(format!("{:p}", ptr)), + FmtType::Default + | FmtType::DebugLowHex + | FmtType::DebugUpHex + | FmtType::LowerHex + | FmtType::UpperHex + | FmtType::Octal + | FmtType::Bin + | FmtType::LowExp + | FmtType::UpperExp => Err(Error::UnsupportedSpec), + } + .map(|s| f.fill_and_align(s, Align::Right)) + } +} + +impl DynDisplay for *mut T { + fn dyn_fmt(&self, f: &FormatSpec) -> Result { + let ptr = *self; + match f.ty { + FmtType::Debug => { + if f.alternate { + Ok(format!("{:#?}", ptr)) + } else { + Ok(format!("{:?}", ptr)) + } + } + FmtType::Ptr => Ok(format!("{:p}", ptr)), + FmtType::Default + | FmtType::DebugLowHex + | FmtType::DebugUpHex + | FmtType::LowerHex + | FmtType::UpperHex + | FmtType::Octal + | FmtType::Bin + | FmtType::LowExp + | FmtType::UpperExp => Err(Error::UnsupportedSpec), + } + .map(|s| f.fill_and_align(s, Align::Right)) + } +} + +impl DynDisplay for &str { + fn dyn_fmt(&self, f: &FormatSpec) -> Result { + match f.ty { + FmtType::Debug => { + if f.alternate { + Ok(format!("{:#?}", self)) + } else { + Ok(format!("{:?}", self)) + } + } + FmtType::DebugLowHex => Ok(format!("{:x?}", self)), + FmtType::DebugUpHex => Ok(format!("{:X?}", self)), + FmtType::Default => match (f.width, f.precision) { + (None, None) => Ok(self.to_string()), + (Some(w), None) => match f.align { + Some(Align::Left) => Ok(format!("{:<1$}", self, w)), + Some(Align::Center) => Ok(format!("{:^1$}", self, w)), + Some(Align::Right) => Ok(format!("{:>1$}", self, w)), + None => Ok(format!("{:1$}", self, w)), + }, + (None, Some(p)) => Ok(format!("{:.1$}", self, p)), + (Some(w), Some(p)) => match f.align { + Some(Align::Left) => Ok(format!("{:<1$.2$}", self, w, p)), + Some(Align::Center) => Ok(format!("{:^1$.2$}", self, w, p)), + Some(Align::Right) => Ok(format!("{:>1$.2$}", self, w, p)), + None => Ok(format!("{:1$.2$}", self, w, p)), + }, + }, + FmtType::Ptr => { + if f.alternate { + Ok(format!("{:#p}", self)) + } else { + Ok(format!("{:p}", self)) + } + } + FmtType::LowerHex + | FmtType::UpperHex + | FmtType::Bin + | FmtType::Octal + | FmtType::LowExp + | FmtType::UpperExp => Err(Error::UnsupportedSpec), + } + } +} + +impl DynDisplay for str { + fn dyn_fmt(&self, f: &FormatSpec) -> Result { + DynDisplay::dyn_fmt(&self, f) + } +} + +impl DynDisplay for String { + fn dyn_fmt(&self, t: &FormatSpec) -> Result { + if matches!(t.ty, FmtType::Ptr) { + return Err(Error::UnsupportedSpec); + } + DynDisplay::dyn_fmt(&self.as_str(), t) + } +} + +macro_rules! impl_dyn_display_float { + ($ty: ty) => { + impl DynDisplay for $ty { + #[inline] + fn dyn_fmt(&self, f: &FormatSpec) -> Result { + match f.ty { + FmtType::Debug => { + if f.alternate { + Ok(format!("{:#?}", self)) + } else { + Ok(format!("{:?}", self)) + } + } + FmtType::DebugUpHex => Ok(format!("{:X?}", self)), + FmtType::DebugLowHex => Ok(format!("{:x?}", self)), + FmtType::Default => match (f.precision, f.sign) { + (None, None) => Ok(format!("{}", self)), + (Some(p), None) => Ok(format!("{:.1$}", self, p)), + (None, Some(s)) => match s { + Sign::Positive => Ok(format!("{:+}", self)), + Sign::Negative => Ok(format!("{:-}", self)), + }, + (Some(p), Some(s)) => match s { + Sign::Positive => Ok(format!("{:+.1$}", self, p)), + Sign::Negative => Ok(format!("{:-.1$}", self, p)), + }, + }, + FmtType::LowExp => match (f.precision, f.sign) { + (None, None) => Ok(format!("{:e}", self)), + (Some(p), None) => Ok(format!("{:.1$e}", self, p)), + (None, Some(s)) => match s { + Sign::Positive => Ok(format!("{:+e}", self)), + Sign::Negative => Ok(format!("{:-e}", self)), + }, + (Some(p), Some(s)) => match s { + Sign::Positive => Ok(format!("{:+.1$e}", self, p)), + Sign::Negative => Ok(format!("{:-.1$e}", self, p)), + }, + }, + FmtType::UpperExp => match (f.precision, f.sign) { + (None, None) => Ok(format!("{:E}", self)), + (Some(p), None) => Ok(format!("{:.1$E}", self, p)), + (None, Some(s)) => match s { + Sign::Positive => Ok(format!("{:+E}", self)), + Sign::Negative => Ok(format!("{:-E}", self)), + }, + (Some(p), Some(s)) => match s { + Sign::Positive => Ok(format!("{:+.1$E}", self, p)), + Sign::Negative => Ok(format!("{:-.1$E}", self, p)), + }, + }, + FmtType::Ptr + | FmtType::Bin + | FmtType::LowerHex + | FmtType::Octal + | FmtType::UpperHex => Err(Error::UnsupportedSpec), + } + .map(|s| f.fill_and_align(s, Align::Right)) + } + } + }; +} + +impl_dyn_display_float!(f32); +impl_dyn_display_float!(f64); + +macro_rules! impl_dyn_display_int { + ($ty: ty) => { + impl DynDisplay for $ty { + fn dyn_fmt(&self, f: &FormatSpec) -> Result { + match f.ty { + FmtType::Default => match (f.alternate, f.zero) { + (true, true) => { + if let Some(w) = f.width { + Ok(format!("{:#01$}", self, w)) + } else { + Ok(format!("{:#0}", self)) + } + } + (true, false) => Ok(format!("{:#}", self)), + (false, true) => { + if let Some(w) = f.width { + Ok(format!("{:01$}", self, w)) + } else { + Ok(format!("{:0}", self)) + } + } + (false, false) => { + // sign is used only if not alternate / zero + if let Some(s) = f.sign { + match s { + Sign::Positive => Ok(format!("{:+}", self)), + Sign::Negative => Ok(format!("{:-}", self)), + } + } else { + Ok(format!("{:}", self)) + } + } + }, + FmtType::Debug => { + if f.alternate { + Ok(format!("{:#?}", self)) + } else { + Ok(format!("{:?}", self)) + } + } + FmtType::LowerHex => match (f.alternate, f.zero) { + (true, true) => Ok(format!("{:#0x}", self)), + (true, false) => Ok(format!("{:#x}", self)), + (false, true) => Ok(format!("{:0x}", self)), + (false, false) => Ok(format!("{:x}", self)), + }, + FmtType::UpperHex => match (f.alternate, f.zero) { + (true, true) => Ok(format!("{:#0X}", self)), + (true, false) => Ok(format!("{:#X}", self)), + (false, true) => Ok(format!("{:0X}", self)), + (false, false) => Ok(format!("{:X}", self)), + }, + FmtType::Bin => match (f.alternate, f.zero) { + (true, true) => Ok(format!("{:#0b}", self)), + (true, false) => Ok(format!("{:#b}", self)), + (false, true) => Ok(format!("{:0b}", self)), + (false, false) => Ok(format!("{:b}", self)), + }, + FmtType::Octal => match (f.alternate, f.zero) { + (true, true) => Ok(format!("{:#0o}", self)), + (true, false) => Ok(format!("{:#o}", self)), + (false, true) => Ok(format!("{:0o}", self)), + (false, false) => Ok(format!("{:o}", self)), + }, + // LowExp doesn't use zero / alternate + FmtType::LowExp => Ok(format!("{:e}", self)), + // UpperExp doesn't use zero / alternate + FmtType::UpperExp => Ok(format!("{:E}", self)), + FmtType::Ptr => { + if f.alternate { + Ok(format!("{:#p}", self)) + } else { + Ok(format!("{:p}", self)) + } + } + + // Special handling for debug-with-hex + FmtType::DebugLowHex => { + if f.alternate { + Ok(format!("{:#x?}", self)) + } else { + Ok(format!("{:x?}", self)) + } + } + FmtType::DebugUpHex => { + if f.alternate { + Ok(format!("{:#X?}", self)) + } else { + Ok(format!("{:X?}", self)) + } + } + } + .map(|s| f.fill_and_align(s, Align::Right)) + } + } + }; +} + +// Signed integers +impl_dyn_display_int!(i8); +impl_dyn_display_int!(i16); +impl_dyn_display_int!(i32); +impl_dyn_display_int!(i64); +impl_dyn_display_int!(i128); +impl_dyn_display_int!(isize); + +// Unsigned integers +impl_dyn_display_int!(u8); +impl_dyn_display_int!(u16); +impl_dyn_display_int!(u32); +impl_dyn_display_int!(u64); +impl_dyn_display_int!(u128); +impl_dyn_display_int!(usize); + +impl DynDisplay for Box { + fn dyn_fmt(&self, f: &FormatSpec) -> Result { + DynDisplay::dyn_fmt(self.as_ref(), f) + } +} + +impl DynDisplay for Rc { + fn dyn_fmt(&self, f: &FormatSpec) -> Result { + DynDisplay::dyn_fmt(self.as_ref(), f) + } +} + +impl DynDisplay for Arc { + fn dyn_fmt(&self, f: &FormatSpec) -> Result { + DynDisplay::dyn_fmt(self.as_ref(), f) + } +} + +impl DynDisplay for Cow<'_, T> { + fn dyn_fmt(&self, f: &FormatSpec) -> Result { + DynDisplay::dyn_fmt(self.as_ref(), f) + } +} + +impl DynDisplay for Cow<'_, str> { + fn dyn_fmt(&self, f: &FormatSpec) -> Result { + DynDisplay::dyn_fmt(self.as_ref(), f) + } +} + +// network +impl_debug_display!(IpAddr); +impl_debug_display!(Ipv4Addr); +impl_debug_display!(Ipv6Addr); +impl_debug_display!(SocketAddr); +impl_debug_display!(SocketAddrV4); +impl_debug_display!(SocketAddrV6); + +// time +impl_debug!(Duration); +impl_debug!(SystemTime); +impl_debug!(Instant); + +// path +impl_debug!(&Path); +impl_debug!(PathBuf); + +// ffi +impl_debug!(OsString); +impl_debug!(&OsStr); From bb4f18f1a48029fbc6f542de86a11a411fd724de Mon Sep 17 00:00:00 2001 From: qjerome Date: Mon, 7 Jul 2025 14:12:42 +0200 Subject: [PATCH 4/5] chore: update Cargo.toml --- Cargo.toml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/Cargo.toml b/Cargo.toml index 52d7277..fc17657 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,6 +2,13 @@ name = "dyf" version = "0.1.0" edition = "2024" +keywords = ["dynamic", "format", "string"] +description = "Dynamic string formatting library for Rust supporting all standard format specifiers" +readme = "README.md" +repository = "https://github.com/qjerome/dyf" +documentation = "https://docs.rs/dyf" +license = "GPL-3.0" +rust-version = "1.85.0" [dependencies] pest = "2.8.1" From d55f27689f995044f3ae80e678b4130a7c559f8c Mon Sep 17 00:00:00 2001 From: qjerome Date: Mon, 7 Jul 2025 14:13:16 +0200 Subject: [PATCH 5/5] doc: whole crate documentation --- README.md | 208 ++++++++++- src/imp.rs | 28 +- src/lib.rs | 934 ++++++++++++++++++++++++++++++++++++++++++++++++-- src/parser.rs | 6 + 4 files changed, 1130 insertions(+), 46 deletions(-) create mode 100644 src/parser.rs diff --git a/README.md b/README.md index dae532b..d0c9b6c 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,206 @@ -# dyf -Rust crate for dynamic string formatting + + +# Dynamic String Formatting for Rust (dyf) + +The `dyf` crate brings dynamic string formatting to Rust while supporting the whole variety of string formats available in Rust. +It provides an easy way to implement dynamic formatting for custom types with the implementation of the `DynDisplay` trait. + +## Features + +- Support for all standard Rust format specifiers +- Dynamic formatting for custom types via the `DynDisplay` trait +- Macro support for convenient usage +- Support for various standard library types + +## Usage + +Add the crate to your project: + +```sh +cargo add dyf +``` + +## Examples + +### Basic Formatting + +```rust +use dyf::{dformat, FormatString}; + +let fmt = FormatString::from_string("Hello, {}!".to_string()).unwrap(); +let result = dformat!(&fmt, "world").unwrap(); +assert_eq!(result, format!("Hello, {}!", "world")); + +let num_fmt = FormatString::from_string("The answer is: {:>5}".to_string()).unwrap(); +let num = 42; +let result = dformat!(&num_fmt, num).unwrap(); +assert_eq!(result, format!("The answer is: {:>5}", num)); +``` + +### Advanced Formatting + +```rust +use dyf::{dformat, FormatString}; + +let fmt = FormatString::from_string("{:05} {:<10.2} {:^10}".to_string()).unwrap(); +let result = dformat!(&fmt, 42, 42.1234, "hello").unwrap(); +assert_eq!(result, format!("{:05} {:<10.2} {:^10}", 42, 42.1234, "hello")); +``` + +### Custom Type Formatting + +```rust +use dyf::{DynDisplay, Error, FormatSpec, dformat, FormatString}; + +struct Point { + x: i32, + y: i32, +} + +impl DynDisplay for Point { + fn dyn_fmt(&self, f: &FormatSpec) -> Result { + Ok(format!("Point({}, {})", self.x, self.y)) + } +} + +impl std::fmt::Display for Point { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "Point({}, {})", self.x, self.y) + } +} + +let point = Point { x: 10, y: 20 }; +let fmt = FormatString::from_string("{}".to_string()).unwrap(); +let result = dformat!(&fmt, point).unwrap(); +assert_eq!(result, format!("{}", point)); +``` + +### Integer Formatting + +```rust +use dyf::{dformat, FormatString}; + +// Decimal formatting +let fmt = FormatString::from_string("{}".to_string()).unwrap(); +let result = dformat!(&fmt, 42).unwrap(); +assert_eq!(result, format!("{}", 42)); + +let fmt = FormatString::from_string("{:5}".to_string()).unwrap(); +let result = dformat!(&fmt, 42).unwrap(); +assert_eq!(result, format!("{:5}", 42)); + +let fmt = FormatString::from_string("{:05}".to_string()).unwrap(); +let result = dformat!(&fmt, 42).unwrap(); +assert_eq!(result, format!("{:05}", 42)); + +let fmt = FormatString::from_string("{:+}".to_string()).unwrap(); +let result = dformat!(&fmt, 42).unwrap(); +assert_eq!(result, format!("{:+}", 42)); + +// Hexadecimal formatting +let fmt = FormatString::from_string("{:x}".to_string()).unwrap(); +let result = dformat!(&fmt, 42).unwrap(); +assert_eq!(result, format!("{:x}", 42)); + +let fmt = FormatString::from_string("{:X}".to_string()).unwrap(); +let result = dformat!(&fmt, 42).unwrap(); +assert_eq!(result, format!("{:X}", 42)); + +// Octal formatting +let fmt = FormatString::from_string("{:o}".to_string()).unwrap(); +let result = dformat!(&fmt, 42).unwrap(); +assert_eq!(result, format!("{:o}", 42)); + +// Binary formatting +let fmt = FormatString::from_string("{:b}".to_string()).unwrap(); +let result = dformat!(&fmt, 42).unwrap(); +assert_eq!(result, format!("{:b}", 42)); +``` + +### Float Formatting + +```rust +use dyf::{dformat, FormatString}; + +let fmt = FormatString::from_string("{}".to_string()).unwrap(); +let result = dformat!(&fmt, 42.0).unwrap(); +assert_eq!(result, format!("{}", 42.0)); + +let fmt = FormatString::from_string("{:e}".to_string()).unwrap(); +let result = dformat!(&fmt, 42.0).unwrap(); +assert_eq!(result, format!("{:e}", 42.0)); + +let fmt = FormatString::from_string("{:.2}".to_string()).unwrap(); +let result = dformat!(&fmt, 42.1234).unwrap(); +assert_eq!(result, format!("{:.2}", 42.1234)); + +let fmt = FormatString::from_string("{:10.2}".to_string()).unwrap(); +let result = dformat!(&fmt, 42.1234).unwrap(); +assert_eq!(result, format!("{:10.2}", 42.1234)); +``` + +### String Formatting + +```rust +use dyf::{dformat, FormatString}; + +let fmt = FormatString::from_string("{}".to_string()).unwrap(); +let result = dformat!(&fmt, "hello").unwrap(); +assert_eq!(result, format!("{}", "hello")); + +let fmt = FormatString::from_string("{:10}".to_string()).unwrap(); +let result = dformat!(&fmt, "hello").unwrap(); +assert_eq!(result, format!("{:10}", "hello")); + +let fmt = FormatString::from_string("{:.3}".to_string()).unwrap(); +let result = dformat!(&fmt, "hello").unwrap(); +assert_eq!(result, format!("{:.3}", "hello")); +``` + +## Supported Format Specifiers + +The crate supports all standard Rust format specifiers, including: + +| Category | Specifiers | +|----------|------------| +| Fill/Alignment | `<` `>` `^` | +| Sign | `+` `-` | +| Alternate | `#` | +| Zero-padding | `0` | +| Width | `{:5}` `{:width$}` | +| Precision | `{:.2}` `{:.precision$}` | +| Type | `?` `x` `X` `o` `b` `e` `E` `p` | + +## Performance Considerations + +The crate is designed with performance in mind. The `FormatString` can be created once and reused multiple times with different arguments. +This is particularly useful in scenarios where the same format string is used repeatedly. + +```rust +use dyf::{dformat, FormatString}; + +let fmt = FormatString::from_string("The value is: {:>10}".to_string()).unwrap(); +let result1 = dformat!(&fmt, 42).unwrap(); +let result2 = dformat!(&fmt, 100).unwrap(); +assert_eq!(result1, format!("The value is: {:>10}", 42)); +assert_eq!(result2, format!("The value is: {:>10}", 100)); +``` + +## Limitations + +While this crate aims to support all standard Rust format specifiers, there might be some edge cases that are not yet covered. +If you encounter any unsupported format specifiers or have suggestions for improvements, please open an issue on the GitHub repository. + +## Contributing + +Contributions are welcome! Please open an issue or submit a pull request on the GitHub repository. + +## License + +This project is licensed under the GNU General Public License v3.0 (GPL-3.0). +By using this software, you agree to the terms and conditions of this license. + +The full license text is available in the LICENSE file in the project root or at: +[https://www.gnu.org/licenses/gpl-3.0.html](https://www.gnu.org/licenses/gpl-3.0.html) + + diff --git a/src/imp.rs b/src/imp.rs index 6627b55..c5ed1c7 100644 --- a/src/imp.rs +++ b/src/imp.rs @@ -31,7 +31,7 @@ macro_rules! impl_debug { | FmtType::Ptr | FmtType::Bin | FmtType::LowExp - | FmtType::UpperExp => Err(Error::UnsupportedSpec), + | FmtType::UpperExp => Err(Error::UnsupportedSpec(f.clone())), } .map(|s| f.fill_and_align(s, Align::Left)) } @@ -60,7 +60,7 @@ macro_rules! impl_debug_display { | FmtType::Ptr | FmtType::Bin | FmtType::LowExp - | FmtType::UpperExp => Err(Error::UnsupportedSpec), + | FmtType::UpperExp => Err(Error::UnsupportedSpec(f.clone())), } .map(|s| f.fill_and_align(s, Align::Left)) } @@ -87,7 +87,7 @@ impl DynDisplay for char { | FmtType::Ptr | FmtType::Bin | FmtType::LowExp - | FmtType::UpperExp => Err(Error::UnsupportedSpec), + | FmtType::UpperExp => Err(Error::UnsupportedSpec(f.clone())), } .map(|s| f.fill_and_align(s, Align::Left)) } @@ -115,7 +115,7 @@ impl DynDisplay for *const T { | FmtType::Octal | FmtType::Bin | FmtType::LowExp - | FmtType::UpperExp => Err(Error::UnsupportedSpec), + | FmtType::UpperExp => Err(Error::UnsupportedSpec(f.clone())), } .map(|s| f.fill_and_align(s, Align::Right)) } @@ -141,7 +141,7 @@ impl DynDisplay for *mut T { | FmtType::Octal | FmtType::Bin | FmtType::LowExp - | FmtType::UpperExp => Err(Error::UnsupportedSpec), + | FmtType::UpperExp => Err(Error::UnsupportedSpec(f.clone())), } .map(|s| f.fill_and_align(s, Align::Right)) } @@ -187,7 +187,7 @@ impl DynDisplay for &str { | FmtType::Bin | FmtType::Octal | FmtType::LowExp - | FmtType::UpperExp => Err(Error::UnsupportedSpec), + | FmtType::UpperExp => Err(Error::UnsupportedSpec(f.clone())), } } } @@ -199,11 +199,11 @@ impl DynDisplay for str { } impl DynDisplay for String { - fn dyn_fmt(&self, t: &FormatSpec) -> Result { - if matches!(t.ty, FmtType::Ptr) { - return Err(Error::UnsupportedSpec); + fn dyn_fmt(&self, f: &FormatSpec) -> Result { + if matches!(f.ty, FmtType::Ptr) { + return Err(Error::UnsupportedSpec(f.clone())); } - DynDisplay::dyn_fmt(&self.as_str(), t) + DynDisplay::dyn_fmt(&self.as_str(), f) } } @@ -262,7 +262,7 @@ macro_rules! impl_dyn_display_float { | FmtType::Bin | FmtType::LowerHex | FmtType::Octal - | FmtType::UpperHex => Err(Error::UnsupportedSpec), + | FmtType::UpperHex => Err(Error::UnsupportedSpec(f.clone())), } .map(|s| f.fill_and_align(s, Align::Right)) } @@ -437,3 +437,9 @@ impl_debug!(PathBuf); // ffi impl_debug!(OsString); impl_debug!(&OsStr); + +impl DynDisplay for &dyn DynDisplay { + fn dyn_fmt(&self, f: &FormatSpec) -> Result { + (*self).dyn_fmt(f) + } +} diff --git a/src/lib.rs b/src/lib.rs index c0cb2fe..fb33c0c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,30 +1,357 @@ +#![deny(unused_imports)] +#![deny(missing_docs)] +//! # Dynamic String Formatting for Rust (dyf) +//! +//! The `dyf` crate brings dynamic string formatting to Rust while supporting the whole variety of string formats available in Rust. +//! It provides an easy way to implement dynamic formatting for custom types with the implementation of the `DynDisplay` trait. +//! +//! ## Features +//! +//! - Support for all standard Rust format specifiers +//! - Dynamic formatting for custom types via the `DynDisplay` trait +//! - Macro support for convenient usage +//! - Support for various standard library types +//! +//! ## Usage +//! +//! Add the crate to your project: +//! +//! ```sh +//! cargo add dyf +//! ``` +//! +//! ## Examples +//! +//! ### Basic Formatting +//! +//! ```rust +//! use dyf::{dformat, FormatString}; +//! +//! let fmt = FormatString::from_string("Hello, {}!".to_string()).unwrap(); +//! let result = dformat!(&fmt, "world").unwrap(); +//! assert_eq!(result, format!("Hello, {}!", "world")); +//! +//! let num_fmt = FormatString::from_string("The answer is: {:>5}".to_string()).unwrap(); +//! let num = 42; +//! let result = dformat!(&num_fmt, num).unwrap(); +//! assert_eq!(result, format!("The answer is: {:>5}", num)); +//! ``` +//! +//! ### Advanced Formatting +//! +//! ```rust +//! use dyf::{dformat, FormatString}; +//! +//! let fmt = FormatString::from_string("{:05} {:<10.2} {:^10}".to_string()).unwrap(); +//! let result = dformat!(&fmt, 42, 42.1234, "hello").unwrap(); +//! assert_eq!(result, format!("{:05} {:<10.2} {:^10}", 42, 42.1234, "hello")); +//! ``` +//! +//! ### Custom Type Formatting +//! +//! ```rust +//! use dyf::{DynDisplay, Error, FormatSpec, dformat, FormatString}; +//! +//! struct Point { +//! x: i32, +//! y: i32, +//! } +//! +//! impl DynDisplay for Point { +//! fn dyn_fmt(&self, f: &FormatSpec) -> Result { +//! Ok(format!("Point({}, {})", self.x, self.y)) +//! } +//! } +//! +//! impl std::fmt::Display for Point { +//! fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { +//! write!(f, "Point({}, {})", self.x, self.y) +//! } +//! } +//! +//! let point = Point { x: 10, y: 20 }; +//! let fmt = FormatString::from_string("{}".to_string()).unwrap(); +//! let result = dformat!(&fmt, point).unwrap(); +//! assert_eq!(result, format!("{}", point)); +//! ``` +//! +//! ### Integer Formatting +//! +//! ```rust +//! use dyf::{dformat, FormatString}; +//! +//! // Decimal formatting +//! let fmt = FormatString::from_string("{}".to_string()).unwrap(); +//! let result = dformat!(&fmt, 42).unwrap(); +//! assert_eq!(result, format!("{}", 42)); +//! +//! let fmt = FormatString::from_string("{:5}".to_string()).unwrap(); +//! let result = dformat!(&fmt, 42).unwrap(); +//! assert_eq!(result, format!("{:5}", 42)); +//! +//! let fmt = FormatString::from_string("{:05}".to_string()).unwrap(); +//! let result = dformat!(&fmt, 42).unwrap(); +//! assert_eq!(result, format!("{:05}", 42)); +//! +//! let fmt = FormatString::from_string("{:+}".to_string()).unwrap(); +//! let result = dformat!(&fmt, 42).unwrap(); +//! assert_eq!(result, format!("{:+}", 42)); +//! +//! // Hexadecimal formatting +//! let fmt = FormatString::from_string("{:x}".to_string()).unwrap(); +//! let result = dformat!(&fmt, 42).unwrap(); +//! assert_eq!(result, format!("{:x}", 42)); +//! +//! let fmt = FormatString::from_string("{:X}".to_string()).unwrap(); +//! let result = dformat!(&fmt, 42).unwrap(); +//! assert_eq!(result, format!("{:X}", 42)); +//! +//! // Octal formatting +//! let fmt = FormatString::from_string("{:o}".to_string()).unwrap(); +//! let result = dformat!(&fmt, 42).unwrap(); +//! assert_eq!(result, format!("{:o}", 42)); +//! +//! // Binary formatting +//! let fmt = FormatString::from_string("{:b}".to_string()).unwrap(); +//! let result = dformat!(&fmt, 42).unwrap(); +//! assert_eq!(result, format!("{:b}", 42)); +//! ``` +//! +//! ### Float Formatting +//! +//! ```rust +//! use dyf::{dformat, FormatString}; +//! +//! let fmt = FormatString::from_string("{}".to_string()).unwrap(); +//! let result = dformat!(&fmt, 42.0).unwrap(); +//! assert_eq!(result, format!("{}", 42.0)); +//! +//! let fmt = FormatString::from_string("{:e}".to_string()).unwrap(); +//! let result = dformat!(&fmt, 42.0).unwrap(); +//! assert_eq!(result, format!("{:e}", 42.0)); +//! +//! let fmt = FormatString::from_string("{:.2}".to_string()).unwrap(); +//! let result = dformat!(&fmt, 42.1234).unwrap(); +//! assert_eq!(result, format!("{:.2}", 42.1234)); +//! +//! let fmt = FormatString::from_string("{:10.2}".to_string()).unwrap(); +//! let result = dformat!(&fmt, 42.1234).unwrap(); +//! assert_eq!(result, format!("{:10.2}", 42.1234)); +//! ``` +//! +//! ### String Formatting +//! +//! ```rust +//! use dyf::{dformat, FormatString}; +//! +//! let fmt = FormatString::from_string("{}".to_string()).unwrap(); +//! let result = dformat!(&fmt, "hello").unwrap(); +//! assert_eq!(result, format!("{}", "hello")); +//! +//! let fmt = FormatString::from_string("{:10}".to_string()).unwrap(); +//! let result = dformat!(&fmt, "hello").unwrap(); +//! assert_eq!(result, format!("{:10}", "hello")); +//! +//! let fmt = FormatString::from_string("{:.3}".to_string()).unwrap(); +//! let result = dformat!(&fmt, "hello").unwrap(); +//! assert_eq!(result, format!("{:.3}", "hello")); +//! ``` +//! +//! ## Supported Format Specifiers +//! +//! The crate supports all standard Rust format specifiers, including: +//! +//! | Category | Specifiers | +//! |----------|------------| +//! | Fill/Alignment | `<` `>` `^` | +//! | Sign | `+` `-` | +//! | Alternate | `#` | +//! | Zero-padding | `0` | +//! | Width | `{:5}` `{:width$}` | +//! | Precision | `{:.2}` `{:.precision$}` | +//! | Type | `?` `x` `X` `o` `b` `e` `E` `p` | +//! +//! ## Performance Considerations +//! +//! The crate is designed with performance in mind. The `FormatString` can be created once and reused multiple times with different arguments. +//! This is particularly useful in scenarios where the same format string is used repeatedly. +//! +//! ```rust +//! use dyf::{dformat, FormatString}; +//! +//! let fmt = FormatString::from_string("The value is: {:>10}".to_string()).unwrap(); +//! let result1 = dformat!(&fmt, 42).unwrap(); +//! let result2 = dformat!(&fmt, 100).unwrap(); +//! assert_eq!(result1, format!("The value is: {:>10}", 42)); +//! assert_eq!(result2, format!("The value is: {:>10}", 100)); +//! ``` +//! +//! ## Limitations +//! +//! While this crate aims to support all standard Rust format specifiers, there might be some edge cases that are not yet covered. +//! If you encounter any unsupported format specifiers or have suggestions for improvements, please open an issue on the GitHub repository. +//! +//! ## Contributing +//! +//! Contributions are welcome! Please open an issue or submit a pull request on the GitHub repository. +//! +//! ## License +//! +//! This project is licensed under the GNU General Public License v3.0 (GPL-3.0). +//! By using this software, you agree to the terms and conditions of this license. +//! +//! The full license text is available in the LICENSE file in the project root or at: +//! [https://www.gnu.org/licenses/gpl-3.0.html](https://www.gnu.org/licenses/gpl-3.0.html) + use std::{ borrow::Cow, fmt::{Debug, Display}, }; use pest::{Parser, iterators::Pair}; -use pest_derive::Parser; use thiserror::Error; mod imp; - -#[derive(Parser)] -#[grammar = "rust_fmt.pest"] // relative to src -struct FmtParser; - -#[derive(Debug)] +mod parser; +use parser::{FmtParser, Rule}; + +/// Specifies the type of formatting to apply to a value. +/// +/// The `FmtType` enum represents the various format types that can be specified +/// in a format string after the colon. These determine how values are converted +/// to strings, including different representations for numbers, debugging output, +/// and other special formats. +/// +/// # Format String Representation +/// +/// In format strings, these types are represented as follows: +/// +/// | FmtType | Format Specifier | Description | +/// |---------|------------------|-------------| +/// | Default | (none) | Default formatting for the type | +/// | Debug | `?` | Debug representation | +/// | DebugLowHex | `x?` | Debug representation with lowercase hexadecimal | +/// | DebugUpHex | `X?` | Debug representation with uppercase hexadecimal | +/// | LowerHex | `x` | Lowercase hexadecimal | +/// | UpperHex | `X` | Uppercase hexadecimal | +/// | Octal | `o` | Octal representation | +/// | Ptr | `p` | Pointer address | +/// | Bin | `b` | Binary representation | +/// | LowExp | `e` | Lowercase exponential notation | +/// | UpperExp | `E` | Uppercase exponential notation | +/// +/// # Examples +/// +/// Basic usage with format specifications: +/// +/// ``` +/// use dyf::{FmtType, FormatSpec}; +/// +/// // Create a format specification for hexadecimal output +/// let hex_spec = FormatSpec { +/// ty: FmtType::LowerHex, +/// ..Default::default() +/// }; +/// +/// // Create a format specification for debug output +/// let debug_spec = FormatSpec { +/// ty: FmtType::Debug, +/// ..Default::default() +/// }; +/// ``` +/// +/// Using with custom formatting: +/// +/// ``` +/// use dyf::{FmtType, FormatSpec, DynDisplay, Error}; +/// +/// struct Color { +/// r: u8, +/// g: u8, +/// b: u8, +/// } +/// +/// impl DynDisplay for Color { +/// fn dyn_fmt(&self, spec: &FormatSpec) -> Result { +/// match spec.ty { +/// FmtType::LowerHex => Ok(format!( +/// "#{:02x}{:02x}{:02x}", +/// self.r, self.g, self.b +/// )), +/// FmtType::UpperHex => Ok(format!( +/// "#{:02X}{:02X}{:02X}", +/// self.r, self.g, self.b +/// )), +/// FmtType::Debug => Ok(format!( +/// "Color {{ r: {}, g: {}, b: {} }}", +/// self.r, self.g, self.b +/// )), +/// _ => Ok(format!( +/// "RGB({}, {}, {})", +/// self.r, self.g, self.b +/// )), +/// } +/// } +/// } +/// ``` +#[derive(Debug, Clone, Copy)] pub enum FmtType { + /// Default formatting for the type. + /// + /// This uses the standard display formatting for the type, equivalent to not + /// specifying a format type in the format string. Default, + + /// Debug representation. + /// + /// This uses the debug formatting for the type, equivalent to the `{:?}` format specifier. Debug, + + /// Debug representation with lowercase hexadecimal. + /// + /// This combines debug formatting with lowercase hexadecimal representation. DebugLowHex, + + /// Debug representation with uppercase hexadecimal. + /// + /// This combines debug formatting with uppercase hexadecimal representation. DebugUpHex, + + /// Lowercase hexadecimal representation. + /// + /// This formats numbers in lowercase hexadecimal, equivalent to the `{:x}` format specifier. LowerHex, + + /// Uppercase hexadecimal representation. + /// + /// This formats numbers in uppercase hexadecimal, equivalent to the `{:X}` format specifier. UpperHex, + + /// Octal representation. + /// + /// This formats numbers in octal (base-8), equivalent to the `{:o}` format specifier. Octal, + + /// Pointer address representation. + /// + /// This formats pointer values as memory addresses, equivalent to the `{:p}` format specifier. Ptr, + + /// Binary representation. + /// + /// This formats numbers in binary (base-2), equivalent to the `{:b}` format specifier. Bin, + + /// Lowercase exponential notation. + /// + /// This formats floating-point numbers in scientific notation with lowercase 'e', + /// equivalent to the `{:e}` format specifier. LowExp, + + /// Uppercase exponential notation. + /// + /// This formats floating-point numbers in scientific notation with uppercase 'E', + /// equivalent to the `{:E}` format specifier. UpperExp, } @@ -46,10 +373,29 @@ impl Display for FmtType { } } +/// Specifies the alignment of formatted text within a field width. +/// +/// The `Align` enum determines how text should be aligned when a width is specified +/// in a format specification. It controls whether the text is left-aligned, right-aligned, +/// or centered within the allocated space. #[derive(Debug, Clone, Copy)] pub enum Align { + /// Left-align the text within the field. + /// + /// When this alignment is used, text is placed at the beginning of the field, + /// with any padding added to the right. Left, + + /// Center the text within the field. + /// + /// When this alignment is used, text is placed in the middle of the field, + /// with padding distributed equally on both sides when possible. Center, + + /// Right-align the text within the field. + /// + /// When this alignment is used, text is placed at the end of the field, + /// with any padding added to the left. Right, } @@ -63,9 +409,30 @@ impl Display for Align { } } +/// Specifies how signs should be displayed for numeric values. +/// +/// The `Sign` enum controls the display of signs for numeric values in formatted output. +/// It determines whether positive numbers should show a plus sign, only negative numbers +/// should show a minus sign, or no special sign handling should be applied. +/// +/// # Format Specification +/// +/// In format strings, these correspond to: +/// - `+` for `Sign::Positive` (show signs for both positive and negative numbers) +/// - `-` for `Sign::Negative` (show signs only for negative numbers, default behavior) #[derive(Debug, Clone, Copy)] pub enum Sign { + /// Always show the sign for numeric values. + /// + /// Positive numbers will be prefixed with `+`, and negative numbers with `-`. + /// This corresponds to the `+` format specifier. Positive, + + /// Only show the sign for negative numbers. + /// + /// Negative numbers will be prefixed with `-`, while positive numbers will + /// have no sign prefix. This is the default behavior and corresponds to + /// the `-` format specifier. Negative, } @@ -78,15 +445,52 @@ impl Display for Sign { } } -#[derive(Debug)] +/// A complete format specification for dynamic formatting. +/// +/// `FormatSpec` represents all the components of a format specification that can appear +/// between the colons in a format string: `"{:<5.2}"`. It controls how values are formatted +/// including alignment, padding, width, precision, and type-specific formatting. +/// +/// # Format String Components +/// +/// A format specification in a string typically looks like: +/// `:[fill][align][sign][#][0][width][.precision][type]` +#[derive(Debug, Clone)] pub struct FormatSpec { + /// The fill character to use for padding. + /// + /// If `None`, spaces will be used for padding. pub fill: Option, + + /// The alignment of the formatted value within the field. + /// + /// If `None`, the default alignment (typically right for numbers, left for text) will be used. pub align: Option, + + /// The sign display option for numeric values. + /// + /// If `None`, signs will only be shown for negative numbers. pub sign: Option, + + /// Whether to use alternate formatting. + /// + /// For example, adding `0x` prefix to hexadecimal numbers or always showing the decimal point. pub alternate: bool, + + /// Whether to pad with zeros instead of the fill character. + /// + /// This is typically used for numeric types to ensure a minimum number of digits. pub zero: bool, + + /// The minimum width of the formatted field. + /// + /// If the formatted value is shorter than this width, it will be padded according to the alignment. pub width: Option, + + /// The precision for floating-point numbers or maximum length for strings. pub precision: Option, + + /// The format type specification. pub ty: FmtType, } @@ -192,6 +596,12 @@ impl FormatSpec { && matches!(self.ty, FmtType::Default) } + /// Applies fill and alignment to a string according to this format specification. + /// + /// # Arguments + /// + /// * `s` - The string to format + /// * `default_align` - The default alignment to use if none is specified pub fn fill_and_align(&self, s: String, default_align: Align) -> String { let width = self.width.unwrap_or(s.len()); if width <= s.len() { @@ -247,19 +657,134 @@ impl Display for Format { } } +/// Errors that can occur during dynamic formatting. +/// +/// This enum represents all possible errors that can occur during the parsing and +/// application of format specifications. #[derive(Debug, Error)] pub enum Error { - #[error("unsupported format spec")] - UnsupportedSpec, - #[error("format strings don't match with number of arguments")] - FormatVsArgs, - #[error("unknown parsing rule={0}")] - UnknownParsingRule(String), + /// An unsupported format specification was encountered. + /// + /// This error occurs when a format specification contains options or combinations + /// that are not supported by the formatting machinery. + #[error("unsupported format spec {0}")] + UnsupportedSpec(FormatSpec), + + /// The number of arguments doesn't match the number of format specifications. + /// + /// This error occurs when the number of arguments provided to a format string + /// doesn't match the number of format specifications in the string. + /// + /// # Examples + /// + /// Providing too few arguments: + /// + /// ```rust + /// use dyf::{FormatString, dformat, Error}; + /// + /// let fmt = FormatString::from_string("{}, {}".to_string()).unwrap(); + /// let result = dformat!(&fmt, "only one argument"); + /// assert!(matches!(result, Err(Error::ArgumentCountMismatch(2, 1)))); + /// ``` + /// + /// Providing too many arguments: + /// + /// ```rust + /// use dyf::{FormatString, dformat, Error}; + /// + /// let fmt = FormatString::from_string("{}".to_string()).unwrap(); + /// let result = dformat!(&fmt, "one", "extra"); + /// assert!(matches!(result, Err(Error::ArgumentCountMismatch(1, 2)))); + /// ``` + #[error( + "number of arguments doesn't match number of format specifications expected={0} found={1}" + )] + ArgumentCountMismatch(usize, usize), + + /// An error occurred during format string parsing. + /// + /// This error wraps parsing errors from the underlying [`pest`] parser and provides + /// information about syntax errors in format strings. #[error("format parsing error: {0}")] Parse(#[from] Box>), } +/// A trait for dynamic display formatting. +/// +/// This trait provides a way to implement custom formatting for types that need to support +/// dynamic format specifications at runtime. It's similar to the standard [`std::fmt::Display`] trait +/// but with additional formatting control through [`FormatSpec`]. +/// +/// # Examples +/// +/// Basic implementation for a custom type: +/// +/// ``` +/// use dyf::{DynDisplay, FormatSpec, Error}; +/// +/// struct Point { +/// x: i32, +/// y: i32, +/// } +/// +/// impl DynDisplay for Point { +/// fn dyn_fmt(&self, spec: &FormatSpec) -> Result { +/// let s = format!("Point({}, {})", self.x, self.y); +/// Ok(spec.fill_and_align(s, dyf::Align::Left)) +/// } +/// } +/// ``` +/// +/// Implementation with format-aware behavior: +/// +/// ``` +/// use dyf::{DynDisplay, FormatSpec, Error, FmtType}; +/// +/// struct Color { +/// r: u8, +/// g: u8, +/// b: u8, +/// } +/// +/// impl DynDisplay for Color { +/// fn dyn_fmt(&self, spec: &FormatSpec) -> Result { +/// match spec.ty { +/// FmtType::LowerHex => Ok(format!( +/// "#{:02x}{:02x}{:02x}", +/// self.r, self.g, self.b +/// )), +/// FmtType::UpperHex => Ok(format!( +/// "#{:02X}{:02X}{:02X}", +/// self.r, self.g, self.b +/// )), +/// FmtType::Debug => Ok(format!( +/// "Color {{ r: {}, g: {}, b: {} }}", +/// self.r, self.g, self.b +/// )), +/// _ => Ok(format!( +/// "RGB({}, {}, {})", +/// self.r, self.g, self.b +/// )), +/// }.map(|s| spec.fill_and_align(s, dyf::Align::Left)) +/// } +/// } +/// ``` pub trait DynDisplay { + /// Formats the value using the given format specification. + /// + /// # Arguments + /// + /// * `spec` - The format specification containing alignment, width, precision, + /// and other formatting options + /// + /// # Returns + /// + /// A `Result` containing the formatted string or an error if formatting fails. + /// + /// # Errors + /// + /// This function may return an error if the format specification is not supported + /// for this type or if formatting fails for other reasons. fn dyn_fmt(&self, f: &FormatSpec) -> Result; } @@ -294,6 +819,43 @@ impl Format { } } +/// A parsed format string that can be used for dynamic formatting. +/// +/// `FormatString` represents a string with embedded format specifications that has been +/// parsed and can be used with the [`dformat!`] macro to perform dynamic formatting operations. +/// It contains the original string along with information about the format specifications +/// found within it. +/// +/// # Examples +/// +/// Basic usage: +/// +/// ``` +/// use dyf::FormatString; +/// +/// let fmt = FormatString::from_string("Hello, {}!".to_string()).unwrap(); +/// assert!(fmt.contains_format()); +/// ``` +/// +/// Creating and using a format string: +/// +/// ``` +/// use dyf::{FormatString, dformat}; +/// +/// let fmt = FormatString::from_string("{:>10} {:.2}".to_string()).unwrap(); +/// let result = dformat!(&fmt, 42, 3.14159).unwrap(); +/// assert_eq!(result, " 42 3.14"); +/// ``` +/// +/// Converting between string types: +/// +/// ``` +/// use dyf::FormatString; +/// +/// let fmt = FormatString::from_string("Value: {:05}".to_string()).unwrap(); +/// let fmt_str = fmt.to_string_lossy(); +/// let owned_str = fmt.into_string(); +/// ``` #[derive(Debug)] pub struct FormatString { s: String, @@ -321,29 +883,144 @@ impl FormatString { }) } + /// Creates a new `FormatString` from a string. + /// + /// # Arguments + /// + /// * `s` - The string containing format specifications + /// + /// # Returns + /// + /// A `Result` containing the parsed [`FormatString`] or an error if parsing fails. + /// + /// # Errors + /// + /// This function may return an error if the input string contains invalid format + /// specifications that cannot be parsed. + /// + /// # Examples + /// + /// ``` + /// use dyf::FormatString; + /// + /// let fmt = FormatString::from_string("Hello, {}!".to_string()).unwrap(); + /// ``` pub fn from_string(s: String) -> Result { Self::new_from_str(s) } + /// Converts the [`FormatString`] into its inner string. + /// + /// This consumes the [`FormatString`] and returns the original string that was used + /// to create it. + /// + /// # Examples + /// + /// ``` + /// use dyf::FormatString; + /// + /// let fmt = FormatString::from_string("Test: {}".to_string()).unwrap(); + /// let s = fmt.into_string(); + /// assert_eq!(s, "Test: {}"); + /// ``` pub fn into_string(self) -> String { self.s } + /// Returns a borrowed version of the format string. + /// + /// This provides access to the original string without consuming the `FormatString`. + /// + /// # Examples + /// + /// ``` + /// use dyf::FormatString; + /// + /// let fmt = FormatString::from_string("Value: {:.2}".to_string()).unwrap(); + /// let borrowed = fmt.to_string_lossy(); + /// assert_eq!(&*borrowed, "Value: {:.2}"); + /// ``` pub fn to_string_lossy(&self) -> Cow<'_, str> { Cow::Borrowed(&self.s) } + /// Returns `true` if the format string contains any format specifications. + /// + /// # Examples + /// + /// ``` + /// use dyf::FormatString; + /// + /// let with_fmt = FormatString::from_string("Hello, {}!".to_string()).unwrap(); + /// assert!(with_fmt.contains_format()); + /// + /// let without_fmt = FormatString::from_string("Hello, world!".to_string()).unwrap(); + /// assert!(!without_fmt.contains_format()); + /// ``` pub fn contains_format(&self) -> bool { !self.fmts.is_empty() } } +/// A formatter that applies format specifications to values. +/// +/// The `Formatter` struct collects arguments and applies format specifications to them +/// to build the final formatted string. Unlike the previous version, it now collects +/// all arguments first using `push_arg` and then performs the formatting in one operation +/// with the `format` method. +/// +/// # Examples +/// +/// Basic usage with a format string: +/// +/// ``` +/// use dyf::{FormatString, Formatter}; +/// +/// let fmt = FormatString::from_string("Hello, {}!".to_string()).unwrap(); +/// let mut formatter = Formatter::from(&fmt); +/// formatter.push_arg(&"world").format().unwrap(); +/// assert_eq!(formatter.into_string(), "Hello, world!"); +/// ``` +/// +/// Formatting multiple values: +/// +/// ``` +/// use dyf::{FormatString, Formatter}; +/// +/// let fmt = FormatString::from_string("{}, {}!".to_string()).unwrap(); +/// let mut formatter = Formatter::from(&fmt); +/// formatter.push_arg(&"Hello").push_arg(&"world").format().unwrap(); +/// assert_eq!(formatter.into_string(), "Hello, world!"); +/// ``` +/// +/// Using with custom types: +/// +/// ``` +/// use dyf::{FormatString, Formatter, DynDisplay, Error, FormatSpec}; +/// +/// struct Point { +/// x: i32, +/// y: i32, +/// } +/// +/// impl DynDisplay for Point { +/// fn dyn_fmt(&self, f: &FormatSpec) -> Result { +/// let s = format!("Point({}, {})", self.x, self.y); +/// Ok(f.fill_and_align(s, dyf::Align::Left)) +/// } +/// } +/// +/// let fmt = FormatString::from_string("Point: {}".to_string()).unwrap(); +/// let point = Point { x: 10, y: 20 }; +/// let mut formatter = Formatter::from(&fmt); +/// formatter.push_arg(&point).format().unwrap(); +/// assert_eq!(formatter.into_string(), "Point: Point(10, 20)"); +/// ``` pub struct Formatter<'s> { /// index in the format string i: usize, - /// index of of the format string argument - i_arg: usize, format_string: &'s FormatString, + args: Vec<&'s dyn DynDisplay>, out: String, } @@ -351,51 +1028,238 @@ impl<'s> From<&'s FormatString> for Formatter<'s> { fn from(value: &'s FormatString) -> Self { Self { i: 0, - i_arg: 0, format_string: value, + args: vec![], out: String::new(), } } } -impl Formatter<'_> { - pub fn format_arg(&mut self, arg: &A) -> Result<&mut Self, Error> { - let Some(arg_fmt) = &self.format_string.fmts.get(self.i_arg) else { - return Ok(self); - }; +impl<'s> Formatter<'s> { + /// Adds an argument to be formatted. + /// + /// This method collects arguments that will be formatted when `format` is called. + /// It supports method chaining for convenient use. + /// + /// # Arguments + /// + /// * `arg` - The argument to format, which must implement `DynDisplay` + /// + /// # Returns + /// + /// A mutable reference to the formatter for method chaining. + /// + /// # Examples + /// + /// ``` + /// use dyf::{FormatString, Formatter}; + /// + /// let fmt = FormatString::from_string("{}, {}!".to_string()).unwrap(); + /// let mut formatter = Formatter::from(&fmt); + /// formatter.push_arg(&"Hello").push_arg(&"world"); + /// ``` + pub fn push_arg(&mut self, arg: &'s A) -> &mut Self + where + A: DynDisplay, + { + self.args.push(arg); + self + } + + /// Applies the format specifications to all collected arguments. + /// + /// This method performs the actual formatting after all arguments have been collected + /// with `push_arg`. It verifies that the number of arguments matches the number of + /// format specifications in the format string. + /// + /// # Returns + /// + /// A mutable reference to the formatter for method chaining. + /// + /// # Errors + /// + /// Returns an error if the number of arguments doesn't match the number of format + /// specifications, or if any individual formatting operation fails. + /// + /// # Examples + /// + /// ``` + /// use dyf::{FormatString, Formatter}; + /// + /// let fmt = FormatString::from_string("{:>5}, {:<5}".to_string()).unwrap(); + /// let mut formatter = Formatter::from(&fmt); + /// formatter.push_arg(&42).push_arg(&"hello").format().unwrap(); + /// assert_eq!(formatter.into_string(), " 42, hello"); + /// ``` + pub fn format(&mut self) -> Result<&mut Self, Error> { + if self.args.len() != self.format_string.fmts.len() { + return Err(Error::ArgumentCountMismatch( + // expected + self.format_string.fmts.len(), + // found + self.args.len(), + )); + } + + for (i, a) in self.args.iter().enumerate() { + // this cannot panic as lengths are equal + let arg_fmt = &self.format_string.fmts[i]; + let slice = &self.format_string.s.as_str()[self.i..]; + self.out.push_str(&slice[..arg_fmt.start - self.i]); + self.out.push_str(&arg_fmt.dyn_fmt_arg(a)?); + self.i = arg_fmt.end; + } + // we copy the rest of the string let slice = &self.format_string.s.as_str()[self.i..]; - self.out.push_str(&slice[..arg_fmt.start - self.i]); - self.out.push_str(&arg_fmt.dyn_fmt_arg(arg)?); - self.i = arg_fmt.end; - self.i_arg += 1; + self.out.push_str(slice); Ok(self) } + /// Returns a borrowed version of the formatted string. + /// + /// This provides access to the current state of the formatted string without + /// consuming the formatter. + /// + /// # Examples + /// + /// ``` + /// use dyf::{FormatString, Formatter}; + /// + /// let fmt = FormatString::from_string("Value: {}".to_string()).unwrap(); + /// let mut formatter = Formatter::from(&fmt); + /// formatter.push_arg(&42).format().unwrap(); + /// let borrowed = formatter.to_string_lossy(); + /// assert_eq!(&*borrowed, "Value: 42"); + /// ``` pub fn to_string_lossy(&self) -> Cow<'_, str> { Cow::Borrowed(&self.out) } + /// Consumes the formatter and returns the formatted string. + /// + /// This finalizes the formatting process and returns the complete formatted string. + /// + /// # Examples + /// + /// ``` + /// use dyf::{FormatString, Formatter}; + /// + /// let fmt = FormatString::from_string("The answer is: {}".to_string()).unwrap(); + /// let mut formatter = Formatter::from(&fmt); + /// formatter.push_arg(&42).format().unwrap(); + /// let result = formatter.into_string(); + /// assert_eq!(result, "The answer is: 42"); + /// ``` pub fn into_string(self) -> String { self.out } } +/// Dynamically formats values according to a format string. +/// +/// The `dformat!` macro provides functionality similar to Rust's standard `format!` macro, +/// but with dynamic formatting capabilities. It uses a pre-parsed `FormatString` to +/// apply format specifications to values at runtime. +/// +/// # Syntax +/// +/// The macro takes two arguments: +/// - A reference to a `FormatString` that has been created from a format string +/// - A list of arguments to format +/// +/// ```ignore +/// dformat!(format_string_ref, arg1, arg2, ...) +/// ``` +/// +/// # Examples +/// +/// Basic usage: +/// +/// ``` +/// use dyf::{FormatString, dformat}; +/// +/// let fmt = FormatString::from_string("Hello, {}!".to_string()).unwrap(); +/// let result = dformat!(&fmt, "world").unwrap(); +/// assert_eq!(result, "Hello, world!"); +/// ``` +/// +/// Formatting with different specifications: +/// +/// ``` +/// use dyf::{FormatString, dformat}; +/// +/// let fmt = FormatString::from_string("{:>5}, {:.2}".to_string()).unwrap(); +/// let result = dformat!(&fmt, 42, 3.14159).unwrap(); +/// assert_eq!(result, " 42, 3.14"); +/// ``` +/// +/// Using with custom types that implement `DynDisplay`: +/// +/// ``` +/// use dyf::{FormatString, dformat, DynDisplay, Error, FormatSpec}; +/// +/// struct Point { +/// x: i32, +/// y: i32, +/// } +/// +/// impl DynDisplay for Point { +/// fn dyn_fmt(&self, spec: &FormatSpec) -> Result { +/// let s = format!("Point({}, {})", self.x, self.y); +/// Ok(spec.fill_and_align(s, dyf::Align::Left)) +/// } +/// } +/// +/// let fmt = FormatString::from_string("Point: {}".to_string()).unwrap(); +/// let point = Point { x: 10, y: 20 }; +/// let result = dformat!(&fmt, point).unwrap(); +/// assert_eq!(result, "Point: Point(10, 20)"); +/// ``` +/// +/// # Errors +/// +/// The macro returns a `Result` which may contain: +/// - `Error::FormatVsArgs` if the number of arguments doesn't match the format specifications +/// - `Error::UnsupportedSpec` if an unsupported format specifier is used +/// - Other formatting errors that may occur during the dynamic formatting process +/// +/// # Performance Considerations +/// +/// For best performance when formatting the same string multiple times: +/// 1. Create the `FormatString` once and reuse it +/// 2. Use the `dformat!` macro for each formatting operation +/// +/// ``` +/// use dyf::{FormatString, dformat}; +/// +/// let fmt = FormatString::from_string("Value: {:>10}".to_string()).unwrap(); +/// let result1 = dformat!(&fmt, 42).unwrap(); +/// let result2 = dformat!(&fmt, "text").unwrap(); +/// ``` +/// +/// # Comparison with Standard `format!` Macro +/// +/// While similar to Rust's standard `format!` macro, `dformat!` provides: +/// - Dynamic formatting capabilities through the `DynDisplay` trait +/// - The ability to pre-parse format strings for reuse +/// - More flexible error handling +/// +/// However, for simple cases where you don't need these features, the standard `format!` +/// macro may be more convenient. #[macro_export] macro_rules! dformat { ($fmt: expr, $($arg: expr),*) => { { - let mut last_err = None; let mut fs = $crate::Formatter::from($fmt); $( - if let Err(e)=fs.format_arg(&$arg) { - last_err = Some(e); - } + fs.push_arg(&$arg); )* - match last_err { - Some(e) => Err(e), - None => Ok(fs.into_string()), + + match fs.format() { + Err(e) => Err(e), + Ok(_) => Ok(fs.into_string()), } } }; @@ -525,6 +1389,10 @@ mod tests { fn test_string_formatting() { // String formatting assert_eq!(format!("{}", "hello"), dformat_lit!("{}", "hello").unwrap()); + assert_eq!( + format!("Hello, {}!", "world"), + dformat_lit!("Hello, {}!", "world").unwrap() + ); assert_eq!( format!("{:10}", "hello"), dformat_lit!("{:10}", "hello").unwrap() diff --git a/src/parser.rs b/src/parser.rs new file mode 100644 index 0000000..439f42d --- /dev/null +++ b/src/parser.rs @@ -0,0 +1,6 @@ +use pest_derive::Parser; + +#[allow(missing_docs)] +#[derive(Parser)] +#[grammar = "rust_fmt.pest"] // relative to src +pub(crate) struct FmtParser;