Skip to main content

coreutils_rs/numfmt/
core.rs

1use std::io::Write;
2
3/// Unit scale for input/output conversion.
4#[derive(Debug, Clone, Copy, PartialEq, Eq)]
5pub enum ScaleUnit {
6    /// No scaling.
7    None,
8    /// SI: K=1000, M=10^6, G=10^9, T=10^12, P=10^15, E=10^18, Z=10^21, Y=10^24.
9    Si,
10    /// IEC: K=1024, M=1048576, G=2^30, T=2^40, P=2^50, E=2^60.
11    Iec,
12    /// IEC with 'i' suffix: Ki=1024, Mi=1048576, etc.
13    IecI,
14    /// Auto-detect from suffix (for --from=auto).
15    Auto,
16}
17
18/// Rounding method.
19#[derive(Debug, Clone, Copy, PartialEq, Eq)]
20pub enum RoundMethod {
21    /// Round up (toward +infinity).
22    Up,
23    /// Round down (toward -infinity).
24    Down,
25    /// Round away from zero.
26    FromZero,
27    /// Round toward zero.
28    TowardsZero,
29    /// Round to nearest, half away from zero (default).
30    Nearest,
31}
32
33/// How to handle invalid input.
34#[derive(Debug, Clone, Copy, PartialEq, Eq)]
35pub enum InvalidMode {
36    /// Print error and exit immediately.
37    Abort,
38    /// Print error but continue processing.
39    Fail,
40    /// Print warning but continue processing.
41    Warn,
42    /// Silently ignore invalid input.
43    Ignore,
44}
45
46/// Bitset for O(1) field membership testing.
47/// Fields are 1-based. Supports fields 1..=128 via u128.
48/// If `all` is true, all fields match.
49/// If a field > 128 is needed, we fall back to the `overflow` Vec.
50struct 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
94/// Configuration for the numfmt command.
95pub 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
133/// SI suffix table: suffix char -> multiplier.
134/// GNU coreutils 9.4 (ubuntu-latest CI) uses uppercase 'K' for SI kilo (1e3).
135const 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
148/// IEC suffix table: suffix char -> multiplier (powers of 1024).
149const 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
162/// Parse a scale unit string.
163pub 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
174/// Parse a round method string.
175pub 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
186/// Parse an invalid mode string.
187pub 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
197/// Parse a field specification string like "1", "1,3", "1-5", or "-".
198/// Returns 1-based field indices.
199pub fn parse_fields(s: &str) -> Result<Vec<usize>, String> {
200    if s == "-" {
201        // All fields - we represent this as an empty vec and handle it specially.
202        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            // Handle open ranges like "-5" or "3-"
211            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                // Open-ended range: we use 0 as sentinel for "all remaining"
223                // For simplicity, return a large upper bound.
224                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
254/// Parse a number with optional suffix, returning the raw numeric value.
255/// Handles suffixes like K, M, G, T, P, E, Z, Y (and Ki, Mi, etc. for iec-i).
256fn 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    // Find where the numeric part ends and the suffix begins.
263    let mut num_end = s.len();
264    let bytes = s.as_bytes();
265    let len = s.len();
266
267    // Check for trailing scale suffix characters.
268    if len > 0 {
269        let last_char = bytes[len - 1] as char;
270
271        match unit {
272            ScaleUnit::Auto | ScaleUnit::IecI => {
273                // Check for 'i' suffix (e.g., Ki, Mi).
274                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    // Parse the numeric part.
300    let value: f64 = num_str
301        .parse()
302        .map_err(|_| format!("invalid number: '{}'", s))?;
303
304    // Apply suffix multiplier.
305    let multiplier = if suffix_str.is_empty() {
306        1.0
307    } else {
308        let suffix_upper = suffix_str.as_bytes()[0].to_ascii_uppercase() as char;
309        match unit {
310            ScaleUnit::Auto => {
311                // Auto-detect: if suffix ends with 'i', use IEC; otherwise SI.
312                if suffix_str.len() >= 2 && suffix_str.as_bytes()[suffix_str.len() - 1] == b'i' {
313                    find_iec_multiplier(suffix_upper)?
314                } else {
315                    find_si_multiplier(suffix_upper)?
316                }
317            }
318            ScaleUnit::Si => find_si_multiplier(suffix_upper)?,
319            ScaleUnit::Iec | ScaleUnit::IecI => find_iec_multiplier(suffix_upper)?,
320            ScaleUnit::None => {
321                return Err(format!("invalid number: '{}'", s));
322            }
323        }
324    };
325
326    Ok(value * multiplier)
327}
328
329#[inline(always)]
330fn is_scale_suffix(c: char) -> bool {
331    matches!(c, 'K' | 'M' | 'G' | 'T' | 'P' | 'E' | 'Z' | 'Y' | 'R' | 'Q')
332}
333
334fn find_si_multiplier(c: char) -> Result<f64, String> {
335    match c.to_ascii_uppercase() {
336        'K' => Ok(1e3),
337        'M' => Ok(1e6),
338        'G' => Ok(1e9),
339        'T' => Ok(1e12),
340        'P' => Ok(1e15),
341        'E' => Ok(1e18),
342        'Z' => Ok(1e21),
343        'Y' => Ok(1e24),
344        'R' => Ok(1e27),
345        'Q' => Ok(1e30),
346        _ => Err(format!("invalid suffix: '{}'", c)),
347    }
348}
349
350fn find_iec_multiplier(c: char) -> Result<f64, String> {
351    match c {
352        'K' => Ok(1024.0),
353        'M' => Ok(1_048_576.0),
354        'G' => Ok(1_073_741_824.0),
355        'T' => Ok(1_099_511_627_776.0),
356        'P' => Ok(1_125_899_906_842_624.0),
357        'E' => Ok(1_152_921_504_606_846_976.0),
358        'Z' => Ok(1_180_591_620_717_411_303_424.0),
359        'Y' => Ok(1_208_925_819_614_629_174_706_176.0),
360        'R' => Ok(1_237_940_039_285_380_274_899_124_224.0),
361        'Q' => Ok(1_267_650_600_228_229_401_496_703_205_376.0),
362        _ => Err(format!("invalid suffix: '{}'", c)),
363    }
364}
365
366/// Apply rounding according to the specified method.
367#[inline(always)]
368fn apply_round(value: f64, method: RoundMethod) -> f64 {
369    match method {
370        RoundMethod::Up => value.ceil(),
371        RoundMethod::Down => value.floor(),
372        RoundMethod::FromZero => {
373            if value >= 0.0 {
374                value.ceil()
375            } else {
376                value.floor()
377            }
378        }
379        RoundMethod::TowardsZero => {
380            if value >= 0.0 {
381                value.floor()
382            } else {
383                value.ceil()
384            }
385        }
386        RoundMethod::Nearest => value.round(),
387    }
388}
389
390/// Format a number with scale suffix for output.
391fn format_scaled(value: f64, unit: ScaleUnit, round: RoundMethod) -> String {
392    match unit {
393        ScaleUnit::None => {
394            // Output as plain number.
395            format_plain_number(value)
396        }
397        ScaleUnit::Si => format_with_scale(value, SI_SUFFIXES, "", round),
398        ScaleUnit::Iec => format_with_scale(value, IEC_SUFFIXES, "", round),
399        ScaleUnit::IecI => format_with_scale(value, IEC_SUFFIXES, "i", round),
400        ScaleUnit::Auto => {
401            // For --to=auto, behave like SI.
402            format_with_scale(value, SI_SUFFIXES, "", round)
403        }
404    }
405}
406
407/// Write a scaled number directly to a byte buffer, avoiding String allocation.
408fn write_scaled_to_buf(buf: &mut Vec<u8>, value: f64, unit: ScaleUnit, round: RoundMethod) {
409    match unit {
410        ScaleUnit::None => {
411            write_plain_number_to_buf(buf, value);
412        }
413        ScaleUnit::Si => write_with_scale_to_buf(buf, value, SI_SUFFIXES, b"", round),
414        ScaleUnit::Iec => write_with_scale_to_buf(buf, value, IEC_SUFFIXES, b"", round),
415        ScaleUnit::IecI => write_with_scale_to_buf(buf, value, IEC_SUFFIXES, b"i", round),
416        ScaleUnit::Auto => write_with_scale_to_buf(buf, value, SI_SUFFIXES, b"", round),
417    }
418}
419
420/// Write a plain number to a byte buffer using itoa for integers.
421#[inline]
422fn write_plain_number_to_buf(buf: &mut Vec<u8>, value: f64) {
423    let int_val = value as i64;
424    if value == (int_val as f64) {
425        let mut itoa_buf = itoa::Buffer::new();
426        buf.extend_from_slice(itoa_buf.format(int_val).as_bytes());
427    } else {
428        // Use enough precision to avoid loss.
429        use std::io::Write;
430        let _ = write!(buf, "{:.1}", value);
431    }
432}
433
434/// Write a scaled number with suffix directly to a byte buffer.
435fn write_with_scale_to_buf(
436    buf: &mut Vec<u8>,
437    value: f64,
438    suffixes: &[(char, f64)],
439    i_suffix: &[u8],
440    round: RoundMethod,
441) {
442    let abs_value = value.abs();
443    let negative = value < 0.0;
444
445    // Find the largest suffix that applies.
446    let mut chosen_idx: Option<usize> = None;
447    for (idx, &(_suffix, mult)) in suffixes.iter().enumerate().rev() {
448        if abs_value >= mult {
449            chosen_idx = Some(idx);
450            break;
451        }
452    }
453
454    let Some(mut idx) = chosen_idx else {
455        // Value is smaller than the smallest suffix, output as-is.
456        write_plain_number_to_buf(buf, value);
457        return;
458    };
459
460    loop {
461        let (suffix, mult) = suffixes[idx];
462        let scaled = value / mult;
463        let abs_scaled = scaled.abs();
464
465        if abs_scaled < 10.0 {
466            let rounded = apply_round_for_display(scaled, round);
467            if rounded.abs() >= 10.0 {
468                let int_val = rounded as i64;
469                if int_val.unsigned_abs() >= 1000 && idx + 1 < suffixes.len() {
470                    idx += 1;
471                    continue;
472                }
473                if negative {
474                    buf.push(b'-');
475                }
476                let mut itoa_buf = itoa::Buffer::new();
477                buf.extend_from_slice(itoa_buf.format(int_val.unsigned_abs()).as_bytes());
478                buf.push(suffix as u8);
479                buf.extend_from_slice(i_suffix);
480                return;
481            }
482            if negative {
483                buf.push(b'-');
484            }
485            // Write N.N format manually
486            let abs_rounded = rounded.abs();
487            let int_part = abs_rounded as u64;
488            let frac_part = ((abs_rounded - int_part as f64) * 10.0).round() as u8;
489            let mut itoa_buf = itoa::Buffer::new();
490            buf.extend_from_slice(itoa_buf.format(int_part).as_bytes());
491            buf.push(b'.');
492            buf.push(b'0' + frac_part);
493            buf.push(suffix as u8);
494            buf.extend_from_slice(i_suffix);
495            return;
496        } else {
497            let int_val = apply_round_int(scaled, round);
498            if int_val.unsigned_abs() >= 1000 {
499                if idx + 1 < suffixes.len() {
500                    idx += 1;
501                    continue;
502                }
503            }
504            if negative {
505                buf.push(b'-');
506            }
507            let mut itoa_buf = itoa::Buffer::new();
508            buf.extend_from_slice(itoa_buf.format(int_val.unsigned_abs()).as_bytes());
509            buf.push(suffix as u8);
510            buf.extend_from_slice(i_suffix);
511            return;
512        }
513    }
514}
515
516/// Format a plain number, removing unnecessary trailing zeros and decimal point.
517fn format_plain_number(value: f64) -> String {
518    let int_val = value as i64;
519    if value == (int_val as f64) {
520        let mut buf = itoa::Buffer::new();
521        buf.format(int_val).to_string()
522    } else {
523        // Use enough precision to avoid loss.
524        format!("{:.1}", value)
525    }
526}
527
528/// Format a number with appropriate scale suffix.
529/// Matches GNU numfmt behavior:
530/// - If scaled value < 10: display with 1 decimal place ("N.Nk")
531/// - If scaled value >= 10: display as integer ("NNk")
532/// - If integer would be >= 1000: promote to next suffix
533fn format_with_scale(
534    value: f64,
535    suffixes: &[(char, f64)],
536    i_suffix: &str,
537    round: RoundMethod,
538) -> String {
539    let abs_value = value.abs();
540    let sign = if value < 0.0 { "-" } else { "" };
541
542    // Find the largest suffix that applies.
543    let mut chosen_idx: Option<usize> = None;
544
545    for (idx, &(_suffix, mult)) in suffixes.iter().enumerate().rev() {
546        if abs_value >= mult {
547            chosen_idx = Some(idx);
548            break;
549        }
550    }
551
552    let Some(mut idx) = chosen_idx else {
553        // Value is smaller than the smallest suffix, output as-is.
554        return format_plain_number(value);
555    };
556
557    loop {
558        let (suffix, mult) = suffixes[idx];
559        let scaled = value / mult;
560        let abs_scaled = scaled.abs();
561
562        if abs_scaled < 10.0 {
563            // Display with 1 decimal place: "N.Nk"
564            let rounded = apply_round_for_display(scaled, round);
565            if rounded.abs() >= 10.0 {
566                // Rounding pushed it past 10, switch to integer display.
567                let int_val = rounded as i64;
568                if int_val.unsigned_abs() >= 1000 && idx + 1 < suffixes.len() {
569                    idx += 1;
570                    continue;
571                }
572                let mut itoa_buf = itoa::Buffer::new();
573                let digits = itoa_buf.format(int_val.unsigned_abs());
574                return format!("{sign}{}{}{}", digits, suffix, i_suffix);
575            }
576            return format!("{sign}{:.1}{}{}", rounded.abs(), suffix, i_suffix);
577        } else {
578            // Display as integer: "NNk"
579            let int_val = apply_round_int(scaled, round);
580            if int_val.unsigned_abs() >= 1000 {
581                if idx + 1 < suffixes.len() {
582                    idx += 1;
583                    continue;
584                }
585                // No next suffix, just output what we have.
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    }
592}
593
594/// Apply rounding for display purposes (when formatting scaled output).
595/// Rounds to 1 decimal place.
596#[inline(always)]
597fn apply_round_for_display(value: f64, method: RoundMethod) -> f64 {
598    let factor = 10.0;
599    let shifted = value * factor;
600    let rounded = match method {
601        RoundMethod::Up => shifted.ceil(),
602        RoundMethod::Down => shifted.floor(),
603        RoundMethod::FromZero => {
604            if shifted >= 0.0 {
605                shifted.ceil()
606            } else {
607                shifted.floor()
608            }
609        }
610        RoundMethod::TowardsZero => {
611            if shifted >= 0.0 {
612                shifted.floor()
613            } else {
614                shifted.ceil()
615            }
616        }
617        RoundMethod::Nearest => shifted.round(),
618    };
619    rounded / factor
620}
621
622/// Apply rounding to get an integer value for display.
623#[inline(always)]
624fn apply_round_int(value: f64, method: RoundMethod) -> i64 {
625    match method {
626        RoundMethod::Up => value.ceil() as i64,
627        RoundMethod::Down => value.floor() as i64,
628        RoundMethod::FromZero => {
629            if value >= 0.0 {
630                value.ceil() as i64
631            } else {
632                value.floor() as i64
633            }
634        }
635        RoundMethod::TowardsZero => {
636            if value >= 0.0 {
637                value.floor() as i64
638            } else {
639                value.ceil() as i64
640            }
641        }
642        RoundMethod::Nearest => value.round() as i64,
643    }
644}
645
646/// Insert thousands grouping separators.
647fn group_thousands(s: &str) -> String {
648    // Find the integer part (before any decimal point).
649    let (integer_part, rest) = if let Some(dot_pos) = s.find('.') {
650        (&s[..dot_pos], &s[dot_pos..])
651    } else {
652        (s, "")
653    };
654
655    // Handle sign.
656    let (sign, digits) = if integer_part.starts_with('-') {
657        ("-", &integer_part[1..])
658    } else {
659        ("", integer_part)
660    };
661
662    if digits.len() <= 3 {
663        return format!("{}{}{}", sign, digits, rest);
664    }
665
666    let mut result = String::with_capacity(digits.len() + digits.len() / 3);
667    let remainder = digits.len() % 3;
668    if remainder > 0 {
669        result.push_str(&digits[..remainder]);
670    }
671    for (i, chunk) in digits.as_bytes()[remainder..].chunks(3).enumerate() {
672        if i > 0 || remainder > 0 {
673            result.push(',');
674        }
675        result.push_str(std::str::from_utf8(chunk).unwrap());
676    }
677
678    format!("{}{}{}", sign, result, rest)
679}
680
681/// Apply width/padding from a printf-style format string to an already-scaled string.
682/// Used when both --to and --format are specified.
683fn apply_format_padding(scaled: &str, fmt: &str) -> String {
684    let bytes = fmt.as_bytes();
685    let mut i = 0;
686
687    // Find '%'.
688    while i < bytes.len() && bytes[i] != b'%' {
689        i += 1;
690    }
691    let prefix = &fmt[..i];
692    if i >= bytes.len() {
693        return format!("{}{}", prefix, scaled);
694    }
695    i += 1; // skip '%'
696
697    // Parse flags.
698    let mut left_align = false;
699    while i < bytes.len() {
700        match bytes[i] {
701            b'0' | b'+' | b' ' | b'#' | b'\'' => {}
702            b'-' => left_align = true,
703            _ => break,
704        }
705        i += 1;
706    }
707
708    // Parse width.
709    let mut width: usize = 0;
710    while i < bytes.len() && bytes[i].is_ascii_digit() {
711        width = width
712            .saturating_mul(10)
713            .saturating_add((bytes[i] - b'0') as usize);
714        i += 1;
715    }
716
717    // Skip precision and conversion char.
718    while i < bytes.len() && (bytes[i] == b'.' || bytes[i].is_ascii_digit()) {
719        i += 1;
720    }
721    if i < bytes.len() {
722        i += 1; // skip conversion char
723    }
724    let suffix = &fmt[i..];
725
726    let padded = if width > 0 && scaled.len() < width {
727        let pad_len = width - scaled.len();
728        if left_align {
729            format!("{}{}", scaled, " ".repeat(pad_len))
730        } else {
731            format!("{}{}", " ".repeat(pad_len), scaled)
732        }
733    } else {
734        scaled.to_string()
735    };
736
737    format!("{}{}{}", prefix, padded, suffix)
738}
739
740/// Pre-parsed format specification for fast repeated formatting.
741struct ParsedFormat {
742    prefix: String,
743    suffix: String,
744    zero_pad: bool,
745    left_align: bool,
746    plus_sign: bool,
747    space_sign: bool,
748    width: usize,
749    precision: Option<usize>,
750    conv: char,
751    is_percent: bool,
752}
753
754/// Parse a printf-style format string once, for reuse across many values.
755fn parse_format_spec(fmt: &str) -> Result<ParsedFormat, String> {
756    let bytes = fmt.as_bytes();
757    let mut i = 0;
758
759    while i < bytes.len() && bytes[i] != b'%' {
760        i += 1;
761    }
762    let prefix = fmt[..i].to_string();
763    if i >= bytes.len() {
764        return Err(format!("invalid format: '{}'", fmt));
765    }
766    i += 1;
767
768    if i >= bytes.len() {
769        return Err(format!("invalid format: '{}'", fmt));
770    }
771
772    if bytes[i] == b'%' {
773        return Ok(ParsedFormat {
774            prefix,
775            suffix: String::new(),
776            zero_pad: false,
777            left_align: false,
778            plus_sign: false,
779            space_sign: false,
780            width: 0,
781            precision: None,
782            conv: '%',
783            is_percent: true,
784        });
785    }
786
787    let mut zero_pad = false;
788    let mut left_align = false;
789    let mut plus_sign = false;
790    let mut space_sign = false;
791    while i < bytes.len() {
792        match bytes[i] {
793            b'0' => zero_pad = true,
794            b'-' => left_align = true,
795            b'+' => plus_sign = true,
796            b' ' => space_sign = true,
797            b'#' | b'\'' => {}
798            _ => break,
799        }
800        i += 1;
801    }
802
803    let mut width: usize = 0;
804    while i < bytes.len() && bytes[i].is_ascii_digit() {
805        width = width
806            .saturating_mul(10)
807            .saturating_add((bytes[i] - b'0') as usize);
808        i += 1;
809    }
810
811    let mut precision: Option<usize> = None;
812    if i < bytes.len() && bytes[i] == b'.' {
813        i += 1;
814        let mut prec: usize = 0;
815        while i < bytes.len() && bytes[i].is_ascii_digit() {
816            prec = prec
817                .saturating_mul(10)
818                .saturating_add((bytes[i] - b'0') as usize);
819            i += 1;
820        }
821        precision = Some(prec);
822    }
823
824    if i >= bytes.len() {
825        return Err(format!("invalid format: '{}'", fmt));
826    }
827    let conv = bytes[i] as char;
828    i += 1;
829    let suffix = fmt[i..].to_string();
830
831    Ok(ParsedFormat {
832        prefix,
833        suffix,
834        zero_pad,
835        left_align,
836        plus_sign,
837        space_sign,
838        width,
839        precision,
840        conv,
841        is_percent: false,
842    })
843}
844
845/// Apply a pre-parsed format specification to a number value.
846fn apply_parsed_format(value: f64, pf: &ParsedFormat) -> Result<String, String> {
847    if pf.is_percent {
848        return Ok(format!("{}%", pf.prefix));
849    }
850
851    let prec = pf.precision.unwrap_or(6);
852    let formatted = match pf.conv {
853        'f' => format!("{:.prec$}", value, prec = prec),
854        'e' => format_scientific(value, prec, 'e'),
855        'E' => format_scientific(value, prec, 'E'),
856        'g' => format_g(value, prec, false),
857        'G' => format_g(value, prec, true),
858        _ => return Err(format!("invalid format character: '{}'", pf.conv)),
859    };
860
861    let sign_str = if value < 0.0 {
862        ""
863    } else if pf.plus_sign {
864        "+"
865    } else if pf.space_sign {
866        " "
867    } else {
868        ""
869    };
870
871    let num_str = if !sign_str.is_empty() && !formatted.starts_with('-') {
872        format!("{}{}", sign_str, formatted)
873    } else {
874        formatted
875    };
876
877    let padded = if pf.width > 0 && num_str.len() < pf.width {
878        let pad_len = pf.width - num_str.len();
879        if pf.left_align {
880            format!("{}{}", num_str, " ".repeat(pad_len))
881        } else if pf.zero_pad {
882            if num_str.starts_with('-') || num_str.starts_with('+') || num_str.starts_with(' ') {
883                let (sign, rest) = num_str.split_at(1);
884                format!("{}{}{}", sign, "0".repeat(pad_len), rest)
885            } else {
886                format!("{}{}", "0".repeat(pad_len), num_str)
887            }
888        } else {
889            format!("{}{}", " ".repeat(pad_len), num_str)
890        }
891    } else {
892        num_str
893    };
894
895    Ok(format!("{}{}{}", pf.prefix, padded, pf.suffix))
896}
897
898/// Format in scientific notation.
899fn format_scientific(value: f64, prec: usize, e_char: char) -> String {
900    if value == 0.0 {
901        let sign = if value.is_sign_negative() { "-" } else { "" };
902        if prec == 0 {
903            return format!("{sign}0{e_char}+00");
904        }
905        return format!("{sign}0.{:0>prec$}{e_char}+00", "", prec = prec);
906    }
907
908    let abs = value.abs();
909    let sign = if value < 0.0 { "-" } else { "" };
910    let exp = abs.log10().floor() as i32;
911    let mantissa = abs / 10f64.powi(exp);
912
913    let factor = 10f64.powi(prec as i32);
914    let mantissa = (mantissa * factor).round() / factor;
915
916    let (mantissa, exp) = if mantissa >= 10.0 {
917        (mantissa / 10.0, exp + 1)
918    } else {
919        (mantissa, exp)
920    };
921
922    let exp_sign = if exp >= 0 { '+' } else { '-' };
923    let exp_abs = exp.unsigned_abs();
924
925    if prec == 0 {
926        format!("{sign}{mantissa:.0}{e_char}{exp_sign}{exp_abs:02}")
927    } else {
928        format!(
929            "{sign}{mantissa:.prec$}{e_char}{exp_sign}{exp_abs:02}",
930            prec = prec
931        )
932    }
933}
934
935/// Format using %g - shortest representation.
936fn format_g(value: f64, prec: usize, upper: bool) -> String {
937    let prec = if prec == 0 { 1 } else { prec };
938
939    if value == 0.0 {
940        let sign = if value.is_sign_negative() { "-" } else { "" };
941        return format!("{sign}0");
942    }
943
944    let abs = value.abs();
945    let exp = abs.log10().floor() as i32;
946    let e_char = if upper { 'E' } else { 'e' };
947
948    if exp < -4 || exp >= prec as i32 {
949        let sig_prec = prec.saturating_sub(1);
950        let s = format_scientific(value, sig_prec, e_char);
951        trim_g_zeros(&s)
952    } else {
953        let decimal_prec = if prec as i32 > exp + 1 {
954            (prec as i32 - exp - 1) as usize
955        } else {
956            0
957        };
958        let s = format!("{value:.decimal_prec$}");
959        trim_g_zeros(&s)
960    }
961}
962
963fn trim_g_zeros(s: &str) -> String {
964    if let Some(e_pos) = s.find(['e', 'E']) {
965        let (mantissa, exponent) = s.split_at(e_pos);
966        let trimmed = mantissa.trim_end_matches('0').trim_end_matches('.');
967        format!("{trimmed}{exponent}")
968    } else {
969        s.trim_end_matches('0').trim_end_matches('.').to_string()
970    }
971}
972
973/// Convert a single numeric token according to the config.
974fn convert_number(
975    token: &str,
976    config: &NumfmtConfig,
977    parsed_fmt: Option<&ParsedFormat>,
978) -> Result<String, String> {
979    // Parse the input number (with optional suffix).
980    let raw_value = parse_number_with_suffix(token, config.from)?;
981
982    // Apply from-unit scaling.
983    let value = raw_value * config.from_unit;
984
985    // Apply to-unit scaling.
986    let value = value / config.to_unit;
987
988    // Format the output.
989    let mut result = if let Some(pf) = parsed_fmt {
990        // If --to is also specified, first scale, then apply format padding.
991        if config.to != ScaleUnit::None {
992            let scaled = format_scaled(value, config.to, config.round);
993            apply_format_padding(&scaled, config.format.as_deref().unwrap_or("%f"))
994        } else {
995            let rounded = apply_round(value, config.round);
996            apply_parsed_format(rounded, pf)?
997        }
998    } else if config.to != ScaleUnit::None {
999        format_scaled(value, config.to, config.round)
1000    } else {
1001        let rounded = apply_round(value, config.round);
1002        format_plain_number(rounded)
1003    };
1004
1005    // Apply grouping.
1006    if config.grouping {
1007        result = group_thousands(&result);
1008    }
1009
1010    // Apply suffix.
1011    if let Some(ref suffix) = config.suffix {
1012        result.push_str(suffix);
1013    }
1014
1015    // Apply padding.
1016    if let Some(pad) = config.padding {
1017        let pad_width = pad.unsigned_abs() as usize;
1018        if result.len() < pad_width {
1019            let deficit = pad_width - result.len();
1020            if pad < 0 {
1021                // Left-align (pad on right).
1022                result = format!("{}{}", result, " ".repeat(deficit));
1023            } else {
1024                // Right-align (pad on left).
1025                result = format!("{}{}", " ".repeat(deficit), result);
1026            }
1027        }
1028    }
1029
1030    Ok(result)
1031}
1032
1033/// Convert a numeric token and write result directly to a byte buffer.
1034/// Returns Ok(true) if conversion succeeded, Ok(false) if the original token
1035/// should be written instead (for non-abort error modes).
1036fn convert_number_to_buf(
1037    token: &str,
1038    config: &NumfmtConfig,
1039    parsed_fmt: Option<&ParsedFormat>,
1040    out: &mut Vec<u8>,
1041) -> Result<(), String> {
1042    // Parse the input number (with optional suffix).
1043    let raw_value = parse_number_with_suffix(token, config.from)?;
1044
1045    // Apply from-unit and to-unit scaling.
1046    let value = raw_value * config.from_unit / config.to_unit;
1047
1048    // Check if we can use the fast path: no format, no grouping, no padding, no suffix.
1049    let use_fast = parsed_fmt.is_none()
1050        && !config.grouping
1051        && config.suffix.is_none()
1052        && config.padding.is_none();
1053
1054    if use_fast && config.to != ScaleUnit::None {
1055        write_scaled_to_buf(out, value, config.to, config.round);
1056        return Ok(());
1057    }
1058
1059    if use_fast && config.to == ScaleUnit::None {
1060        let rounded = apply_round(value, config.round);
1061        write_plain_number_to_buf(out, rounded);
1062        return Ok(());
1063    }
1064
1065    // Slow path: use String-based convert_number for complex formatting.
1066    let result = if let Some(pf) = parsed_fmt {
1067        if config.to != ScaleUnit::None {
1068            let scaled = format_scaled(value, config.to, config.round);
1069            apply_format_padding(&scaled, config.format.as_deref().unwrap_or("%f"))
1070        } else {
1071            let rounded = apply_round(value, config.round);
1072            apply_parsed_format(rounded, pf)?
1073        }
1074    } else if config.to != ScaleUnit::None {
1075        format_scaled(value, config.to, config.round)
1076    } else {
1077        let rounded = apply_round(value, config.round);
1078        format_plain_number(rounded)
1079    };
1080
1081    let mut result = result;
1082
1083    if config.grouping {
1084        result = group_thousands(&result);
1085    }
1086
1087    if let Some(ref suffix) = config.suffix {
1088        result.push_str(suffix);
1089    }
1090
1091    if let Some(pad) = config.padding {
1092        let pad_width = pad.unsigned_abs() as usize;
1093        if result.len() < pad_width {
1094            let deficit = pad_width - result.len();
1095            if pad < 0 {
1096                result = format!("{}{}", result, " ".repeat(deficit));
1097            } else {
1098                result = format!("{}{}", " ".repeat(deficit), result);
1099            }
1100        }
1101    }
1102
1103    out.extend_from_slice(result.as_bytes());
1104    Ok(())
1105}
1106
1107/// Split a line into fields based on the delimiter.
1108fn split_fields<'a>(line: &'a str, delimiter: Option<char>) -> Vec<&'a str> {
1109    match delimiter {
1110        Some(delim) => line.split(delim).collect(),
1111        None => {
1112            // Whitespace splitting: split on runs of whitespace, but preserve
1113            // leading whitespace as empty fields.
1114            let mut fields = Vec::new();
1115            let bytes = line.as_bytes();
1116            let len = bytes.len();
1117            let mut i = 0;
1118            let mut field_start = 0;
1119            let mut in_space = true;
1120            let mut first = true;
1121
1122            while i < len {
1123                let c = bytes[i];
1124                if c == b' ' || c == b'\t' || c == b'\r' || c == b'\x0b' || c == b'\x0c' {
1125                    if !in_space && !first {
1126                        fields.push(&line[field_start..i]);
1127                    }
1128                    in_space = true;
1129                    i += 1;
1130                } else {
1131                    if in_space {
1132                        field_start = i;
1133                        in_space = false;
1134                        first = false;
1135                    }
1136                    i += 1;
1137                }
1138            }
1139            if !in_space {
1140                fields.push(&line[field_start..]);
1141            }
1142
1143            if fields.is_empty() {
1144                vec![line]
1145            } else {
1146                fields
1147            }
1148        }
1149    }
1150}
1151
1152/// Reassemble fields into a line with proper spacing.
1153fn reassemble_fields(
1154    original: &str,
1155    fields: &[&str],
1156    converted: &[String],
1157    delimiter: Option<char>,
1158) -> String {
1159    match delimiter {
1160        Some(delim) => converted.join(&delim.to_string()),
1161        None => {
1162            // For whitespace-delimited input, reconstruct preserving original spacing.
1163            let mut result = String::with_capacity(original.len());
1164            let mut field_idx = 0;
1165            let mut in_space = true;
1166            let mut i = 0;
1167            let bytes = original.as_bytes();
1168
1169            while i < bytes.len() {
1170                let c = bytes[i] as char;
1171                if c.is_ascii_whitespace() {
1172                    if !in_space && field_idx > 0 {
1173                        // We just finished a field.
1174                    }
1175                    result.push(c);
1176                    in_space = true;
1177                    i += 1;
1178                } else {
1179                    if in_space {
1180                        in_space = false;
1181                        // Output the converted field instead of the original.
1182                        if field_idx < converted.len() {
1183                            result.push_str(&converted[field_idx]);
1184                        } else if field_idx < fields.len() {
1185                            result.push_str(fields[field_idx]);
1186                        }
1187                        field_idx += 1;
1188                        // Skip past the original field characters.
1189                        while i < bytes.len() && !(bytes[i] as char).is_ascii_whitespace() {
1190                            i += 1;
1191                        }
1192                        continue;
1193                    }
1194                    i += 1;
1195                }
1196            }
1197
1198            result
1199        }
1200    }
1201}
1202
1203/// Process a single line according to the numfmt configuration.
1204pub fn process_line(line: &str, config: &NumfmtConfig) -> Result<String, String> {
1205    process_line_with_fmt(line, config, None)
1206}
1207
1208/// Process a single line with a pre-parsed format specification.
1209fn process_line_with_fmt(
1210    line: &str,
1211    config: &NumfmtConfig,
1212    parsed_fmt: Option<&ParsedFormat>,
1213) -> Result<String, String> {
1214    let fields = split_fields(line, config.delimiter);
1215
1216    if fields.is_empty() {
1217        return Ok(line.to_string());
1218    }
1219
1220    let all_fields = config.field.is_empty();
1221
1222    let mut converted: Vec<String> = Vec::with_capacity(fields.len());
1223    for (i, field) in fields.iter().enumerate() {
1224        let field_num = i + 1; // 1-based
1225        let should_convert = all_fields || config.field.contains(&field_num);
1226
1227        if should_convert {
1228            match convert_number(field, config, parsed_fmt) {
1229                Ok(s) => converted.push(s),
1230                Err(e) => match config.invalid {
1231                    InvalidMode::Abort => return Err(e),
1232                    InvalidMode::Fail => {
1233                        eprintln!("numfmt: {}", e);
1234                        converted.push(field.to_string());
1235                    }
1236                    InvalidMode::Warn => {
1237                        eprintln!("numfmt: {}", e);
1238                        converted.push(field.to_string());
1239                    }
1240                    InvalidMode::Ignore => {
1241                        converted.push(field.to_string());
1242                    }
1243                },
1244            }
1245        } else {
1246            converted.push(field.to_string());
1247        }
1248    }
1249
1250    Ok(reassemble_fields(
1251        line,
1252        &fields,
1253        &converted,
1254        config.delimiter,
1255    ))
1256}
1257
1258/// Fast path: process a delimiter-separated line by writing directly to output buffer.
1259/// Scans for delimiter byte positions, writes non-target fields as raw bytes,
1260/// converts only target fields. No intermediate String allocations.
1261fn process_line_fast_delim(
1262    line: &[u8],
1263    delim: u8,
1264    field_set: &FieldSet,
1265    config: &NumfmtConfig,
1266    parsed_fmt: Option<&ParsedFormat>,
1267    out: &mut Vec<u8>,
1268) -> Result<(), String> {
1269    let mut field_num: usize = 1;
1270    let mut start = 0;
1271    let len = line.len();
1272
1273    loop {
1274        // Find next delimiter or end of line.
1275        let end = memchr::memchr(delim, &line[start..])
1276            .map(|pos| start + pos)
1277            .unwrap_or(len);
1278
1279        if field_set.contains(field_num) {
1280            // This field needs conversion.
1281            // Safety: we treat the bytes as str. For ASCII numeric fields this is fine.
1282            // If non-UTF8, the parse will fail gracefully.
1283            let field_str = std::str::from_utf8(&line[start..end])
1284                .map_err(|_| "invalid number: '<non-utf8>'".to_string())?;
1285
1286            match convert_number_to_buf(field_str, config, parsed_fmt, out) {
1287                Ok(()) => {}
1288                Err(e) => match config.invalid {
1289                    InvalidMode::Abort => return Err(e),
1290                    InvalidMode::Fail | InvalidMode::Warn => {
1291                        eprintln!("numfmt: {}", e);
1292                        out.extend_from_slice(&line[start..end]);
1293                    }
1294                    InvalidMode::Ignore => {
1295                        out.extend_from_slice(&line[start..end]);
1296                    }
1297                },
1298            }
1299        } else {
1300            // Write field bytes directly without conversion.
1301            out.extend_from_slice(&line[start..end]);
1302        }
1303
1304        if end >= len {
1305            break;
1306        }
1307
1308        // Write delimiter.
1309        out.push(delim);
1310        start = end + 1;
1311        field_num += 1;
1312    }
1313
1314    Ok(())
1315}
1316
1317/// Fast path: process a whitespace-separated line by writing directly to output buffer.
1318/// Preserves original whitespace. Converts only target fields.
1319fn process_line_fast_ws(
1320    line: &[u8],
1321    field_set: &FieldSet,
1322    config: &NumfmtConfig,
1323    parsed_fmt: Option<&ParsedFormat>,
1324    out: &mut Vec<u8>,
1325) -> Result<(), String> {
1326    let len = line.len();
1327    let mut i = 0;
1328    let mut field_num: usize = 0;
1329
1330    // State machine: alternate between whitespace and field content.
1331    while i < len {
1332        let c = line[i];
1333        if c == b' ' || c == b'\t' || c == b'\r' || c == b'\x0b' || c == b'\x0c' {
1334            // Whitespace: write directly.
1335            out.push(c);
1336            i += 1;
1337        } else {
1338            // Start of a field.
1339            field_num += 1;
1340            let field_start = i;
1341
1342            // Find end of field.
1343            while i < len {
1344                let fc = line[i];
1345                if fc == b' ' || fc == b'\t' || fc == b'\r' || fc == b'\x0b' || fc == b'\x0c' {
1346                    break;
1347                }
1348                i += 1;
1349            }
1350            let field_end = i;
1351
1352            if field_set.contains(field_num) {
1353                let field_str = std::str::from_utf8(&line[field_start..field_end])
1354                    .map_err(|_| "invalid number: '<non-utf8>'".to_string())?;
1355
1356                match convert_number_to_buf(field_str, config, parsed_fmt, out) {
1357                    Ok(()) => {}
1358                    Err(e) => match config.invalid {
1359                        InvalidMode::Abort => return Err(e),
1360                        InvalidMode::Fail | InvalidMode::Warn => {
1361                            eprintln!("numfmt: {}", e);
1362                            out.extend_from_slice(&line[field_start..field_end]);
1363                        }
1364                        InvalidMode::Ignore => {
1365                            out.extend_from_slice(&line[field_start..field_end]);
1366                        }
1367                    },
1368                }
1369            } else {
1370                out.extend_from_slice(&line[field_start..field_end]);
1371            }
1372        }
1373    }
1374
1375    // Handle completely blank / empty lines.
1376    // GNU numfmt treats an empty line as an invalid number at field 1.
1377    if field_num == 0 {
1378        if field_set.contains(1) {
1379            match convert_number_to_buf("", config, parsed_fmt, out) {
1380                Ok(()) => {}
1381                Err(e) => match config.invalid {
1382                    InvalidMode::Abort | InvalidMode::Fail => return Err(e),
1383                    InvalidMode::Warn => {
1384                        eprintln!("numfmt: {}", e);
1385                    }
1386                    InvalidMode::Ignore => {}
1387                },
1388            }
1389        } else {
1390            out.extend_from_slice(line);
1391        }
1392    }
1393
1394    Ok(())
1395}
1396
1397/// Run the numfmt command with the given configuration and input.
1398pub fn run_numfmt<R: std::io::BufRead, W: Write>(
1399    input: R,
1400    mut output: W,
1401    config: &NumfmtConfig,
1402) -> Result<(), String> {
1403    // Pre-parse format spec once for all lines.
1404    let parsed_fmt = if let Some(ref fmt) = config.format {
1405        Some(parse_format_spec(fmt)?)
1406    } else {
1407        None
1408    };
1409
1410    // Pre-compute field membership as a bitset.
1411    let field_set = FieldSet::from_config(&config.field);
1412
1413    let terminator = if config.zero_terminated { b'\0' } else { b'\n' };
1414    let mut header_remaining = config.header;
1415    let mut buf = Vec::with_capacity(4096);
1416    let mut out_buf = Vec::with_capacity(4096);
1417    let mut reader = input;
1418    let mut had_error = false;
1419
1420    // Determine delimiter byte for fast path.
1421    let delim_byte = config.delimiter.map(|c| {
1422        // Only supports single-byte delimiters for fast path.
1423        if c.is_ascii() { Some(c as u8) } else { None }
1424    });
1425    // Flatten Option<Option<u8>> to Option<u8>
1426    let delim_byte = delim_byte.and_then(|x| x);
1427
1428    loop {
1429        buf.clear();
1430        let bytes_read = reader
1431            .read_until(terminator, &mut buf)
1432            .map_err(|e| format!("read error: {}", e))?;
1433        if bytes_read == 0 {
1434            break;
1435        }
1436
1437        // Remove the terminator for processing.
1438        let line = if buf.last() == Some(&terminator) {
1439            &buf[..buf.len() - 1]
1440        } else {
1441            &buf[..]
1442        };
1443
1444        if header_remaining > 0 {
1445            header_remaining -= 1;
1446            output
1447                .write_all(line)
1448                .map_err(|e| format!("write error: {}", e))?;
1449            output
1450                .write_all(&[terminator])
1451                .map_err(|e| format!("write error: {}", e))?;
1452            continue;
1453        }
1454
1455        out_buf.clear();
1456
1457        // Use fast path: process directly on byte slices.
1458        let result = if let Some(db) = delim_byte {
1459            process_line_fast_delim(
1460                line,
1461                db,
1462                &field_set,
1463                config,
1464                parsed_fmt.as_ref(),
1465                &mut out_buf,
1466            )
1467        } else if config.delimiter.is_some() {
1468            // Non-ASCII delimiter: fall back to String path.
1469            let line_str = String::from_utf8_lossy(line);
1470            match process_line_with_fmt(&line_str, config, parsed_fmt.as_ref()) {
1471                Ok(result) => {
1472                    out_buf.extend_from_slice(result.as_bytes());
1473                    Ok(())
1474                }
1475                Err(e) => Err(e),
1476            }
1477        } else {
1478            // Whitespace-delimited fast path.
1479            process_line_fast_ws(line, &field_set, config, parsed_fmt.as_ref(), &mut out_buf)
1480        };
1481
1482        match result {
1483            Ok(()) => {
1484                output
1485                    .write_all(&out_buf)
1486                    .map_err(|e| format!("write error: {}", e))?;
1487                output
1488                    .write_all(&[terminator])
1489                    .map_err(|e| format!("write error: {}", e))?;
1490            }
1491            Err(e) => match config.invalid {
1492                InvalidMode::Abort => {
1493                    eprintln!("numfmt: {}", e);
1494                    return Err(e);
1495                }
1496                InvalidMode::Fail => {
1497                    eprintln!("numfmt: {}", e);
1498                    output
1499                        .write_all(line)
1500                        .map_err(|e| format!("write error: {}", e))?;
1501                    output
1502                        .write_all(&[terminator])
1503                        .map_err(|e| format!("write error: {}", e))?;
1504                    had_error = true;
1505                }
1506                InvalidMode::Warn => {
1507                    eprintln!("numfmt: {}", e);
1508                    output
1509                        .write_all(line)
1510                        .map_err(|e| format!("write error: {}", e))?;
1511                    output
1512                        .write_all(&[terminator])
1513                        .map_err(|e| format!("write error: {}", e))?;
1514                }
1515                InvalidMode::Ignore => {
1516                    output
1517                        .write_all(line)
1518                        .map_err(|e| format!("write error: {}", e))?;
1519                    output
1520                        .write_all(&[terminator])
1521                        .map_err(|e| format!("write error: {}", e))?;
1522                }
1523            },
1524        }
1525    }
1526
1527    output.flush().map_err(|e| format!("flush error: {}", e))?;
1528
1529    if had_error {
1530        Err("conversion errors occurred".to_string())
1531    } else {
1532        Ok(())
1533    }
1534}