1use std::io::Write;
2
3#[derive(Debug, Clone, Copy, PartialEq, Eq)]
5pub enum ScaleUnit {
6 None,
8 Si,
10 Iec,
12 IecI,
14 Auto,
16}
17
18#[derive(Debug, Clone, Copy, PartialEq, Eq)]
20pub enum RoundMethod {
21 Up,
23 Down,
25 FromZero,
27 TowardsZero,
29 Nearest,
31}
32
33#[derive(Debug, Clone, Copy, PartialEq, Eq)]
35pub enum InvalidMode {
36 Abort,
38 Fail,
40 Warn,
42 Ignore,
44}
45
46struct FieldSet {
51 bits: u128,
52 all: bool,
53 overflow: Vec<usize>,
54}
55
56impl FieldSet {
57 fn from_config(field: &[usize]) -> Self {
58 if field.is_empty() {
59 return FieldSet {
60 bits: 0,
61 all: true,
62 overflow: Vec::new(),
63 };
64 }
65 let mut bits: u128 = 0;
66 let mut overflow = Vec::new();
67 for &f in field {
68 if f >= 1 && f <= 128 {
69 bits |= 1u128 << (f - 1);
70 } else if f > 128 {
71 overflow.push(f);
72 }
73 }
74 FieldSet {
75 bits,
76 all: false,
77 overflow,
78 }
79 }
80
81 #[inline(always)]
82 fn contains(&self, field_num: usize) -> bool {
83 if self.all {
84 return true;
85 }
86 if field_num >= 1 && field_num <= 128 {
87 (self.bits & (1u128 << (field_num - 1))) != 0
88 } else {
89 self.overflow.contains(&field_num)
90 }
91 }
92}
93
94pub struct NumfmtConfig {
96 pub from: ScaleUnit,
97 pub to: ScaleUnit,
98 pub from_unit: f64,
99 pub to_unit: f64,
100 pub padding: Option<i32>,
101 pub round: RoundMethod,
102 pub suffix: Option<String>,
103 pub format: Option<String>,
104 pub field: Vec<usize>,
105 pub delimiter: Option<char>,
106 pub header: usize,
107 pub invalid: InvalidMode,
108 pub grouping: bool,
109 pub zero_terminated: bool,
110}
111
112impl Default for NumfmtConfig {
113 fn default() -> Self {
114 Self {
115 from: ScaleUnit::None,
116 to: ScaleUnit::None,
117 from_unit: 1.0,
118 to_unit: 1.0,
119 padding: None,
120 round: RoundMethod::FromZero,
121 suffix: None,
122 format: None,
123 field: vec![1],
124 delimiter: None,
125 header: 0,
126 invalid: InvalidMode::Abort,
127 grouping: false,
128 zero_terminated: false,
129 }
130 }
131}
132
133const SI_SUFFIXES: &[(char, f64)] = &[
136 ('K', 1e3),
137 ('M', 1e6),
138 ('G', 1e9),
139 ('T', 1e12),
140 ('P', 1e15),
141 ('E', 1e18),
142 ('Z', 1e21),
143 ('Y', 1e24),
144 ('R', 1e27),
145 ('Q', 1e30),
146];
147
148const IEC_SUFFIXES: &[(char, f64)] = &[
150 ('K', 1024.0),
151 ('M', 1_048_576.0),
152 ('G', 1_073_741_824.0),
153 ('T', 1_099_511_627_776.0),
154 ('P', 1_125_899_906_842_624.0),
155 ('E', 1_152_921_504_606_846_976.0),
156 ('Z', 1_180_591_620_717_411_303_424.0),
157 ('Y', 1_208_925_819_614_629_174_706_176.0),
158 ('R', 1_237_940_039_285_380_274_899_124_224.0),
159 ('Q', 1_267_650_600_228_229_401_496_703_205_376.0),
160];
161
162pub fn parse_scale_unit(s: &str) -> Result<ScaleUnit, String> {
164 match s {
165 "none" => Ok(ScaleUnit::None),
166 "si" => Ok(ScaleUnit::Si),
167 "iec" => Ok(ScaleUnit::Iec),
168 "iec-i" => Ok(ScaleUnit::IecI),
169 "auto" => Ok(ScaleUnit::Auto),
170 _ => Err(format!("invalid unit: '{}'", s)),
171 }
172}
173
174pub fn parse_round_method(s: &str) -> Result<RoundMethod, String> {
176 match s {
177 "up" => Ok(RoundMethod::Up),
178 "down" => Ok(RoundMethod::Down),
179 "from-zero" => Ok(RoundMethod::FromZero),
180 "towards-zero" => Ok(RoundMethod::TowardsZero),
181 "nearest" => Ok(RoundMethod::Nearest),
182 _ => Err(format!("invalid rounding method: '{}'", s)),
183 }
184}
185
186pub fn parse_invalid_mode(s: &str) -> Result<InvalidMode, String> {
188 match s {
189 "abort" => Ok(InvalidMode::Abort),
190 "fail" => Ok(InvalidMode::Fail),
191 "warn" => Ok(InvalidMode::Warn),
192 "ignore" => Ok(InvalidMode::Ignore),
193 _ => Err(format!("invalid mode: '{}'", s)),
194 }
195}
196
197pub fn parse_fields(s: &str) -> Result<Vec<usize>, String> {
200 if s == "-" {
201 return Ok(vec![]);
203 }
204 let mut fields = Vec::new();
205 for part in s.split(',') {
206 let part = part.trim();
207 if let Some(dash_pos) = part.find('-') {
208 let start_str = &part[..dash_pos];
209 let end_str = &part[dash_pos + 1..];
210 if start_str.is_empty() && end_str.is_empty() {
212 return Ok(vec![]);
213 }
214 let start: usize = if start_str.is_empty() {
215 1
216 } else {
217 start_str
218 .parse()
219 .map_err(|_| format!("invalid field value '{}'", part))?
220 };
221 let end: usize = if end_str.is_empty() {
222 9999
225 } else {
226 end_str
227 .parse()
228 .map_err(|_| format!("invalid field value '{}'", part))?
229 };
230 if start == 0 {
231 return Err(format!("fields are numbered from 1: '{}'", part));
232 }
233 for i in start..=end {
234 if !fields.contains(&i) {
235 fields.push(i);
236 }
237 }
238 } else {
239 let n: usize = part
240 .parse()
241 .map_err(|_| format!("invalid field value '{}'", part))?;
242 if n == 0 {
243 return Err("fields are numbered from 1".to_string());
244 }
245 if !fields.contains(&n) {
246 fields.push(n);
247 }
248 }
249 }
250 fields.sort();
251 Ok(fields)
252}
253
254fn parse_number_with_suffix(s: &str, unit: ScaleUnit) -> Result<f64, String> {
257 let s = s.trim();
258 if s.is_empty() {
259 return Err("invalid number: ''".to_string());
260 }
261
262 let mut num_end = s.len();
264 let bytes = s.as_bytes();
265 let len = s.len();
266
267 if len > 0 {
269 let last_char = bytes[len - 1] as char;
270
271 match unit {
272 ScaleUnit::Auto | ScaleUnit::IecI => {
273 if last_char == 'i' && len >= 2 {
275 let prefix_char = (bytes[len - 2] as char).to_ascii_uppercase();
276 if is_scale_suffix(prefix_char) {
277 num_end = len - 2;
278 }
279 } else {
280 let upper = last_char.to_ascii_uppercase();
281 if is_scale_suffix(upper) {
282 num_end = len - 1;
283 }
284 }
285 }
286 ScaleUnit::Si | ScaleUnit::Iec => {
287 let upper = last_char.to_ascii_uppercase();
288 if is_scale_suffix(upper) {
289 num_end = len - 1;
290 }
291 }
292 ScaleUnit::None => {}
293 }
294 }
295
296 let num_str = &s[..num_end];
297 let suffix_str = &s[num_end..];
298
299 let value: f64 = num_str.parse().map_err(|_| {
301 let has_leading_digits = {
304 let b = num_str.as_bytes();
305 let start = if !b.is_empty() && (b[0] == b'+' || b[0] == b'-') {
306 1
307 } else {
308 0
309 };
310 start < b.len() && b[start].is_ascii_digit()
311 };
312 if has_leading_digits {
313 format!("invalid suffix in input: '{}'", s)
314 } else {
315 format!("invalid number: '{}'", s)
316 }
317 })?;
318
319 let multiplier = if suffix_str.is_empty() {
321 1.0
322 } else {
323 let suffix_upper = suffix_str.as_bytes()[0].to_ascii_uppercase() as char;
324 match unit {
325 ScaleUnit::Auto => {
326 if suffix_str.len() >= 2 && suffix_str.as_bytes()[suffix_str.len() - 1] == b'i' {
328 find_iec_multiplier(suffix_upper)?
329 } else {
330 find_si_multiplier(suffix_upper)?
331 }
332 }
333 ScaleUnit::Si => find_si_multiplier(suffix_upper)?,
334 ScaleUnit::Iec | ScaleUnit::IecI => find_iec_multiplier(suffix_upper)?,
335 ScaleUnit::None => {
336 return Err(format!("invalid number: '{}'", s));
337 }
338 }
339 };
340
341 Ok(value * multiplier)
342}
343
344#[inline(always)]
345fn is_scale_suffix(c: char) -> bool {
346 matches!(c, 'K' | 'M' | 'G' | 'T' | 'P' | 'E' | 'Z' | 'Y' | 'R' | 'Q')
347}
348
349fn find_si_multiplier(c: char) -> Result<f64, String> {
350 match c.to_ascii_uppercase() {
351 'K' => Ok(1e3),
352 'M' => Ok(1e6),
353 'G' => Ok(1e9),
354 'T' => Ok(1e12),
355 'P' => Ok(1e15),
356 'E' => Ok(1e18),
357 'Z' => Ok(1e21),
358 'Y' => Ok(1e24),
359 'R' => Ok(1e27),
360 'Q' => Ok(1e30),
361 _ => Err(format!("invalid suffix: '{}'", c)),
362 }
363}
364
365fn find_iec_multiplier(c: char) -> Result<f64, String> {
366 match c {
367 'K' => Ok(1024.0),
368 'M' => Ok(1_048_576.0),
369 'G' => Ok(1_073_741_824.0),
370 'T' => Ok(1_099_511_627_776.0),
371 'P' => Ok(1_125_899_906_842_624.0),
372 'E' => Ok(1_152_921_504_606_846_976.0),
373 'Z' => Ok(1_180_591_620_717_411_303_424.0),
374 'Y' => Ok(1_208_925_819_614_629_174_706_176.0),
375 'R' => Ok(1_237_940_039_285_380_274_899_124_224.0),
376 'Q' => Ok(1_267_650_600_228_229_401_496_703_205_376.0),
377 _ => Err(format!("invalid suffix: '{}'", c)),
378 }
379}
380
381#[inline(always)]
383fn apply_round(value: f64, method: RoundMethod) -> f64 {
384 match method {
385 RoundMethod::Up => value.ceil(),
386 RoundMethod::Down => value.floor(),
387 RoundMethod::FromZero => {
388 if value >= 0.0 {
389 value.ceil()
390 } else {
391 value.floor()
392 }
393 }
394 RoundMethod::TowardsZero => {
395 if value >= 0.0 {
396 value.floor()
397 } else {
398 value.ceil()
399 }
400 }
401 RoundMethod::Nearest => value.round(),
402 }
403}
404
405fn format_scaled(value: f64, unit: ScaleUnit, round: RoundMethod) -> String {
407 match unit {
408 ScaleUnit::None => {
409 format_plain_number(value)
411 }
412 ScaleUnit::Si => format_with_scale(value, SI_SUFFIXES, "", round),
413 ScaleUnit::Iec => format_with_scale(value, IEC_SUFFIXES, "", round),
414 ScaleUnit::IecI => format_with_scale(value, IEC_SUFFIXES, "i", round),
415 ScaleUnit::Auto => {
416 format_with_scale(value, SI_SUFFIXES, "", round)
418 }
419 }
420}
421
422fn write_scaled_to_buf(buf: &mut Vec<u8>, value: f64, unit: ScaleUnit, round: RoundMethod) {
424 match unit {
425 ScaleUnit::None => {
426 write_plain_number_to_buf(buf, value);
427 }
428 ScaleUnit::Si => write_with_scale_to_buf(buf, value, SI_SUFFIXES, b"", round),
429 ScaleUnit::Iec => write_with_scale_to_buf(buf, value, IEC_SUFFIXES, b"", round),
430 ScaleUnit::IecI => write_with_scale_to_buf(buf, value, IEC_SUFFIXES, b"i", round),
431 ScaleUnit::Auto => write_with_scale_to_buf(buf, value, SI_SUFFIXES, b"", round),
432 }
433}
434
435#[inline]
437fn write_plain_number_to_buf(buf: &mut Vec<u8>, value: f64) {
438 let int_val = value as i64;
439 if value == (int_val as f64) {
440 let mut itoa_buf = itoa::Buffer::new();
441 buf.extend_from_slice(itoa_buf.format(int_val).as_bytes());
442 } else {
443 use std::io::Write;
445 let _ = write!(buf, "{:.1}", value);
446 }
447}
448
449fn write_with_scale_to_buf(
451 buf: &mut Vec<u8>,
452 value: f64,
453 suffixes: &[(char, f64)],
454 i_suffix: &[u8],
455 round: RoundMethod,
456) {
457 let abs_value = value.abs();
458 let negative = value < 0.0;
459
460 let mut chosen_idx: Option<usize> = None;
462 for (idx, &(_suffix, mult)) in suffixes.iter().enumerate().rev() {
463 if abs_value >= mult {
464 chosen_idx = Some(idx);
465 break;
466 }
467 }
468
469 let Some(mut idx) = chosen_idx else {
470 write_plain_number_to_buf(buf, value);
472 return;
473 };
474
475 loop {
476 let (suffix, mult) = suffixes[idx];
477 let scaled = value / mult;
478 let abs_scaled = scaled.abs();
479
480 if abs_scaled < 10.0 {
481 let rounded = apply_round_for_display(scaled, round);
482 if rounded.abs() >= 10.0 {
483 let int_val = rounded as i64;
484 if int_val.unsigned_abs() >= 1000 && idx + 1 < suffixes.len() {
485 idx += 1;
486 continue;
487 }
488 if negative {
489 buf.push(b'-');
490 }
491 let mut itoa_buf = itoa::Buffer::new();
492 buf.extend_from_slice(itoa_buf.format(int_val.unsigned_abs()).as_bytes());
493 buf.push(suffix as u8);
494 buf.extend_from_slice(i_suffix);
495 return;
496 }
497 if negative {
498 buf.push(b'-');
499 }
500 let abs_rounded = rounded.abs();
502 let int_part = abs_rounded as u64;
503 let frac_part = ((abs_rounded - int_part as f64) * 10.0).round() as u8;
504 let mut itoa_buf = itoa::Buffer::new();
505 buf.extend_from_slice(itoa_buf.format(int_part).as_bytes());
506 buf.push(b'.');
507 buf.push(b'0' + frac_part);
508 buf.push(suffix as u8);
509 buf.extend_from_slice(i_suffix);
510 return;
511 } else {
512 let int_val = apply_round_int(scaled, round);
513 if int_val.unsigned_abs() >= 1000 {
514 if idx + 1 < suffixes.len() {
515 idx += 1;
516 continue;
517 }
518 }
519 if negative {
520 buf.push(b'-');
521 }
522 let mut itoa_buf = itoa::Buffer::new();
523 buf.extend_from_slice(itoa_buf.format(int_val.unsigned_abs()).as_bytes());
524 buf.push(suffix as u8);
525 buf.extend_from_slice(i_suffix);
526 return;
527 }
528 }
529}
530
531fn format_plain_number(value: f64) -> String {
533 let int_val = value as i64;
534 if value == (int_val as f64) {
535 let mut buf = itoa::Buffer::new();
536 buf.format(int_val).to_string()
537 } else {
538 format!("{:.1}", value)
540 }
541}
542
543fn format_with_scale(
549 value: f64,
550 suffixes: &[(char, f64)],
551 i_suffix: &str,
552 round: RoundMethod,
553) -> String {
554 let abs_value = value.abs();
555 let sign = if value < 0.0 { "-" } else { "" };
556
557 let mut chosen_idx: Option<usize> = None;
559
560 for (idx, &(_suffix, mult)) in suffixes.iter().enumerate().rev() {
561 if abs_value >= mult {
562 chosen_idx = Some(idx);
563 break;
564 }
565 }
566
567 let Some(mut idx) = chosen_idx else {
568 return format_plain_number(value);
570 };
571
572 loop {
573 let (suffix, mult) = suffixes[idx];
574 let scaled = value / mult;
575 let abs_scaled = scaled.abs();
576
577 if abs_scaled < 10.0 {
578 let rounded = apply_round_for_display(scaled, round);
580 if rounded.abs() >= 10.0 {
581 let int_val = rounded as i64;
583 if int_val.unsigned_abs() >= 1000 && idx + 1 < suffixes.len() {
584 idx += 1;
585 continue;
586 }
587 let mut itoa_buf = itoa::Buffer::new();
588 let digits = itoa_buf.format(int_val.unsigned_abs());
589 return format!("{sign}{}{}{}", digits, suffix, i_suffix);
590 }
591 return format!("{sign}{:.1}{}{}", rounded.abs(), suffix, i_suffix);
592 } else {
593 let int_val = apply_round_int(scaled, round);
595 if int_val.unsigned_abs() >= 1000 {
596 if idx + 1 < suffixes.len() {
597 idx += 1;
598 continue;
599 }
600 }
602 let mut itoa_buf = itoa::Buffer::new();
603 let digits = itoa_buf.format(int_val.unsigned_abs());
604 return format!("{sign}{}{}{}", digits, suffix, i_suffix);
605 }
606 }
607}
608
609#[inline(always)]
612fn apply_round_for_display(value: f64, method: RoundMethod) -> f64 {
613 let factor = 10.0;
614 let shifted = value * factor;
615 let rounded = match method {
616 RoundMethod::Up => shifted.ceil(),
617 RoundMethod::Down => shifted.floor(),
618 RoundMethod::FromZero => {
619 if shifted >= 0.0 {
620 shifted.ceil()
621 } else {
622 shifted.floor()
623 }
624 }
625 RoundMethod::TowardsZero => {
626 if shifted >= 0.0 {
627 shifted.floor()
628 } else {
629 shifted.ceil()
630 }
631 }
632 RoundMethod::Nearest => shifted.round(),
633 };
634 rounded / factor
635}
636
637#[inline(always)]
639fn apply_round_int(value: f64, method: RoundMethod) -> i64 {
640 match method {
641 RoundMethod::Up => value.ceil() as i64,
642 RoundMethod::Down => value.floor() as i64,
643 RoundMethod::FromZero => {
644 if value >= 0.0 {
645 value.ceil() as i64
646 } else {
647 value.floor() as i64
648 }
649 }
650 RoundMethod::TowardsZero => {
651 if value >= 0.0 {
652 value.floor() as i64
653 } else {
654 value.ceil() as i64
655 }
656 }
657 RoundMethod::Nearest => value.round() as i64,
658 }
659}
660
661fn group_thousands(s: &str) -> String {
663 let (integer_part, rest) = if let Some(dot_pos) = s.find('.') {
665 (&s[..dot_pos], &s[dot_pos..])
666 } else {
667 (s, "")
668 };
669
670 let (sign, digits) = if integer_part.starts_with('-') {
672 ("-", &integer_part[1..])
673 } else {
674 ("", integer_part)
675 };
676
677 if digits.len() <= 3 {
678 return format!("{}{}{}", sign, digits, rest);
679 }
680
681 let mut result = String::with_capacity(digits.len() + digits.len() / 3);
682 let remainder = digits.len() % 3;
683 if remainder > 0 {
684 result.push_str(&digits[..remainder]);
685 }
686 for (i, chunk) in digits.as_bytes()[remainder..].chunks(3).enumerate() {
687 if i > 0 || remainder > 0 {
688 result.push(',');
689 }
690 result.push_str(std::str::from_utf8(chunk).unwrap());
691 }
692
693 format!("{}{}{}", sign, result, rest)
694}
695
696fn apply_format_padding(scaled: &str, fmt: &str) -> String {
699 let bytes = fmt.as_bytes();
700 let mut i = 0;
701
702 while i < bytes.len() && bytes[i] != b'%' {
704 i += 1;
705 }
706 let prefix = &fmt[..i];
707 if i >= bytes.len() {
708 return format!("{}{}", prefix, scaled);
709 }
710 i += 1; let mut left_align = false;
714 while i < bytes.len() {
715 match bytes[i] {
716 b'0' | b'+' | b' ' | b'#' | b'\'' => {}
717 b'-' => left_align = true,
718 _ => break,
719 }
720 i += 1;
721 }
722
723 let mut width: usize = 0;
725 while i < bytes.len() && bytes[i].is_ascii_digit() {
726 width = width
727 .saturating_mul(10)
728 .saturating_add((bytes[i] - b'0') as usize);
729 i += 1;
730 }
731
732 while i < bytes.len() && (bytes[i] == b'.' || bytes[i].is_ascii_digit()) {
734 i += 1;
735 }
736 if i < bytes.len() {
737 i += 1; }
739 let suffix = &fmt[i..];
740
741 let padded = if width > 0 && scaled.len() < width {
742 let pad_len = width - scaled.len();
743 if left_align {
744 format!("{}{}", scaled, " ".repeat(pad_len))
745 } else {
746 format!("{}{}", " ".repeat(pad_len), scaled)
747 }
748 } else {
749 scaled.to_string()
750 };
751
752 format!("{}{}{}", prefix, padded, suffix)
753}
754
755struct ParsedFormat {
757 prefix: String,
758 suffix: String,
759 zero_pad: bool,
760 left_align: bool,
761 plus_sign: bool,
762 space_sign: bool,
763 width: usize,
764 precision: Option<usize>,
765 conv: char,
766 is_percent: bool,
767}
768
769fn parse_format_spec(fmt: &str) -> Result<ParsedFormat, String> {
771 let bytes = fmt.as_bytes();
772 let mut i = 0;
773
774 while i < bytes.len() && bytes[i] != b'%' {
775 i += 1;
776 }
777 let prefix = fmt[..i].to_string();
778 if i >= bytes.len() {
779 return Err(format!("invalid format: '{}'", fmt));
780 }
781 i += 1;
782
783 if i >= bytes.len() {
784 return Err(format!("invalid format: '{}'", fmt));
785 }
786
787 if bytes[i] == b'%' {
788 return Ok(ParsedFormat {
789 prefix,
790 suffix: String::new(),
791 zero_pad: false,
792 left_align: false,
793 plus_sign: false,
794 space_sign: false,
795 width: 0,
796 precision: None,
797 conv: '%',
798 is_percent: true,
799 });
800 }
801
802 let mut zero_pad = false;
803 let mut left_align = false;
804 let mut plus_sign = false;
805 let mut space_sign = false;
806 while i < bytes.len() {
807 match bytes[i] {
808 b'0' => zero_pad = true,
809 b'-' => left_align = true,
810 b'+' => plus_sign = true,
811 b' ' => space_sign = true,
812 b'#' | b'\'' => {}
813 _ => break,
814 }
815 i += 1;
816 }
817
818 let mut width: usize = 0;
819 while i < bytes.len() && bytes[i].is_ascii_digit() {
820 width = width
821 .saturating_mul(10)
822 .saturating_add((bytes[i] - b'0') as usize);
823 i += 1;
824 }
825
826 let mut precision: Option<usize> = None;
827 if i < bytes.len() && bytes[i] == b'.' {
828 i += 1;
829 let mut prec: usize = 0;
830 while i < bytes.len() && bytes[i].is_ascii_digit() {
831 prec = prec
832 .saturating_mul(10)
833 .saturating_add((bytes[i] - b'0') as usize);
834 i += 1;
835 }
836 precision = Some(prec);
837 }
838
839 if i >= bytes.len() {
840 return Err(format!("invalid format: '{}'", fmt));
841 }
842 let conv = bytes[i] as char;
843 i += 1;
844 let suffix = fmt[i..].to_string();
845
846 Ok(ParsedFormat {
847 prefix,
848 suffix,
849 zero_pad,
850 left_align,
851 plus_sign,
852 space_sign,
853 width,
854 precision,
855 conv,
856 is_percent: false,
857 })
858}
859
860fn apply_parsed_format(value: f64, pf: &ParsedFormat) -> Result<String, String> {
862 if pf.is_percent {
863 return Ok(format!("{}%", pf.prefix));
864 }
865
866 let prec = pf.precision.unwrap_or(6);
867 let formatted = match pf.conv {
868 'f' => format!("{:.prec$}", value, prec = prec),
869 'e' => format_scientific(value, prec, 'e'),
870 'E' => format_scientific(value, prec, 'E'),
871 'g' => format_g(value, prec, false),
872 'G' => format_g(value, prec, true),
873 _ => return Err(format!("invalid format character: '{}'", pf.conv)),
874 };
875
876 let sign_str = if value < 0.0 {
877 ""
878 } else if pf.plus_sign {
879 "+"
880 } else if pf.space_sign {
881 " "
882 } else {
883 ""
884 };
885
886 let num_str = if !sign_str.is_empty() && !formatted.starts_with('-') {
887 format!("{}{}", sign_str, formatted)
888 } else {
889 formatted
890 };
891
892 let padded = if pf.width > 0 && num_str.len() < pf.width {
893 let pad_len = pf.width - num_str.len();
894 if pf.left_align {
895 format!("{}{}", num_str, " ".repeat(pad_len))
896 } else if pf.zero_pad {
897 if num_str.starts_with('-') || num_str.starts_with('+') || num_str.starts_with(' ') {
898 let (sign, rest) = num_str.split_at(1);
899 format!("{}{}{}", sign, "0".repeat(pad_len), rest)
900 } else {
901 format!("{}{}", "0".repeat(pad_len), num_str)
902 }
903 } else {
904 format!("{}{}", " ".repeat(pad_len), num_str)
905 }
906 } else {
907 num_str
908 };
909
910 Ok(format!("{}{}{}", pf.prefix, padded, pf.suffix))
911}
912
913fn format_scientific(value: f64, prec: usize, e_char: char) -> String {
915 if value == 0.0 {
916 let sign = if value.is_sign_negative() { "-" } else { "" };
917 if prec == 0 {
918 return format!("{sign}0{e_char}+00");
919 }
920 return format!("{sign}0.{:0>prec$}{e_char}+00", "", prec = prec);
921 }
922
923 let abs = value.abs();
924 let sign = if value < 0.0 { "-" } else { "" };
925 let exp = abs.log10().floor() as i32;
926 let mantissa = abs / 10f64.powi(exp);
927
928 let factor = 10f64.powi(prec as i32);
929 let mantissa = (mantissa * factor).round() / factor;
930
931 let (mantissa, exp) = if mantissa >= 10.0 {
932 (mantissa / 10.0, exp + 1)
933 } else {
934 (mantissa, exp)
935 };
936
937 let exp_sign = if exp >= 0 { '+' } else { '-' };
938 let exp_abs = exp.unsigned_abs();
939
940 if prec == 0 {
941 format!("{sign}{mantissa:.0}{e_char}{exp_sign}{exp_abs:02}")
942 } else {
943 format!(
944 "{sign}{mantissa:.prec$}{e_char}{exp_sign}{exp_abs:02}",
945 prec = prec
946 )
947 }
948}
949
950fn format_g(value: f64, prec: usize, upper: bool) -> String {
952 let prec = if prec == 0 { 1 } else { prec };
953
954 if value == 0.0 {
955 let sign = if value.is_sign_negative() { "-" } else { "" };
956 return format!("{sign}0");
957 }
958
959 let abs = value.abs();
960 let exp = abs.log10().floor() as i32;
961 let e_char = if upper { 'E' } else { 'e' };
962
963 if exp < -4 || exp >= prec as i32 {
964 let sig_prec = prec.saturating_sub(1);
965 let s = format_scientific(value, sig_prec, e_char);
966 trim_g_zeros(&s)
967 } else {
968 let decimal_prec = if prec as i32 > exp + 1 {
969 (prec as i32 - exp - 1) as usize
970 } else {
971 0
972 };
973 let s = format!("{value:.decimal_prec$}");
974 trim_g_zeros(&s)
975 }
976}
977
978fn trim_g_zeros(s: &str) -> String {
979 if let Some(e_pos) = s.find(['e', 'E']) {
980 let (mantissa, exponent) = s.split_at(e_pos);
981 let trimmed = mantissa.trim_end_matches('0').trim_end_matches('.');
982 format!("{trimmed}{exponent}")
983 } else {
984 s.trim_end_matches('0').trim_end_matches('.').to_string()
985 }
986}
987
988fn convert_number(
990 token: &str,
991 config: &NumfmtConfig,
992 parsed_fmt: Option<&ParsedFormat>,
993) -> Result<String, String> {
994 let raw_value = parse_number_with_suffix(token, config.from)?;
996
997 let value = raw_value * config.from_unit;
999
1000 let value = value / config.to_unit;
1002
1003 let mut result = if let Some(pf) = parsed_fmt {
1005 if config.to != ScaleUnit::None {
1007 let scaled = format_scaled(value, config.to, config.round);
1008 apply_format_padding(&scaled, config.format.as_deref().unwrap_or("%f"))
1009 } else {
1010 let rounded = apply_round(value, config.round);
1011 apply_parsed_format(rounded, pf)?
1012 }
1013 } else if config.to != ScaleUnit::None {
1014 format_scaled(value, config.to, config.round)
1015 } else {
1016 let rounded = apply_round(value, config.round);
1017 format_plain_number(rounded)
1018 };
1019
1020 if config.grouping {
1022 result = group_thousands(&result);
1023 }
1024
1025 if let Some(ref suffix) = config.suffix {
1027 result.push_str(suffix);
1028 }
1029
1030 if let Some(pad) = config.padding {
1032 let pad_width = pad.unsigned_abs() as usize;
1033 if result.len() < pad_width {
1034 let deficit = pad_width - result.len();
1035 if pad < 0 {
1036 result = format!("{}{}", result, " ".repeat(deficit));
1038 } else {
1039 result = format!("{}{}", " ".repeat(deficit), result);
1041 }
1042 }
1043 }
1044
1045 Ok(result)
1046}
1047
1048fn convert_number_to_buf(
1052 token: &str,
1053 config: &NumfmtConfig,
1054 parsed_fmt: Option<&ParsedFormat>,
1055 out: &mut Vec<u8>,
1056) -> Result<(), String> {
1057 let raw_value = parse_number_with_suffix(token, config.from)?;
1059
1060 let value = raw_value * config.from_unit / config.to_unit;
1062
1063 let use_fast = parsed_fmt.is_none()
1065 && !config.grouping
1066 && config.suffix.is_none()
1067 && config.padding.is_none();
1068
1069 if use_fast && config.to != ScaleUnit::None {
1070 write_scaled_to_buf(out, value, config.to, config.round);
1071 return Ok(());
1072 }
1073
1074 if use_fast && config.to == ScaleUnit::None {
1075 let rounded = apply_round(value, config.round);
1076 write_plain_number_to_buf(out, rounded);
1077 return Ok(());
1078 }
1079
1080 let result = if let Some(pf) = parsed_fmt {
1082 if config.to != ScaleUnit::None {
1083 let scaled = format_scaled(value, config.to, config.round);
1084 apply_format_padding(&scaled, config.format.as_deref().unwrap_or("%f"))
1085 } else {
1086 let rounded = apply_round(value, config.round);
1087 apply_parsed_format(rounded, pf)?
1088 }
1089 } else if config.to != ScaleUnit::None {
1090 format_scaled(value, config.to, config.round)
1091 } else {
1092 let rounded = apply_round(value, config.round);
1093 format_plain_number(rounded)
1094 };
1095
1096 let mut result = result;
1097
1098 if config.grouping {
1099 result = group_thousands(&result);
1100 }
1101
1102 if let Some(ref suffix) = config.suffix {
1103 result.push_str(suffix);
1104 }
1105
1106 if let Some(pad) = config.padding {
1107 let pad_width = pad.unsigned_abs() as usize;
1108 if result.len() < pad_width {
1109 let deficit = pad_width - result.len();
1110 if pad < 0 {
1111 result = format!("{}{}", result, " ".repeat(deficit));
1112 } else {
1113 result = format!("{}{}", " ".repeat(deficit), result);
1114 }
1115 }
1116 }
1117
1118 out.extend_from_slice(result.as_bytes());
1119 Ok(())
1120}
1121
1122fn split_fields<'a>(line: &'a str, delimiter: Option<char>) -> Vec<&'a str> {
1124 match delimiter {
1125 Some(delim) => line.split(delim).collect(),
1126 None => {
1127 let mut fields = Vec::new();
1130 let bytes = line.as_bytes();
1131 let len = bytes.len();
1132 let mut i = 0;
1133 let mut field_start = 0;
1134 let mut in_space = true;
1135 let mut first = true;
1136
1137 while i < len {
1138 let c = bytes[i];
1139 if c == b' ' || c == b'\t' || c == b'\r' || c == b'\x0b' || c == b'\x0c' {
1140 if !in_space && !first {
1141 fields.push(&line[field_start..i]);
1142 }
1143 in_space = true;
1144 i += 1;
1145 } else {
1146 if in_space {
1147 field_start = i;
1148 in_space = false;
1149 first = false;
1150 }
1151 i += 1;
1152 }
1153 }
1154 if !in_space {
1155 fields.push(&line[field_start..]);
1156 }
1157
1158 if fields.is_empty() {
1159 vec![line]
1160 } else {
1161 fields
1162 }
1163 }
1164 }
1165}
1166
1167fn reassemble_fields(
1169 original: &str,
1170 fields: &[&str],
1171 converted: &[String],
1172 delimiter: Option<char>,
1173) -> String {
1174 match delimiter {
1175 Some(delim) => converted.join(&delim.to_string()),
1176 None => {
1177 let mut result = String::with_capacity(original.len());
1179 let mut field_idx = 0;
1180 let mut in_space = true;
1181 let mut i = 0;
1182 let bytes = original.as_bytes();
1183
1184 while i < bytes.len() {
1185 let c = bytes[i] as char;
1186 if c.is_ascii_whitespace() {
1187 if !in_space && field_idx > 0 {
1188 }
1190 result.push(c);
1191 in_space = true;
1192 i += 1;
1193 } else {
1194 if in_space {
1195 in_space = false;
1196 if field_idx < converted.len() {
1198 result.push_str(&converted[field_idx]);
1199 } else if field_idx < fields.len() {
1200 result.push_str(fields[field_idx]);
1201 }
1202 field_idx += 1;
1203 while i < bytes.len() && !(bytes[i] as char).is_ascii_whitespace() {
1205 i += 1;
1206 }
1207 continue;
1208 }
1209 i += 1;
1210 }
1211 }
1212
1213 result
1214 }
1215 }
1216}
1217
1218pub fn process_line(line: &str, config: &NumfmtConfig) -> Result<String, String> {
1220 process_line_with_fmt(line, config, None)
1221}
1222
1223fn process_line_with_fmt(
1225 line: &str,
1226 config: &NumfmtConfig,
1227 parsed_fmt: Option<&ParsedFormat>,
1228) -> Result<String, String> {
1229 let fields = split_fields(line, config.delimiter);
1230
1231 if fields.is_empty() {
1232 return Ok(line.to_string());
1233 }
1234
1235 let all_fields = config.field.is_empty();
1236
1237 let mut converted: Vec<String> = Vec::with_capacity(fields.len());
1238 for (i, field) in fields.iter().enumerate() {
1239 let field_num = i + 1; let should_convert = all_fields || config.field.contains(&field_num);
1241
1242 if should_convert {
1243 match convert_number(field, config, parsed_fmt) {
1244 Ok(s) => converted.push(s),
1245 Err(e) => match config.invalid {
1246 InvalidMode::Abort => return Err(e),
1247 InvalidMode::Fail => {
1248 eprintln!("numfmt: {}", e);
1249 converted.push(field.to_string());
1250 }
1251 InvalidMode::Warn => {
1252 eprintln!("numfmt: {}", e);
1253 converted.push(field.to_string());
1254 }
1255 InvalidMode::Ignore => {
1256 converted.push(field.to_string());
1257 }
1258 },
1259 }
1260 } else {
1261 converted.push(field.to_string());
1262 }
1263 }
1264
1265 Ok(reassemble_fields(
1266 line,
1267 &fields,
1268 &converted,
1269 config.delimiter,
1270 ))
1271}
1272
1273fn process_line_fast_delim(
1277 line: &[u8],
1278 delim: u8,
1279 field_set: &FieldSet,
1280 config: &NumfmtConfig,
1281 parsed_fmt: Option<&ParsedFormat>,
1282 out: &mut Vec<u8>,
1283) -> Result<(), String> {
1284 let mut field_num: usize = 1;
1285 let mut start = 0;
1286 let len = line.len();
1287
1288 loop {
1289 let end = memchr::memchr(delim, &line[start..])
1291 .map(|pos| start + pos)
1292 .unwrap_or(len);
1293
1294 if field_set.contains(field_num) {
1295 let field_str = std::str::from_utf8(&line[start..end])
1299 .map_err(|_| "invalid number: '<non-utf8>'".to_string())?;
1300
1301 match convert_number_to_buf(field_str, config, parsed_fmt, out) {
1302 Ok(()) => {}
1303 Err(e) => match config.invalid {
1304 InvalidMode::Abort => return Err(e),
1305 InvalidMode::Fail | InvalidMode::Warn => {
1306 eprintln!("numfmt: {}", e);
1307 out.extend_from_slice(&line[start..end]);
1308 }
1309 InvalidMode::Ignore => {
1310 out.extend_from_slice(&line[start..end]);
1311 }
1312 },
1313 }
1314 } else {
1315 out.extend_from_slice(&line[start..end]);
1317 }
1318
1319 if end >= len {
1320 break;
1321 }
1322
1323 out.push(delim);
1325 start = end + 1;
1326 field_num += 1;
1327 }
1328
1329 Ok(())
1330}
1331
1332fn process_line_fast_ws(
1335 line: &[u8],
1336 field_set: &FieldSet,
1337 config: &NumfmtConfig,
1338 parsed_fmt: Option<&ParsedFormat>,
1339 out: &mut Vec<u8>,
1340) -> Result<(), String> {
1341 let len = line.len();
1342 let mut i = 0;
1343 let mut field_num: usize = 0;
1344
1345 while i < len {
1347 let c = line[i];
1348 if c == b' ' || c == b'\t' || c == b'\r' || c == b'\x0b' || c == b'\x0c' {
1349 out.push(c);
1351 i += 1;
1352 } else {
1353 field_num += 1;
1355 let field_start = i;
1356
1357 while i < len {
1359 let fc = line[i];
1360 if fc == b' ' || fc == b'\t' || fc == b'\r' || fc == b'\x0b' || fc == b'\x0c' {
1361 break;
1362 }
1363 i += 1;
1364 }
1365 let field_end = i;
1366
1367 if field_set.contains(field_num) {
1368 let field_str = std::str::from_utf8(&line[field_start..field_end])
1369 .map_err(|_| "invalid number: '<non-utf8>'".to_string())?;
1370
1371 match convert_number_to_buf(field_str, config, parsed_fmt, out) {
1372 Ok(()) => {}
1373 Err(e) => match config.invalid {
1374 InvalidMode::Abort => return Err(e),
1375 InvalidMode::Fail | InvalidMode::Warn => {
1376 eprintln!("numfmt: {}", e);
1377 out.extend_from_slice(&line[field_start..field_end]);
1378 }
1379 InvalidMode::Ignore => {
1380 out.extend_from_slice(&line[field_start..field_end]);
1381 }
1382 },
1383 }
1384 } else {
1385 out.extend_from_slice(&line[field_start..field_end]);
1386 }
1387 }
1388 }
1389
1390 if field_num == 0 {
1393 if field_set.contains(1) {
1394 match convert_number_to_buf("", config, parsed_fmt, out) {
1395 Ok(()) => {}
1396 Err(e) => match config.invalid {
1397 InvalidMode::Abort | InvalidMode::Fail => return Err(e),
1398 InvalidMode::Warn => {
1399 eprintln!("numfmt: {}", e);
1400 }
1401 InvalidMode::Ignore => {}
1402 },
1403 }
1404 } else {
1405 out.extend_from_slice(line);
1406 }
1407 }
1408
1409 Ok(())
1410}
1411
1412pub fn run_numfmt<R: std::io::BufRead, W: Write>(
1414 input: R,
1415 mut output: W,
1416 config: &NumfmtConfig,
1417) -> Result<(), String> {
1418 let parsed_fmt = if let Some(ref fmt) = config.format {
1420 Some(parse_format_spec(fmt)?)
1421 } else {
1422 None
1423 };
1424
1425 let field_set = FieldSet::from_config(&config.field);
1427
1428 let terminator = if config.zero_terminated { b'\0' } else { b'\n' };
1429 let mut header_remaining = config.header;
1430 let mut buf = Vec::with_capacity(4096);
1431 let mut out_buf = Vec::with_capacity(4096);
1432 let mut reader = input;
1433 let mut had_error = false;
1434
1435 let delim_byte = config.delimiter.map(|c| {
1437 if c.is_ascii() { Some(c as u8) } else { None }
1439 });
1440 let delim_byte = delim_byte.and_then(|x| x);
1442
1443 loop {
1444 buf.clear();
1445 let bytes_read = reader
1446 .read_until(terminator, &mut buf)
1447 .map_err(|e| format!("read error: {}", e))?;
1448 if bytes_read == 0 {
1449 break;
1450 }
1451
1452 let line = if buf.last() == Some(&terminator) {
1454 &buf[..buf.len() - 1]
1455 } else {
1456 &buf[..]
1457 };
1458
1459 if header_remaining > 0 {
1460 header_remaining -= 1;
1461 output
1462 .write_all(line)
1463 .map_err(|e| format!("write error: {}", e))?;
1464 output
1465 .write_all(&[terminator])
1466 .map_err(|e| format!("write error: {}", e))?;
1467 continue;
1468 }
1469
1470 out_buf.clear();
1471
1472 let result = if let Some(db) = delim_byte {
1474 process_line_fast_delim(
1475 line,
1476 db,
1477 &field_set,
1478 config,
1479 parsed_fmt.as_ref(),
1480 &mut out_buf,
1481 )
1482 } else if config.delimiter.is_some() {
1483 let line_str = String::from_utf8_lossy(line);
1485 match process_line_with_fmt(&line_str, config, parsed_fmt.as_ref()) {
1486 Ok(result) => {
1487 out_buf.extend_from_slice(result.as_bytes());
1488 Ok(())
1489 }
1490 Err(e) => Err(e),
1491 }
1492 } else {
1493 process_line_fast_ws(line, &field_set, config, parsed_fmt.as_ref(), &mut out_buf)
1495 };
1496
1497 match result {
1498 Ok(()) => {
1499 output
1500 .write_all(&out_buf)
1501 .map_err(|e| format!("write error: {}", e))?;
1502 output
1503 .write_all(&[terminator])
1504 .map_err(|e| format!("write error: {}", e))?;
1505 }
1506 Err(e) => match config.invalid {
1507 InvalidMode::Abort => {
1508 eprintln!("numfmt: {}", e);
1509 return Err(e);
1510 }
1511 InvalidMode::Fail => {
1512 eprintln!("numfmt: {}", e);
1513 output
1514 .write_all(line)
1515 .map_err(|e| format!("write error: {}", e))?;
1516 output
1517 .write_all(&[terminator])
1518 .map_err(|e| format!("write error: {}", e))?;
1519 had_error = true;
1520 }
1521 InvalidMode::Warn => {
1522 eprintln!("numfmt: {}", e);
1523 output
1524 .write_all(line)
1525 .map_err(|e| format!("write error: {}", e))?;
1526 output
1527 .write_all(&[terminator])
1528 .map_err(|e| format!("write error: {}", e))?;
1529 }
1530 InvalidMode::Ignore => {
1531 output
1532 .write_all(line)
1533 .map_err(|e| format!("write error: {}", e))?;
1534 output
1535 .write_all(&[terminator])
1536 .map_err(|e| format!("write error: {}", e))?;
1537 }
1538 },
1539 }
1540 }
1541
1542 output.flush().map_err(|e| format!("flush error: {}", e))?;
1543
1544 if had_error {
1545 Err("conversion errors occurred".to_string())
1546 } else {
1547 Ok(())
1548 }
1549}