1use crate::{parse, Align, Base, Builder, Dynamic, Numeric, Sign};
2use iterext::prelude::*;
3use std::{any::type_name, collections::VecDeque, str::FromStr};
4
5#[derive(Debug, thiserror::Error, PartialEq, Eq, Clone)]
6pub enum Error {
7 #[error("Zero formatter is only compatible with Align::Right or Align::Decimal")]
8 IncompatibleAlignment,
9 #[error("{0:?} formatting not implemented for {1}")]
10 NotImplemented(Base, &'static str),
11}
12
13#[derive(Clone, PartialEq, Eq, Debug, Default)]
15pub struct NumFmt {
16 pub(crate) fill: Option<char>,
17 pub(crate) align: Align,
18 pub(crate) sign: Sign,
19 pub(crate) hash: bool,
20 pub(crate) zero: bool,
21 pub(crate) width: usize,
22 pub(crate) precision: Option<usize>,
23 pub(crate) base: Base,
24 pub(crate) separator: Option<char>,
25 pub(crate) spacing: Option<usize>,
26 pub(crate) decimal_separator: Option<char>,
27}
28
29impl NumFmt {
30 pub fn builder() -> Builder {
32 Builder::default()
33 }
34
35 pub fn from_str(s: &str) -> Result<Self, parse::Error> {
39 parse::parse(s)
40 }
41
42 #[inline]
43 fn width_desired(&self, dynamic: Dynamic) -> usize {
44 let mut width_desired = self.width_with(dynamic);
45 if self.hash() {
46 width_desired = width_desired.saturating_sub(2);
47 }
48 if width_desired == 0 {
49 width_desired = 1;
50 }
51
52 width_desired
53 }
54
55 fn normalize(&self, digits: impl Iterator<Item = char>, dynamic: Dynamic) -> VecDeque<char> {
61 let pad_to = if self.zero() {
62 self.width_desired(dynamic)
63 } else {
64 1
65 };
66
67 let pad_char = if self.zero() { '0' } else { self.fill() };
68
69 let mut digits = digits.peekable();
70 let mut digits: Box<dyn Iterator<Item = char>> = if digits.peek().is_some() {
71 Box::new(digits)
72 } else {
73 Box::new(std::iter::once('0'))
74 };
75
76 digits = Box::new(digits.pad(pad_char, pad_to));
77
78 if let Some((separator, spacing)) = self.separator_and_spacing_with(dynamic) {
79 digits.separate(separator, spacing)
80 } else {
81 digits.collect()
82 }
83 }
84
85 pub fn fmt<N: Numeric>(&self, number: N) -> Result<String, Error> {
91 self.fmt_with(number, Dynamic::default())
92 }
93
94 pub fn fmt_with<N: Numeric>(&self, number: N, dynamic: Dynamic) -> Result<String, Error> {
109 if self.zero() && !(self.align() == Align::Right || self.align() == Align::Decimal) {
110 return Err(Error::IncompatibleAlignment);
111 }
112 let negative = number.is_negative() && self.base() == Base::Decimal;
113 let decimal_separator = self.decimal_separator();
114
115 let matches_separator = |ch: char| {
118 self.separator_and_spacing_with(dynamic)
119 .map(|(separator, _)| separator == ch)
120 .unwrap_or_default()
121 };
122
123 let (mut digits, decimal_pos): (VecDeque<_>, Option<usize>) = match self.base() {
126 Base::Binary => (
127 self.normalize(
128 number
129 .binary()
130 .ok_or_else(|| Error::NotImplemented(self.base(), type_name::<N>()))?,
131 dynamic,
132 ),
133 None,
134 ),
135 Base::Octal => (
136 self.normalize(
137 number
138 .octal()
139 .ok_or_else(|| Error::NotImplemented(self.base(), type_name::<N>()))?,
140 dynamic,
141 ),
142 None,
143 ),
144 Base::Decimal => {
145 let (left, right) = number.decimal();
146 let mut dq = self.normalize(left, dynamic);
147 let decimal = dq.len();
148 let past_decimal: Option<Box<dyn Iterator<Item = char>>> =
149 match (right, self.precision_with(dynamic)) {
150 (Some(digits), None) => Some(Box::new(digits)),
151 (Some(digits), Some(precision)) => Some(Box::new(
152 digits.chain(std::iter::repeat('0')).take(precision),
153 )),
154 (None, Some(precision)) => {
155 Some(Box::new(std::iter::repeat('0').take(precision)))
156 }
157 (None, None) => None,
158 };
159 if let Some(past_decimal) = past_decimal {
160 dq.push_front(self.decimal_separator());
161
162 for item in past_decimal {
164 dq.push_front(item);
165 }
166 }
167 (dq, Some(decimal))
168 }
169 Base::LowerHex => (
170 self.normalize(
171 number
172 .hex()
173 .ok_or_else(|| Error::NotImplemented(self.base(), type_name::<N>()))?,
174 dynamic,
175 ),
176 None,
177 ),
178 Base::UpperHex => (
179 self.normalize(
180 number
181 .hex()
182 .ok_or_else(|| Error::NotImplemented(self.base(), type_name::<N>()))?
183 .map(|ch| ch.to_ascii_uppercase()),
184 dynamic,
185 ),
186 None,
187 ),
188 };
189
190 debug_assert!(
191 {
192 let legal: Box<dyn Fn(&char) -> bool> = match self.base() {
193 Base::Binary => {
194 Box::new(move |ch| matches_separator(*ch) || ('0'..='1').contains(ch))
195 }
196 Base::Octal => {
197 Box::new(move |ch| matches_separator(*ch) || ('0'..='7').contains(ch))
198 }
199 Base::Decimal => Box::new(move |ch| {
200 *ch == decimal_separator
201 || matches_separator(*ch)
202 || ('0'..='9').contains(ch)
203 }),
204 Base::LowerHex => Box::new(move |ch| {
205 matches_separator(*ch)
206 || ('0'..='9').contains(ch)
207 || ('a'..='f').contains(ch)
208 }),
209 Base::UpperHex => Box::new(move |ch| {
210 matches_separator(*ch)
211 || ('0'..='9').contains(ch)
212 || ('A'..='F').contains(ch)
213 }),
214 };
215 digits.iter().all(legal)
216 },
217 "illegal characters in number; check its `impl Numeric`",
218 );
219
220 let width_desired = self.width_desired(dynamic);
221 let mut decimal_pos = decimal_pos.unwrap_or_else(|| digits.len());
222 let mut digit_count = if self.align() == Align::Decimal {
223 decimal_pos
224 } else {
225 digits.len()
226 };
227 while digit_count > width_desired && {
229 let last = *digits.back().expect("can't be empty while decimal_pos > 0");
230 last == '0' || matches_separator(last)
231 } {
232 digit_count -= 1;
233 decimal_pos -= 1;
234 digits.pop_back();
235 }
236
237 let width_used = digits.len();
238 let (mut padding_front, padding_rear) = match self.align() {
239 Align::Right => (width_desired.saturating_sub(width_used), 0),
240 Align::Left => (0, width_desired.saturating_sub(width_used)),
241 Align::Center => {
242 let unused_width = width_desired.saturating_sub(width_used);
243 let half_unused_width = unused_width / 2;
244 (unused_width - half_unused_width, half_unused_width)
246 }
247 Align::Decimal => (width_desired.saturating_sub(decimal_pos), 0),
248 };
249
250 let sign_char = match (self.sign(), negative) {
251 (Sign::PlusAndMinus, _) => Some(if negative { '-' } else { '+' }),
252 (Sign::OnlyMinus, true) => Some('-'),
253 (Sign::OnlyMinus, false) => None,
254 };
255 if sign_char.is_some() {
256 padding_front = padding_front.saturating_sub(1);
257 if !digits.is_empty() {
258 let back = *digits.back().expect("known not to be empty");
259 if back == '0' || matches_separator(back) {
260 digits.pop_back();
261 }
262 }
263 }
264
265 let prefix = match (self.hash(), self.base()) {
266 (false, _) => None,
267 (_, Base::Binary) => Some("0b"),
268 (_, Base::Octal) => Some("0o"),
269 (_, Base::Decimal) => Some("0d"),
270 (_, Base::LowerHex) | (_, Base::UpperHex) => Some("0x"),
271 };
272 if prefix.is_some() {
273 padding_front = padding_front.saturating_sub(2);
274 }
275
276 let mut rendered = String::with_capacity(padding_front + padding_rear + width_used + 3);
278
279 if !self.zero {
286 for _ in 0..padding_front {
287 rendered.push(self.fill());
288 }
289 }
290
291 if let Some(sign) = sign_char {
292 rendered.push(sign);
293 }
294 if let Some(prefix) = prefix {
295 rendered.push_str(prefix);
296 }
297
298 if self.zero {
299 for _ in 0..padding_front {
300 rendered.push(self.fill());
301 }
302 }
303
304 for digit in digits.into_iter().rev() {
305 rendered.push(digit);
306 }
307 for _ in 0..padding_rear {
308 rendered.push(self.fill());
309 }
310
311 Ok(rendered)
312 }
313
314 #[inline]
316 pub fn fill(&self) -> char {
317 self.fill.unwrap_or(' ')
318 }
319
320 #[inline]
322 pub fn align(&self) -> Align {
323 self.align
324 }
325
326 #[inline]
328 pub fn sign(&self) -> Sign {
329 self.sign
330 }
331
332 #[inline]
334 pub fn hash(&self) -> bool {
335 self.hash
336 }
337
338 #[inline]
340 pub fn zero(&self) -> bool {
341 self.zero && self.fill() == '0'
342 }
343
344 #[inline]
346 pub fn width(&self) -> usize {
347 self.width
348 }
349
350 #[inline]
355 pub fn precision(&self) -> Option<usize> {
356 self.precision
357 }
358
359 #[inline]
361 pub fn base(&self) -> Base {
362 self.base
363 }
364
365 fn separator_and_spacing_with(&self, dynamic: Dynamic) -> Option<(char, usize)> {
371 match (self.separator, self.spacing_with(dynamic)) {
372 (Some(sep), Some(spc)) => Some((sep, spc)),
373 (Some(sep), None) => Some((sep, 3)),
374 (None, Some(spc)) => Some((',', spc)),
375 (None, None) => None,
376 }
377 }
378
379 fn separator_and_spacing(&self) -> Option<(char, usize)> {
385 self.separator_and_spacing_with(Dynamic::default())
386 }
387
388 #[inline]
390 pub fn separator(&self) -> Option<char> {
391 self.separator_and_spacing().map(|(separator, _)| separator)
392 }
393
394 #[inline]
396 pub fn spacing(&self) -> Option<usize> {
397 self.separator_and_spacing().map(|(_, spacing)| spacing)
398 }
399
400 #[inline]
402 pub fn decimal_separator(&self) -> char {
403 self.decimal_separator.unwrap_or('.')
404 }
405
406 fn width_with(&self, dynamic: Dynamic) -> usize {
407 dynamic.width.unwrap_or(self.width)
408 }
409
410 fn precision_with(&self, dynamic: Dynamic) -> Option<usize> {
411 dynamic.precision.or(self.precision)
412 }
413
414 fn spacing_with(&self, dynamic: Dynamic) -> Option<usize> {
415 dynamic.spacing.or(self.spacing)
416 }
417}
418
419impl FromStr for NumFmt {
420 type Err = parse::Error;
421
422 fn from_str(s: &str) -> Result<Self, Self::Err> {
423 parse::parse(s)
424 }
425}
426
427#[cfg(test)]
428mod tests {
429 use super::*;
430
431 #[test]
432 fn test_dynamic_width() {
433 let fmt = NumFmt::from_str("#04x_2").unwrap();
434 assert!(fmt.zero());
435 assert_eq!(fmt.fmt(0).unwrap(), "0x00");
436
437 let dynamic = Dynamic::width(7);
438 dbg!(
439 fmt.separator(),
440 dynamic,
441 fmt.width_with(dynamic),
442 fmt.precision_with(dynamic),
443 fmt.spacing_with(dynamic)
444 );
445
446 assert_eq!(fmt.fmt_with(0, dynamic).unwrap(), "0x00_00");
447 }
448
449 #[test]
450 fn test_separator() {
451 let fmt = NumFmt::from_str(",").unwrap();
452 assert_eq!(fmt.fmt(123_456_789).unwrap(), "123,456,789");
453 }
454}