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
46pub struct NumfmtConfig {
48 pub from: ScaleUnit,
49 pub to: ScaleUnit,
50 pub from_unit: f64,
51 pub to_unit: f64,
52 pub padding: Option<i32>,
53 pub round: RoundMethod,
54 pub suffix: Option<String>,
55 pub format: Option<String>,
56 pub field: Vec<usize>,
57 pub delimiter: Option<char>,
58 pub header: usize,
59 pub invalid: InvalidMode,
60 pub grouping: bool,
61 pub zero_terminated: bool,
62}
63
64impl Default for NumfmtConfig {
65 fn default() -> Self {
66 Self {
67 from: ScaleUnit::None,
68 to: ScaleUnit::None,
69 from_unit: 1.0,
70 to_unit: 1.0,
71 padding: None,
72 round: RoundMethod::Nearest,
73 suffix: None,
74 format: None,
75 field: vec![1],
76 delimiter: None,
77 header: 0,
78 invalid: InvalidMode::Abort,
79 grouping: false,
80 zero_terminated: false,
81 }
82 }
83}
84
85const SI_SUFFIXES: &[(char, f64)] = &[
88 ('k', 1e3),
89 ('M', 1e6),
90 ('G', 1e9),
91 ('T', 1e12),
92 ('P', 1e15),
93 ('E', 1e18),
94 ('Z', 1e21),
95 ('Y', 1e24),
96];
97
98const IEC_SUFFIXES: &[(char, f64)] = &[
100 ('K', 1024.0),
101 ('M', 1_048_576.0),
102 ('G', 1_073_741_824.0),
103 ('T', 1_099_511_627_776.0),
104 ('P', 1_125_899_906_842_624.0),
105 ('E', 1_152_921_504_606_846_976.0),
106 ('Z', 1_180_591_620_717_411_303_424.0),
107 ('Y', 1_208_925_819_614_629_174_706_176.0),
108];
109
110pub fn parse_scale_unit(s: &str) -> Result<ScaleUnit, String> {
112 match s {
113 "none" => Ok(ScaleUnit::None),
114 "si" => Ok(ScaleUnit::Si),
115 "iec" => Ok(ScaleUnit::Iec),
116 "iec-i" => Ok(ScaleUnit::IecI),
117 "auto" => Ok(ScaleUnit::Auto),
118 _ => Err(format!("invalid unit: '{}'", s)),
119 }
120}
121
122pub fn parse_round_method(s: &str) -> Result<RoundMethod, String> {
124 match s {
125 "up" => Ok(RoundMethod::Up),
126 "down" => Ok(RoundMethod::Down),
127 "from-zero" => Ok(RoundMethod::FromZero),
128 "towards-zero" => Ok(RoundMethod::TowardsZero),
129 "nearest" => Ok(RoundMethod::Nearest),
130 _ => Err(format!("invalid rounding method: '{}'", s)),
131 }
132}
133
134pub fn parse_invalid_mode(s: &str) -> Result<InvalidMode, String> {
136 match s {
137 "abort" => Ok(InvalidMode::Abort),
138 "fail" => Ok(InvalidMode::Fail),
139 "warn" => Ok(InvalidMode::Warn),
140 "ignore" => Ok(InvalidMode::Ignore),
141 _ => Err(format!("invalid mode: '{}'", s)),
142 }
143}
144
145pub fn parse_fields(s: &str) -> Result<Vec<usize>, String> {
148 if s == "-" {
149 return Ok(vec![]);
151 }
152 let mut fields = Vec::new();
153 for part in s.split(',') {
154 let part = part.trim();
155 if let Some(dash_pos) = part.find('-') {
156 let start_str = &part[..dash_pos];
157 let end_str = &part[dash_pos + 1..];
158 if start_str.is_empty() && end_str.is_empty() {
160 return Ok(vec![]);
161 }
162 let start: usize = if start_str.is_empty() {
163 1
164 } else {
165 start_str
166 .parse()
167 .map_err(|_| format!("invalid field value '{}'", part))?
168 };
169 let end: usize = if end_str.is_empty() {
170 9999
173 } else {
174 end_str
175 .parse()
176 .map_err(|_| format!("invalid field value '{}'", part))?
177 };
178 if start == 0 {
179 return Err(format!("fields are numbered from 1: '{}'", part));
180 }
181 for i in start..=end {
182 if !fields.contains(&i) {
183 fields.push(i);
184 }
185 }
186 } else {
187 let n: usize = part
188 .parse()
189 .map_err(|_| format!("invalid field value '{}'", part))?;
190 if n == 0 {
191 return Err("fields are numbered from 1".to_string());
192 }
193 if !fields.contains(&n) {
194 fields.push(n);
195 }
196 }
197 }
198 fields.sort();
199 Ok(fields)
200}
201
202fn parse_number_with_suffix(s: &str, unit: ScaleUnit) -> Result<f64, String> {
205 let s = s.trim();
206 if s.is_empty() {
207 return Err("invalid number: ''".to_string());
208 }
209
210 let mut num_end = s.len();
212 let bytes = s.as_bytes();
213 let len = s.len();
214
215 if len > 0 {
217 let last_char = bytes[len - 1] as char;
218
219 match unit {
220 ScaleUnit::Auto | ScaleUnit::IecI => {
221 if last_char == 'i' && len >= 2 {
223 let prefix_char = (bytes[len - 2] as char).to_ascii_uppercase();
224 if is_scale_suffix(prefix_char) {
225 num_end = len - 2;
226 }
227 } else {
228 let upper = last_char.to_ascii_uppercase();
229 if is_scale_suffix(upper) {
230 num_end = len - 1;
231 }
232 }
233 }
234 ScaleUnit::Si | ScaleUnit::Iec => {
235 let upper = last_char.to_ascii_uppercase();
236 if is_scale_suffix(upper) {
237 num_end = len - 1;
238 }
239 }
240 ScaleUnit::None => {}
241 }
242 }
243
244 let num_str = &s[..num_end];
245 let suffix_str = &s[num_end..];
246
247 let value: f64 = num_str
249 .parse()
250 .map_err(|_| format!("invalid number: '{}'", s))?;
251
252 let multiplier = if suffix_str.is_empty() {
254 1.0
255 } else {
256 let suffix_upper = suffix_str.chars().next().unwrap().to_ascii_uppercase();
257 match unit {
258 ScaleUnit::Auto => {
259 if suffix_str.len() >= 2 && suffix_str.ends_with('i') {
261 find_iec_multiplier(suffix_upper)?
262 } else {
263 find_si_multiplier(suffix_upper)?
264 }
265 }
266 ScaleUnit::Si => find_si_multiplier(suffix_upper)?,
267 ScaleUnit::Iec | ScaleUnit::IecI => find_iec_multiplier(suffix_upper)?,
268 ScaleUnit::None => {
269 return Err(format!("invalid number: '{}'", s));
270 }
271 }
272 };
273
274 Ok(value * multiplier)
275}
276
277fn is_scale_suffix(c: char) -> bool {
278 matches!(c, 'K' | 'M' | 'G' | 'T' | 'P' | 'E' | 'Z' | 'Y')
279}
280
281fn find_si_multiplier(c: char) -> Result<f64, String> {
282 for &(suffix, mult) in SI_SUFFIXES {
283 if suffix.eq_ignore_ascii_case(&c) {
284 return Ok(mult);
285 }
286 }
287 Err(format!("invalid suffix: '{}'", c))
288}
289
290fn find_iec_multiplier(c: char) -> Result<f64, String> {
291 for &(suffix, mult) in IEC_SUFFIXES {
292 if suffix == c {
293 return Ok(mult);
294 }
295 }
296 Err(format!("invalid suffix: '{}'", c))
297}
298
299fn apply_round(value: f64, method: RoundMethod) -> f64 {
301 match method {
302 RoundMethod::Up => value.ceil(),
303 RoundMethod::Down => value.floor(),
304 RoundMethod::FromZero => {
305 if value >= 0.0 {
306 value.ceil()
307 } else {
308 value.floor()
309 }
310 }
311 RoundMethod::TowardsZero => {
312 if value >= 0.0 {
313 value.floor()
314 } else {
315 value.ceil()
316 }
317 }
318 RoundMethod::Nearest => value.round(),
319 }
320}
321
322fn format_scaled(value: f64, unit: ScaleUnit, round: RoundMethod) -> String {
324 match unit {
325 ScaleUnit::None => {
326 format_plain_number(value)
328 }
329 ScaleUnit::Si => format_with_scale(value, SI_SUFFIXES, "", round),
330 ScaleUnit::Iec => format_with_scale(value, IEC_SUFFIXES, "", round),
331 ScaleUnit::IecI => format_with_scale(value, IEC_SUFFIXES, "i", round),
332 ScaleUnit::Auto => {
333 format_with_scale(value, SI_SUFFIXES, "", round)
335 }
336 }
337}
338
339fn format_plain_number(value: f64) -> String {
341 let int_val = value as i64;
342 if value == (int_val as f64) {
343 format!("{}", int_val)
344 } else {
345 format!("{:.1}", value)
347 }
348}
349
350fn format_with_scale(
352 value: f64,
353 suffixes: &[(char, f64)],
354 i_suffix: &str,
355 round: RoundMethod,
356) -> String {
357 let abs_value = value.abs();
358 let sign = if value < 0.0 { "-" } else { "" };
359
360 let mut chosen_suffix = None;
362 let mut chosen_mult = 1.0;
363
364 for &(suffix, mult) in suffixes.iter().rev() {
365 if abs_value >= mult {
366 chosen_suffix = Some(suffix);
367 chosen_mult = mult;
368 break;
369 }
370 }
371
372 if let Some(suffix) = chosen_suffix {
373 let scaled = value / chosen_mult;
374 let scaled = apply_round_for_display(scaled, round);
375
376 let base = suffixes[0].1; if scaled.abs() >= base {
380 let mut found_current = false;
382 for &(next_suffix, next_mult) in suffixes.iter() {
383 if found_current {
384 let re_scaled = value / next_mult;
385 let re_scaled = apply_round_for_display(re_scaled, round);
386 return format!("{sign}{:.1}{}{}", re_scaled.abs(), next_suffix, i_suffix);
387 }
388 if next_suffix == suffix {
389 found_current = true;
390 }
391 }
392 }
394
395 format!("{sign}{:.1}{}{}", scaled.abs(), suffix, i_suffix)
396 } else {
397 format_plain_number(value)
399 }
400}
401
402fn apply_round_for_display(value: f64, method: RoundMethod) -> f64 {
404 let factor = 10.0;
406 let shifted = value * factor;
407 let rounded = match method {
408 RoundMethod::Up => shifted.ceil(),
409 RoundMethod::Down => shifted.floor(),
410 RoundMethod::FromZero => {
411 if shifted >= 0.0 {
412 shifted.ceil()
413 } else {
414 shifted.floor()
415 }
416 }
417 RoundMethod::TowardsZero => {
418 if shifted >= 0.0 {
419 shifted.floor()
420 } else {
421 shifted.ceil()
422 }
423 }
424 RoundMethod::Nearest => shifted.round(),
425 };
426 rounded / factor
427}
428
429fn group_thousands(s: &str) -> String {
431 let (integer_part, rest) = if let Some(dot_pos) = s.find('.') {
433 (&s[..dot_pos], &s[dot_pos..])
434 } else {
435 (s, "")
436 };
437
438 let (sign, digits) = if integer_part.starts_with('-') {
440 ("-", &integer_part[1..])
441 } else {
442 ("", integer_part)
443 };
444
445 if digits.len() <= 3 {
446 return format!("{}{}{}", sign, digits, rest);
447 }
448
449 let mut result = String::with_capacity(digits.len() + digits.len() / 3);
450 let remainder = digits.len() % 3;
451 if remainder > 0 {
452 result.push_str(&digits[..remainder]);
453 }
454 for (i, chunk) in digits.as_bytes()[remainder..].chunks(3).enumerate() {
455 if i > 0 || remainder > 0 {
456 result.push(',');
457 }
458 result.push_str(std::str::from_utf8(chunk).unwrap());
459 }
460
461 format!("{}{}{}", sign, result, rest)
462}
463
464fn apply_format_padding(scaled: &str, fmt: &str) -> String {
467 let bytes = fmt.as_bytes();
468 let mut i = 0;
469
470 while i < bytes.len() && bytes[i] != b'%' {
472 i += 1;
473 }
474 let prefix = &fmt[..i];
475 if i >= bytes.len() {
476 return format!("{}{}", prefix, scaled);
477 }
478 i += 1; let mut left_align = false;
482 while i < bytes.len() {
483 match bytes[i] {
484 b'0' | b'+' | b' ' | b'#' | b'\'' => {}
485 b'-' => left_align = true,
486 _ => break,
487 }
488 i += 1;
489 }
490
491 let mut width: usize = 0;
493 while i < bytes.len() && bytes[i].is_ascii_digit() {
494 width = width
495 .saturating_mul(10)
496 .saturating_add((bytes[i] - b'0') as usize);
497 i += 1;
498 }
499
500 while i < bytes.len() && (bytes[i] == b'.' || bytes[i].is_ascii_digit()) {
502 i += 1;
503 }
504 if i < bytes.len() {
505 i += 1; }
507 let suffix = &fmt[i..];
508
509 let padded = if width > 0 && scaled.len() < width {
510 let pad_len = width - scaled.len();
511 if left_align {
512 format!("{}{}", scaled, " ".repeat(pad_len))
513 } else {
514 format!("{}{}", " ".repeat(pad_len), scaled)
515 }
516 } else {
517 scaled.to_string()
518 };
519
520 format!("{}{}{}", prefix, padded, suffix)
521}
522
523fn apply_format(value: f64, fmt: &str) -> Result<String, String> {
525 let bytes = fmt.as_bytes();
527 let mut i = 0;
528
529 while i < bytes.len() && bytes[i] != b'%' {
531 i += 1;
532 }
533 let prefix = &fmt[..i];
534 if i >= bytes.len() {
535 return Err(format!("invalid format: '{}'", fmt));
536 }
537 i += 1; if i >= bytes.len() {
540 return Err(format!("invalid format: '{}'", fmt));
541 }
542
543 if bytes[i] == b'%' {
545 return Ok(format!("{}%", prefix));
546 }
547
548 let mut zero_pad = false;
550 let mut left_align = false;
551 let mut plus_sign = false;
552 let mut space_sign = false;
553 while i < bytes.len() {
554 match bytes[i] {
555 b'0' => zero_pad = true,
556 b'-' => left_align = true,
557 b'+' => plus_sign = true,
558 b' ' => space_sign = true,
559 b'#' => {}
560 b'\'' => {} _ => break,
562 }
563 i += 1;
564 }
565
566 let mut width: usize = 0;
568 while i < bytes.len() && bytes[i].is_ascii_digit() {
569 width = width
570 .saturating_mul(10)
571 .saturating_add((bytes[i] - b'0') as usize);
572 i += 1;
573 }
574
575 let mut precision: Option<usize> = None;
577 if i < bytes.len() && bytes[i] == b'.' {
578 i += 1;
579 let mut prec: usize = 0;
580 while i < bytes.len() && bytes[i].is_ascii_digit() {
581 prec = prec
582 .saturating_mul(10)
583 .saturating_add((bytes[i] - b'0') as usize);
584 i += 1;
585 }
586 precision = Some(prec);
587 }
588
589 if i >= bytes.len() {
591 return Err(format!("invalid format: '{}'", fmt));
592 }
593 let conv = bytes[i] as char;
594 i += 1;
595 let suffix = &fmt[i..];
596
597 let prec = precision.unwrap_or(6);
598 let formatted = match conv {
599 'f' => format!("{:.prec$}", value, prec = prec),
600 'e' => format_scientific(value, prec, 'e'),
601 'E' => format_scientific(value, prec, 'E'),
602 'g' => format_g(value, prec, false),
603 'G' => format_g(value, prec, true),
604 _ => return Err(format!("invalid format character: '{}'", conv)),
605 };
606
607 let sign_str = if value < 0.0 {
609 ""
610 } else if plus_sign {
611 "+"
612 } else if space_sign {
613 " "
614 } else {
615 ""
616 };
617
618 let num_str = if !sign_str.is_empty() && !formatted.starts_with('-') {
619 format!("{}{}", sign_str, formatted)
620 } else {
621 formatted
622 };
623
624 let padded = if width > 0 && num_str.len() < width {
626 let pad_len = width - num_str.len();
627 if left_align {
628 format!("{}{}", num_str, " ".repeat(pad_len))
629 } else if zero_pad {
630 if num_str.starts_with('-') || num_str.starts_with('+') || num_str.starts_with(' ') {
631 let (sign, rest) = num_str.split_at(1);
632 format!("{}{}{}", sign, "0".repeat(pad_len), rest)
633 } else {
634 format!("{}{}", "0".repeat(pad_len), num_str)
635 }
636 } else {
637 format!("{}{}", " ".repeat(pad_len), num_str)
638 }
639 } else {
640 num_str
641 };
642
643 Ok(format!("{}{}{}", prefix, padded, suffix))
644}
645
646fn format_scientific(value: f64, prec: usize, e_char: char) -> String {
648 if value == 0.0 {
649 let sign = if value.is_sign_negative() { "-" } else { "" };
650 if prec == 0 {
651 return format!("{sign}0{e_char}+00");
652 }
653 return format!("{sign}0.{:0>prec$}{e_char}+00", "", prec = prec);
654 }
655
656 let abs = value.abs();
657 let sign = if value < 0.0 { "-" } else { "" };
658 let exp = abs.log10().floor() as i32;
659 let mantissa = abs / 10f64.powi(exp);
660
661 let factor = 10f64.powi(prec as i32);
662 let mantissa = (mantissa * factor).round() / factor;
663
664 let (mantissa, exp) = if mantissa >= 10.0 {
665 (mantissa / 10.0, exp + 1)
666 } else {
667 (mantissa, exp)
668 };
669
670 let exp_sign = if exp >= 0 { '+' } else { '-' };
671 let exp_abs = exp.unsigned_abs();
672
673 if prec == 0 {
674 format!("{sign}{mantissa:.0}{e_char}{exp_sign}{exp_abs:02}")
675 } else {
676 format!(
677 "{sign}{mantissa:.prec$}{e_char}{exp_sign}{exp_abs:02}",
678 prec = prec
679 )
680 }
681}
682
683fn format_g(value: f64, prec: usize, upper: bool) -> String {
685 let prec = if prec == 0 { 1 } else { prec };
686
687 if value == 0.0 {
688 let sign = if value.is_sign_negative() { "-" } else { "" };
689 return format!("{sign}0");
690 }
691
692 let abs = value.abs();
693 let exp = abs.log10().floor() as i32;
694 let e_char = if upper { 'E' } else { 'e' };
695
696 if exp < -4 || exp >= prec as i32 {
697 let sig_prec = prec.saturating_sub(1);
698 let s = format_scientific(value, sig_prec, e_char);
699 trim_g_zeros(&s)
700 } else {
701 let decimal_prec = if prec as i32 > exp + 1 {
702 (prec as i32 - exp - 1) as usize
703 } else {
704 0
705 };
706 let s = format!("{value:.decimal_prec$}");
707 trim_g_zeros(&s)
708 }
709}
710
711fn trim_g_zeros(s: &str) -> String {
712 if let Some(e_pos) = s.find(['e', 'E']) {
713 let (mantissa, exponent) = s.split_at(e_pos);
714 let trimmed = mantissa.trim_end_matches('0').trim_end_matches('.');
715 format!("{trimmed}{exponent}")
716 } else {
717 s.trim_end_matches('0').trim_end_matches('.').to_string()
718 }
719}
720
721fn convert_number(token: &str, config: &NumfmtConfig) -> Result<String, String> {
723 let raw_value = parse_number_with_suffix(token, config.from)?;
725
726 let value = raw_value * config.from_unit;
728
729 let value = value / config.to_unit;
731
732 let mut result = if let Some(ref fmt) = config.format {
734 if config.to != ScaleUnit::None {
736 let scaled = format_scaled(value, config.to, config.round);
737 apply_format_padding(&scaled, fmt)
739 } else {
740 let rounded = apply_round(value, config.round);
741 apply_format(rounded, fmt)?
742 }
743 } else if config.to != ScaleUnit::None {
744 format_scaled(value, config.to, config.round)
745 } else {
746 let rounded = apply_round(value, config.round);
747 format_plain_number(rounded)
748 };
749
750 if config.grouping {
752 result = group_thousands(&result);
753 }
754
755 if let Some(ref suffix) = config.suffix {
757 result.push_str(suffix);
758 }
759
760 if let Some(pad) = config.padding {
762 let pad_width = pad.unsigned_abs() as usize;
763 if result.len() < pad_width {
764 let deficit = pad_width - result.len();
765 if pad < 0 {
766 result = format!("{}{}", result, " ".repeat(deficit));
768 } else {
769 result = format!("{}{}", " ".repeat(deficit), result);
771 }
772 }
773 }
774
775 Ok(result)
776}
777
778fn split_fields<'a>(line: &'a str, delimiter: Option<char>) -> Vec<&'a str> {
780 match delimiter {
781 Some(delim) => line.split(delim).collect(),
782 None => {
783 let mut fields = Vec::new();
786 let mut chars = line.char_indices().peekable();
787 let mut field_start = 0;
788 let mut in_space = true;
789 let mut first = true;
790
791 while let Some(&(i, c)) = chars.peek() {
792 if c.is_whitespace() {
793 if !in_space && !first {
794 fields.push(&line[field_start..i]);
795 }
796 in_space = true;
797 chars.next();
798 } else {
799 if in_space {
800 field_start = i;
801 in_space = false;
802 first = false;
803 }
804 chars.next();
805 }
806 }
807 if !in_space {
808 fields.push(&line[field_start..]);
809 }
810
811 if fields.is_empty() {
812 vec![line]
813 } else {
814 fields
815 }
816 }
817 }
818}
819
820fn reassemble_fields(
822 original: &str,
823 fields: &[&str],
824 converted: &[String],
825 delimiter: Option<char>,
826) -> String {
827 match delimiter {
828 Some(delim) => converted.join(&delim.to_string()),
829 None => {
830 let mut result = String::with_capacity(original.len());
832 let mut field_idx = 0;
833 let mut in_space = true;
834 let mut i = 0;
835 let bytes = original.as_bytes();
836
837 while i < bytes.len() {
838 let c = bytes[i] as char;
839 if c.is_ascii_whitespace() {
840 if !in_space && field_idx > 0 {
841 }
843 result.push(c);
844 in_space = true;
845 i += 1;
846 } else {
847 if in_space {
848 in_space = false;
849 if field_idx < converted.len() {
851 result.push_str(&converted[field_idx]);
852 } else if field_idx < fields.len() {
853 result.push_str(fields[field_idx]);
854 }
855 field_idx += 1;
856 while i < bytes.len() && !(bytes[i] as char).is_ascii_whitespace() {
858 i += 1;
859 }
860 continue;
861 }
862 i += 1;
863 }
864 }
865
866 result
867 }
868 }
869}
870
871pub fn process_line(line: &str, config: &NumfmtConfig) -> Result<String, String> {
873 let fields = split_fields(line, config.delimiter);
874
875 if fields.is_empty() {
876 return Ok(line.to_string());
877 }
878
879 let all_fields = config.field.is_empty();
880
881 let mut converted: Vec<String> = Vec::with_capacity(fields.len());
882 for (i, field) in fields.iter().enumerate() {
883 let field_num = i + 1; let should_convert = all_fields || config.field.contains(&field_num);
885
886 if should_convert {
887 match convert_number(field, config) {
888 Ok(s) => converted.push(s),
889 Err(e) => match config.invalid {
890 InvalidMode::Abort => return Err(e),
891 InvalidMode::Fail => {
892 eprintln!("numfmt: {}", e);
893 converted.push(field.to_string());
894 }
895 InvalidMode::Warn => {
896 eprintln!("numfmt: {}", e);
897 converted.push(field.to_string());
898 }
899 InvalidMode::Ignore => {
900 converted.push(field.to_string());
901 }
902 },
903 }
904 } else {
905 converted.push(field.to_string());
906 }
907 }
908
909 Ok(reassemble_fields(
910 line,
911 &fields,
912 &converted,
913 config.delimiter,
914 ))
915}
916
917pub fn run_numfmt<R: std::io::BufRead, W: Write>(
919 input: R,
920 mut output: W,
921 config: &NumfmtConfig,
922) -> Result<(), String> {
923 let terminator = if config.zero_terminated { b'\0' } else { b'\n' };
924 let mut header_remaining = config.header;
925 let mut buf = Vec::new();
926 let mut reader = input;
927 let mut had_error = false;
928
929 loop {
930 buf.clear();
931 let bytes_read = reader
932 .read_until(terminator, &mut buf)
933 .map_err(|e| format!("read error: {}", e))?;
934 if bytes_read == 0 {
935 break;
936 }
937
938 let line = if buf.last() == Some(&terminator) {
940 &buf[..buf.len() - 1]
941 } else {
942 &buf[..]
943 };
944 let line_str = String::from_utf8_lossy(line);
945
946 if header_remaining > 0 {
947 header_remaining -= 1;
948 output
949 .write_all(line_str.as_bytes())
950 .map_err(|e| format!("write error: {}", e))?;
951 output
952 .write_all(&[terminator])
953 .map_err(|e| format!("write error: {}", e))?;
954 continue;
955 }
956
957 match process_line(&line_str, config) {
958 Ok(result) => {
959 output
960 .write_all(result.as_bytes())
961 .map_err(|e| format!("write error: {}", e))?;
962 output
963 .write_all(&[terminator])
964 .map_err(|e| format!("write error: {}", e))?;
965 }
966 Err(e) => {
967 match config.invalid {
968 InvalidMode::Abort => {
969 eprintln!("numfmt: {}", e);
970 return Err(e);
971 }
972 InvalidMode::Fail => {
973 eprintln!("numfmt: {}", e);
974 output
976 .write_all(line_str.as_bytes())
977 .map_err(|e| format!("write error: {}", e))?;
978 output
979 .write_all(&[terminator])
980 .map_err(|e| format!("write error: {}", e))?;
981 had_error = true;
982 }
983 InvalidMode::Warn => {
984 eprintln!("numfmt: {}", e);
985 output
986 .write_all(line_str.as_bytes())
987 .map_err(|e| format!("write error: {}", e))?;
988 output
989 .write_all(&[terminator])
990 .map_err(|e| format!("write error: {}", e))?;
991 }
992 InvalidMode::Ignore => {
993 output
994 .write_all(line_str.as_bytes())
995 .map_err(|e| format!("write error: {}", e))?;
996 output
997 .write_all(&[terminator])
998 .map_err(|e| format!("write error: {}", e))?;
999 }
1000 }
1001 }
1002 }
1003 }
1004
1005 output.flush().map_err(|e| format!("flush error: {}", e))?;
1006
1007 if had_error {
1008 Err("conversion errors occurred".to_string())
1009 } else {
1010 Ok(())
1011 }
1012}