Skip to content

Commit 5108d49

Browse files
authored
numfmt: align error messages for suffixes (#9887)
1 parent 238b27e commit 5108d49

5 files changed

Lines changed: 244 additions & 27 deletions

File tree

src/uu/numfmt/locales/en-US.ftl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ numfmt-error-invalid-header = invalid header value { $value }
5959
numfmt-error-grouping-cannot-be-combined-with-to = grouping cannot be combined with --to
6060
numfmt-error-delimiter-must-be-single-character = the delimiter must be a single character
6161
numfmt-error-invalid-number-empty = invalid number: ''
62+
numfmt-error-invalid-specific-suffix = invalid suffix in input { $input }: { $suffix }
6263
numfmt-error-invalid-suffix = invalid suffix in input: { $input }
6364
numfmt-error-invalid-number = invalid number: { $input }
6465
numfmt-error-missing-i-suffix = missing 'i' suffix in input: '{ $number }{ $suffix }' (e.g Ki/Mi/Gi)

src/uu/numfmt/locales/fr-FR.ftl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ numfmt-error-grouping-cannot-be-combined-with-to = le groupement ne peut pas êt
5959
numfmt-error-delimiter-must-be-single-character = le délimiteur doit être un seul caractère
6060
numfmt-error-invalid-number-empty = nombre invalide : ''
6161
numfmt-error-invalid-suffix = suffixe invalide dans l'entrée : { $input }
62+
numfmt-error-invalid-specific-suffix = suffixe invalide dans l'entrée { $input } : { $suffix }
6263
numfmt-error-invalid-number = nombre invalide : { $input }
6364
numfmt-error-missing-i-suffix = suffixe 'i' manquant dans l'entrée : '{ $number }{ $suffix }' (par ex. Ki/Mi/Gi)
6465
numfmt-error-rejecting-suffix = rejet du suffixe dans l'entrée : '{ $number }{ $suffix }' (considérez utiliser --from)

src/uu/numfmt/src/format.rs

Lines changed: 184 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -62,12 +62,97 @@ impl<'a> Iterator for WhitespaceSplitter<'a> {
6262
}
6363
}
6464

