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.parse().map_err(|_| {
301        // GNU distinguishes "invalid number" (no leading digits at all) from
302        // "invalid suffix in input" (valid numeric prefix but trailing junk).
303        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    // Apply suffix multiplier.
320    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                // Auto-detect: if suffix ends with 'i', use IEC; otherwise SI.
327                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/// Apply rounding according to the specified method.
382#[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
405/// Format a number with scale suffix for output.
406fn format_scaled(value: f64, unit: ScaleUnit, round: RoundMethod) -> String {
407    match unit {
408        ScaleUnit::None => {
409            // Output as plain number.
410            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            // For --to=auto, behave like SI.
417            format_with_scale(value, SI_SUFFIXES, "", round)
418        }
419    }
420}
421
422/// Write a scaled number directly to a byte buffer, avoiding String allocation.
423fn 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/// Write a plain number to a byte buffer using itoa for integers.
436#[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 enough precision to avoid loss.
444        use std::io::Write;
445        let _ = write!(buf, "{:.1}", value);
446    }
447}
448
449/// Write a scaled number with suffix directly to a byte buffer.
450fn 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    // Find the largest suffix that applies.
461    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        // Value is smaller than the smallest suffix, output as-is.
471        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            // Write N.N format manually
501            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
531/// Format a plain number, removing unnecessary trailing zeros and decimal point.
532fn 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        // Use enough precision to avoid loss.
539        format!("{:.1}", value)
540    }
541}
542
543/// Format a number with appropriate scale suffix.
544/// Matches GNU numfmt behavior:
545/// - If scaled value < 10: display with 1 decimal place ("N.Nk")
546/// - If scaled value >= 10: display as integer ("NNk")
547/// - If integer would be >= 1000: promote to next suffix
548fn 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    // Find the largest suffix that applies.
558    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        // Value is smaller than the smallest suffix, output as-is.
569        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            // Display with 1 decimal place: "N.Nk"
579            let rounded = apply_round_for_display(scaled, round);
580            if rounded.abs() >= 10.0 {
581                // Rounding pushed it past 10, switch to integer display.
582                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            // Display as integer: "NNk"
594            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                // No next suffix, just output what we have.
601            }
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/// Apply rounding for display purposes (when formatting scaled output).
610/// Rounds to 1 decimal place.
611#[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/// Apply rounding to get an integer value for display.
638#[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
661/// Insert thousands grouping separators.
662fn group_thousands(s: &str) -> String {
663    // Find the integer part (before any decimal point).
664    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    // Handle sign.
671    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
696/// Apply width/padding from a printf-style format string to an already-scaled string.
697/// Used when both --to and --format are specified.
698fn apply_format_padding(scaled: &str, fmt: &str) -> String {
699    let bytes = fmt.as_bytes();
700    let mut i = 0;
701
702    // Find '%'.
703    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; // skip '%'
711
712    // Parse flags.
713    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    // Parse width.
724    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    // Skip precision and conversion char.
733    while i < bytes.len() && (bytes[i] == b'.' || bytes[i].is_ascii_digit()) {
734        i += 1;
735    }
736    if i < bytes.len() {
737        i += 1; // skip conversion char
738    }
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
755/// Pre-parsed format specification for fast repeated formatting.
756struct 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
769/// Parse a printf-style format string once, for reuse across many values.
770fn 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
860/// Apply a pre-parsed format specification to a number value.
861fn 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
913/// Format in scientific notation.
914fn 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
950/// Format using %g - shortest representation.
951fn 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
988/// Convert a single numeric token according to the config.
989fn convert_number(
990    token: &str,
991    config: &NumfmtConfig,
992    parsed_fmt: Option<&ParsedFormat>,
993) -> Result<String, String> {
994    // Parse the input number (with optional suffix).
995    let raw_value = parse_number_with_suffix(token, config.from)?;
996
997    // Apply from-unit scaling.
998    let value = raw_value * config.from_unit;
999
1000    // Apply to-unit scaling.
1001    let value = value / config.to_unit;
1002
1003    // Format the output.
1004    let mut result = if let Some(pf) = parsed_fmt {
1005        // If --to is also specified, first scale, then apply format padding.
1006        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    // Apply grouping.
1021    if config.grouping {
1022        result = group_thousands(&result);
1023    }
1024
1025    // Apply suffix.
1026    if let Some(ref suffix) = config.suffix {
1027        result.push_str(suffix);
1028    }
1029
1030    // Apply padding.
1031    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                // Left-align (pad on right).
1037                result = format!("{}{}", result, " ".repeat(deficit));
1038            } else {
1039                // Right-align (pad on left).
1040                result = format!("{}{}", " ".repeat(deficit), result);
1041            }
1042        }
1043    }
1044
1045    Ok(result)
1046}
1047
1048/// Convert a numeric token and write result directly to a byte buffer.
1049/// Returns Ok(true) if conversion succeeded, Ok(false) if the original token
1050/// should be written instead (for non-abort error modes).
1051fn convert_number_to_buf(
1052    token: &str,
1053    config: &NumfmtConfig,
1054    parsed_fmt: Option<&ParsedFormat>,
1055    out: &mut Vec<u8>,
1056) -> Result<(), String> {
1057    // Parse the input number (with optional suffix).
1058    let raw_value = parse_number_with_suffix(token, config.from)?;
1059
1060    // Apply from-unit and to-unit scaling.
1061    let value = raw_value * config.from_unit / config.to_unit;
1062
1063    // Check if we can use the fast path: no format, no grouping, no padding, no suffix.
1064    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    // Slow path: use String-based convert_number for complex formatting.
1081    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
1122/// Split a line into fields based on the delimiter.
1123fn 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            // Whitespace splitting: split on runs of whitespace, but preserve
1128            // leading whitespace as empty fields.
1129            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
1167/// Reassemble fields into a line with proper spacing.
1168fn 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            // For whitespace-delimited input, reconstruct preserving original spacing.
1178            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                        // We just finished a field.
1189                    }
1190                    result.push(c);
1191                    in_space = true;
1192                    i += 1;
1193                } else {
1194                    if in_space {
1195                        in_space = false;
1196                        // Output the converted field instead of the original.
1197                        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                        // Skip past the original field characters.
1204                        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
1218/// Process a single line according to the numfmt configuration.
1219pub fn process_line(line: &str, config: &NumfmtConfig) -> Result<String, String> {
1220    process_line_with_fmt(line, config, None)
1221}
1222
1223/// Process a single line with a pre-parsed format specification.
1224fn 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; // 1-based
1240        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
1273/// Fast path: process a delimiter-separated line by writing directly to output buffer.
1274/// Scans for delimiter byte positions, writes non-target fields as raw bytes,
1275/// converts only target fields. No intermediate String allocations.
1276fn 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        // Find next delimiter or end of line.
1290        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            // This field needs conversion.
1296            // Safety: we treat the bytes as str. For ASCII numeric fields this is fine.
1297            // If non-UTF8, the parse will fail gracefully.
1298            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            // Write field bytes directly without conversion.
1316            out.extend_from_slice(&line[start..end]);
1317        }
1318
1319        if end >= len {
1320            break;
1321        }
1322
1323        // Write delimiter.
1324        out.push(delim);
1325        start = end + 1;
1326        field_num += 1;
1327    }
1328
1329    Ok(())
1330}
1331
1332/// Fast path: process a whitespace-separated line by writing directly to output buffer.
1333/// Preserves original whitespace. Converts only target fields.
1334fn 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    // State machine: alternate between whitespace and field content.
1346    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            // Whitespace: write directly.
1350            out.push(c);
1351            i += 1;
1352        } else {
1353            // Start of a field.
1354            field_num += 1;
1355            let field_start = i;
1356
1357            // Find end of field.
1358            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    // Handle completely blank / empty lines.
1391    // GNU numfmt treats an empty line as an invalid number at field 1.
1392    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
1412/// Run the numfmt command with the given configuration and input.
1413pub fn run_numfmt<R: std::io::BufRead, W: Write>(
1414    input: R,
1415    mut output: W,
1416    config: &NumfmtConfig,
1417) -> Result<(), String> {
1418    // Pre-parse format spec once for all lines.
1419    let parsed_fmt = if let Some(ref fmt) = config.format {
1420        Some(parse_format_spec(fmt)?)
1421    } else {
1422        None
1423    };
1424
1425    // Pre-compute field membership as a bitset.
1426    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    // Determine delimiter byte for fast path.
1436    let delim_byte = config.delimiter.map(|c| {
1437        // Only supports single-byte delimiters for fast path.
1438        if c.is_ascii() { Some(c as u8) } else { None }
1439    });
1440    // Flatten Option<Option<u8>> to Option<u8>
1441    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        // Remove the terminator for processing.
1453        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        // Use fast path: process directly on byte slices.
1473        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            // Non-ASCII delimiter: fall back to String path.
1484            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            // Whitespace-delimited fast path.
1494            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}