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..fc17657 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,16 @@ +[package] +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" +pest_derive = "2.8.1" +thiserror = "2.0.12" 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 new file mode 100644 index 0000000..c5ed1c7 --- /dev/null +++ b/src/imp.rs @@ -0,0 +1,445 @@ +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(f.clone())), + } + .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(f.clone())), + } + .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(f.clone())), + } + .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(f.clone())), + } + .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(f.clone())), + } + .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(f.clone())), + } + } +} + +impl DynDisplay for str { + fn dyn_fmt(&self, f: &FormatSpec) -> Result { + DynDisplay::dyn_fmt(&self, f) + } +} + +impl DynDisplay for String { + 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(), f) + } +} + +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(f.clone())), + } + .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); + +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 new file mode 100644 index 0000000..fb33c0c --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,1643 @@ +#![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 thiserror::Error; + +mod imp; +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, +} + +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"), + } + } +} + +/// 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, +} + +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, ">"), + } + } +} + +/// 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, +} + +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, "-"), + } + } +} + +/// 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, +} + +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) + } +} + +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, + } + } +} + +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) + } + + /// 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() { + 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 + } + } +} + +#[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, "}}") + } +} + +/// 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 { + /// 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; +} + +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 { + start, + end, + arg, + spec: spec.unwrap_or_default(), + } + } + + fn dyn_fmt_arg(&self, arg: &D) -> Result { + DynDisplay::dyn_fmt(arg, &self.spec) + } +} + +/// 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, + fmts: Vec, +} + +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 { + // WARNING: here anything else than Rule::format is ignored + if p.as_rule() == Rule::format { + fmts.push(Format::from_pair(p)) + } + } + + Ok(Self { + s: s.as_ref().to_string(), + fmts, + }) + } + + /// 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, + format_string: &'s FormatString, + args: Vec<&'s dyn DynDisplay>, + out: String, +} + +impl<'s> From<&'s FormatString> for Formatter<'s> { + fn from(value: &'s FormatString) -> Self { + Self { + i: 0, + format_string: value, + args: vec![], + out: String::new(), + } + } +} + +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); + + 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 fs = $crate::Formatter::from($fmt); + $( + fs.push_arg(&$arg); + )* + + match fs.format() { + Err(e) => Err(e), + Ok(_) => Ok(fs.into_string()), + } + } + }; +} + +#[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}", + ] { + 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_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_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() + ); + 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_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/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; diff --git a/src/rust_fmt.pest b/src/rust_fmt.pest new file mode 100644 index 0000000..7594c85 --- /dev/null +++ b/src/rust_fmt.pest @@ -0,0 +1,30 @@ +format_string = _{ SOI ~ (maybe_format | ANY)* ~ EOI } +maybe_format = _{ ("{" ~ "{" | "}" ~ "}") | format } +format = { "{" ~ argument? ~ (":" ~ format_spec)? ~ WHITESPACE* ~ "}" } +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 } +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_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 | "_")+ } + +WHITESPACE = _{ " " | "\t" }