65-
fn parse_suffix(s: &str) -> Result<(f64, Option<Suffix>)> {
65+
fn find_numeric_beginning(s: &str) -> Option<&str> {
66+
let mut decimal_point_seen = false;
67+
if s.is_empty() {
68+
return None;
69+
}
70+
71+
for (idx, c) in s.char_indices() {
72+
if c == '-' && idx == 0 {
73+
continue;
74+
}
75+
if c.is_ascii_digit() {
76+
continue;
77+
}
78+
if c == '.' && !decimal_point_seen {
79+
decimal_point_seen = true;
80+
continue;
81+
}
82+
if s[..idx].parse::<f64>().is_err() {
83+
return None;
84+
}
85+
return Some(&s[..idx]);
86+
}
87+
88+
Some(s)
89+
}
90+
91+
// finds the valid beginning part of an input string, or None.
92+
fn find_valid_number_with_suffix<'a>(s: &'a str, unit: &Unit) -> Option<&'a str> {
93+
let numeric_part = find_numeric_beginning(s)?;
94+
95+
let accepts_suffix = unit != &Unit::None;
96+
let accepts_i = [Unit::Auto, Unit::Iec(true)].contains(unit);
97+
98+
let mut characters = s.chars().skip(numeric_part.len());
99+
let potential_suffix = characters.next();
100+
let potential_i = characters.next();
101+
102+
if !accepts_suffix {
103+
return Some(numeric_part);
104+
}
105+
106+
match (potential_suffix, potential_i) {
107+
(Some(suffix), None) if RawSuffix::try_from(&suffix).is_ok() => {
108+
Some(&s[..=numeric_part.len()])
109+
}
110+
(Some(suffix), Some('i')) if accepts_i && RawSuffix::try_from(&suffix).is_ok() => {
111+
Some(&s[..numeric_part.len() + 2])
112+
}
113+
(Some(suffix), Some(_)) if RawSuffix::try_from(&suffix).is_ok() => {
114+
Some(&s[..=numeric_part.len()])
115+
}
116+
_ => Some(numeric_part),
117+
}
118+
}
119+
120+
fn detailed_error_message(s: &str, unit: &Unit) -> Option<String> {
121+
if s.is_empty() {
122+
return Some(translate!("numfmt-error-invalid-number-empty"));
123+
}
124+
125+
let valid_part = find_valid_number_with_suffix(s, unit)
126+
.ok_or(translate!("numfmt-error-invalid-number", "input" => s.quote()))
127+
.ok()?;
128+
129+
if valid_part != s && valid_part.parse::<f64>().is_ok() {
130+
return match s.chars().nth(valid_part.len()) {
131+
Some(v) if RawSuffix::try_from(&v).is_ok() => Some(
132+
translate!("numfmt-error-rejecting-suffix", "number" => valid_part, "suffix" => s[valid_part.len()..]),
133+
),
134+
135+
_ => Some(translate!("numfmt-error-invalid-suffix", "input" => s.quote())),
136+
};
137+
}
138+
139+
if valid_part != s && valid_part.parse::<f64>().is_err() {
140+
return Some(
141+
translate!("numfmt-error-invalid-specific-suffix", "input" => s.quote(), "suffix" => s[valid_part.len()..].quote()),
142+
);
143+
}
144+
None
145+
}
146+
147+
fn parse_suffix(s: &str, unit: &Unit) -> Result<(f64, Option<Suffix>)> {
66148
if s.is_empty() {
67149
return Err(translate!("numfmt-error-invalid-number-empty"));
68150
}
69151

70152
let with_i = s.ends_with('i');
153+
if with_i && ![Unit::Auto, Unit::Iec(true)].contains(unit) {
154+
return Err(translate!("numfmt-error-invalid-suffix", "input" => s.quote()));
155+
}
71156
let mut iter = s.chars();
72157
if with_i {
73158
iter.next_back();
@@ -86,17 +171,7 @@ fn parse_suffix(s: &str) -> Result<(f64, Option<Suffix>)> {
86171
Some('Q') => Some((RawSuffix::Q, with_i)),
87172
Some('0'..='9') if !with_i => None,
88173
_ => {
89-
// If with_i is true, the string ends with 'i' but there's no valid suffix letter
90-
// This is always an invalid suffix (e.g., "1i", "2Ai")
91-
if with_i {
92-
return Err(translate!("numfmt-error-invalid-suffix", "input" => s.quote()));
93-
}
94-
// For other cases, check if the number part (without the last character) is valid
95-
let number_part = &s[..s.len() - 1];
96-
if number_part.is_empty() || number_part.parse::<f64>().is_err() {
97-
return Err(translate!("numfmt-error-invalid-number", "input" => s.quote()));
98-
}
99-
return Err(translate!("numfmt-error-invalid-suffix", "input" => s.quote()));
174+
return Err(translate!("numfmt-error-invalid-number", "input" => s.quote()));
100175
}
101176
};
102177

@@ -164,7 +239,8 @@ fn remove_suffix(i: f64, s: Option<Suffix>, u: &Unit) -> Result<f64> {
164239
}
165240

166241
fn transform_from(s: &str, opts: &TransformOptions) -> Result<f64> {
167-
let (i, suffix) = parse_suffix(s)?;
242+
let (i, suffix) = parse_suffix(s, &opts.from)
243+
.map_err(|original| detailed_error_message(s, &opts.from).unwrap_or(original))?;
168244
let i = i * (opts.from_unit as f64);
169245

170246
remove_suffix(i, suffix, &opts.from).map(|n| {
@@ -491,7 +567,7 @@ mod tests {
491567

492568
#[test]
493569
fn test_parse_suffix_q_r_k() {
494-
let result = parse_suffix("1Q");
570+
let result = parse_suffix("1Q", &Unit::Auto);
495571
assert!(result.is_ok());
496572
let (number, suffix) = result.unwrap();
497573
assert_eq!(number, 1.0);
@@ -500,7 +576,7 @@ mod tests {
500576
assert_eq!(raw_suffix as i32, RawSuffix::Q as i32);
501577
assert!(!with_i);
502578

503-
let result = parse_suffix("2R");
579+
let result = parse_suffix("2R", &Unit::Auto);
504580
assert!(result.is_ok());
505581
let (number, suffix) = result.unwrap();
506582
assert_eq!(number, 2.0);
@@ -509,7 +585,7 @@ mod tests {
509585
assert_eq!(raw_suffix as i32, RawSuffix::R as i32);
510586
assert!(!with_i);
511587

512-
let result = parse_suffix("3k");
588+
let result = parse_suffix("3k", &Unit::Auto);
513589
assert!(result.is_ok());
514590
let (number, suffix) = result.unwrap();
515591
assert_eq!(number, 3.0);
@@ -518,7 +594,7 @@ mod tests {
518594
assert_eq!(raw_suffix as i32, RawSuffix::K as i32);
519595
assert!(!with_i);
520596

521-
let result = parse_suffix("4Qi");
597+
let result = parse_suffix("4Qi", &Unit::Auto);
522598
assert!(result.is_ok());
523599
let (number, suffix) = result.unwrap();
524600
assert_eq!(number, 4.0);
@@ -527,7 +603,7 @@ mod tests {
527603
assert_eq!(raw_suffix as i32, RawSuffix::Q as i32);
528604
assert!(with_i);
529605

530-
let result = parse_suffix("5Ri");
606+
let result = parse_suffix("5Ri", &Unit::Auto);
531607
assert!(result.is_ok());
532608
let (number, suffix) = result.unwrap();
533609
assert_eq!(number, 5.0);
@@ -539,22 +615,41 @@ mod tests {
539615

540616
#[test]
541617
fn test_parse_suffix_error_messages() {
542-
let result = parse_suffix("foo");
618+
let result = parse_suffix("foo", &Unit::Auto);
543619
assert!(result.is_err());
544620
let error = result.unwrap_err();
545621
assert!(error.contains("numfmt-error-invalid-number") || error.contains("invalid number"));
546622
assert!(!error.contains("invalid suffix"));
547623

548-
let result = parse_suffix("World");
624+
let result = parse_suffix("World", &Unit::Auto);
549625
assert!(result.is_err());
550626
let error = result.unwrap_err();
551627
assert!(error.contains("numfmt-error-invalid-number") || error.contains("invalid number"));
552628
assert!(!error.contains("invalid suffix"));
629+
}
553630

554-
let result = parse_suffix("123i");
555-
assert!(result.is_err());
556-
let error = result.unwrap_err();
631+
#[test]
632+
fn test_detailed_error_message() {
633+
let result = detailed_error_message("123i", &Unit::Auto);
634+
assert!(result.is_some());
635+
let error = result.unwrap();
557636
assert!(error.contains("numfmt-error-invalid-suffix") || error.contains("invalid suffix"));
637+
638+
let result = detailed_error_message("5MF", &Unit::Auto);
639+
assert!(result.is_some());
640+
let error = result.unwrap();
641+
assert!(
642+
error.contains("numfmt-error-invalid-specific-suffix")
643+
|| error.contains("invalid suffix")
644+
);
645+
646+
let result = detailed_error_message("5KM", &Unit::Auto);
647+
assert!(result.is_some());
648+
let error = result.unwrap();
649+
assert!(
650+
error.contains("numfmt-error-invalid-specific-suffix")
651+
|| error.contains("invalid suffix")
652+
);
558653
}
559654

560655
#[test]
@@ -578,6 +673,72 @@ mod tests {
578673
assert_eq!(result.unwrap(), IEC_BASES[9]);
579674
}
580675

676+
#[test]
677+
fn test_find_valid_part() {
678+
assert_eq!(
679+
find_valid_number_with_suffix("12345KL", &Unit::Auto),
680+
Some("12345K")
681+
);
682+
assert_eq!(
683+
find_valid_number_with_suffix("12345K", &Unit::Auto),
684+
Some("12345K")
685+
);
686+
assert_eq!(
687+
find_valid_number_with_suffix("12345", &Unit::Auto),
688+
Some("12345")
689+
);
690+
assert_eq!(
691+
find_valid_number_with_suffix("asd12345KL", &Unit::Auto),
692+
None
693+
);
694+
assert_eq!(
695+
find_valid_number_with_suffix("8asdf", &Unit::Auto),
696+
Some("8")
697+
);
698+
assert_eq!(find_valid_number_with_suffix("5i", &Unit::Si), Some("5"));
699+
assert_eq!(
700+
find_valid_number_with_suffix("5i", &Unit::Iec(true)),
701+
Some("5")
702+
);
703+
assert_eq!(
704+
find_valid_number_with_suffix("0.1KL", &Unit::Auto),
705+
Some("0.1K")
706+
);
707+
assert_eq!(
708+
find_valid_number_with_suffix("0.1", &Unit::Auto),
709+
Some("0.1")
710+
);
711+
assert_eq!(
712+
find_valid_number_with_suffix("-0.1MT", &Unit::Auto),
713+
Some("-0.1M")
714+
);
715+
assert_eq!(
716+
find_valid_number_with_suffix("-0.1PT", &Unit::Auto),
717+
Some("-0.1P")
718+
);
719+
assert_eq!(
720+
find_valid_number_with_suffix("-0.1PT", &Unit::Auto),
721+
Some("-0.1P")
722+
);
723+
assert_eq!(
724+
find_valid_number_with_suffix("123.4.5", &Unit::Auto),
725+
Some("123.4")
726+
);
727+
assert_eq!(
728+
find_valid_number_with_suffix("0.55KiJ", &Unit::Iec(true)),
729+
Some("0.55Ki")
730+
);
731+
assert_eq!(
732+
find_valid_number_with_suffix("0.55KiJ", &Unit::Iec(false)),
733+
Some("0.55K")
734+
);
735+
assert_eq!(
736+
find_valid_number_with_suffix("123KICK", &Unit::Auto),
737+
Some("123K")
738+
);
739+
assert_eq!(find_valid_number_with_suffix("", &Unit::Auto), None);
740+
}
741+
581742
#[test]
582743
fn test_consider_suffix_q_r() {
583744
use crate::options::RoundMethod;

src/uu/numfmt/src/units.rs

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,26 @@ pub enum RawSuffix {
4646
Q,
4747
}
4848

49+
impl TryFrom<&char> for RawSuffix {
50+
type Error = String;
51+
52+
fn try_from(value: &char) -> Result<Self> {
53+
match value {
54+
'K' | 'k' => Ok(Self::K),
55+
'M' => Ok(Self::M),
56+
'G' => Ok(Self::G),
57+
'T' => Ok(Self::T),
58+
'P' => Ok(Self::P),
59+
'E' => Ok(Self::E),
60+
'Z' => Ok(Self::Z),
61+
'Y' => Ok(Self::Y),
62+
'R' => Ok(Self::R),
63+
'Q' => Ok(Self::Q),
64+
_ => Err(format!("Invalid suffix: {value}")),
65+
}
66+
}
67+
}
68+
4969
pub type Suffix = (RawSuffix, WithI);
5070

5171
pub struct DisplayableSuffix(pub Suffix, pub Unit);

0 commit comments

Comments
 (0)