Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
94 changes: 63 additions & 31 deletions src/uu/numfmt/src/format.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,10 @@
// For the full copyright and license information, please view the LICENSE
// file that was distributed with this source code.

// spell-checker:ignore powf seps
// spell-checker:ignore powf seps replacen

use uucore::display::Quotable;
use uucore::i18n::decimal::locale_grouping_separator;
use uucore::i18n::decimal::{locale_decimal_separator, locale_grouping_separator};
use uucore::translate;

use crate::numeric::ParsedNumber;
Expand All @@ -16,30 +16,37 @@ use crate::units::{
};

fn find_numeric_beginning(s: &str) -> Option<&str> {
let mut decimal_point_seen = false;
let dec_sep = locale_decimal_separator();
let mut seen_dec = false;
if s.is_empty() {
return None;
}

if s.starts_with('.') {
return Some(".");
if s.starts_with(dec_sep) {
return Some(&s[..dec_sep.len()]);
}

for (idx, c) in s.char_indices() {
if c == '-' && idx == 0 {
let mut chars = s.char_indices().peekable();
while let Some((i, c)) = chars.next() {
if c == '-' && i == 0 {
continue;
}
if c.is_ascii_digit() {
continue;
}
if c == '.' && !decimal_point_seen {
decimal_point_seen = true;
if !seen_dec && s[i..].starts_with(dec_sep) {
seen_dec = true;
// skip past any remaining bytes of a multi-byte sep
for _ in 1..dec_sep.chars().count() {
chars.next();
}
continue;
}
if s[..idx].parse::<f64>().is_err() {
let num_str = s[..i].replace(dec_sep, ".");
if num_str.parse::<f64>().is_err() {
return None;
}
return Some(&s[..idx]);
return Some(&s[..i]);
}

Some(s)
Expand Down Expand Up @@ -149,15 +156,27 @@ fn detailed_error_message(s: &str, unit: Unit, unit_separator: &str) -> Option<S
}

fn parse_number_part(s: &str, input: &str) -> Result<ParsedNumber> {
if s.ends_with('.') {
let dec_sep = locale_decimal_separator();
if s.ends_with(dec_sep) {
return Err(translate!("numfmt-error-invalid-number", "input" => input.quote()));
}

if let Ok(n) = s.parse::<i128>() {
return Ok(ParsedNumber::ExactInt(n));
}

s.parse::<f64>()
if dec_sep != "." && s.contains('.') {
return Err(translate!("numfmt-error-invalid-number", "input" => input.quote()));
}

let normalized = if dec_sep == "." {
s.to_string()
} else {
s.replace(dec_sep, ".")
};

normalized
.parse::<f64>()
.map(ParsedNumber::Float)
.map_err(|_| translate!("numfmt-error-invalid-number", "input" => input.quote()))
}
Expand Down Expand Up @@ -234,7 +253,8 @@ fn apply_grouping(s: &str) -> String {
} else {
("", s)
};
let (integer, fraction) = rest.split_once('.').map_or((rest, ""), |(i, f)| (i, f));
let dec_sep = locale_decimal_separator();
let (integer, fraction) = rest.split_once(dec_sep).map_or((rest, ""), |(i, f)| (i, f));
if integer.len() < 4 {
return s.to_string();
}
Expand Down Expand Up @@ -263,7 +283,7 @@ fn apply_grouping(s: &str) -> String {
}

if !fraction.is_empty() {
grouped.push('.');
grouped.push_str(dec_sep);
grouped.push_str(fraction);
}

Expand Down Expand Up @@ -341,7 +361,8 @@ impl<'a> Iterator for WhitespaceSplitter<'a, '_> {
/// Returns the implicit precision of a number, which is the count of digits after the dot. For
/// example, 1.23 has an implicit precision of 2.
fn parse_implicit_precision(s: &str) -> usize {
match s.split_once('.') {
let dec_sep = locale_decimal_separator();
match s.split_once(dec_sep) {
Some((_, decimal_part)) => decimal_part
.chars()
.take_while(char::is_ascii_digit)
Expand Down Expand Up @@ -533,7 +554,11 @@ fn try_format_exact_int_without_suffix_scaling(
Some(if precision == 0 {
scaled.to_string()
} else {
format!("{scaled}.{}", "0".repeat(precision))
format!(
"{scaled}{}{}",
locale_decimal_separator(),
"0".repeat(precision)
)
})
}

Expand All @@ -552,25 +577,32 @@ fn transform_to(
let s = s.to_f64();
let i2 = s / (opts.to_unit as f64);
let (i2, s) = consider_suffix(i2, opts.to, round_method, precision)?;
Ok(match s {
None => {
format!(
"{:.precision$}",
round_with_precision(i2, round_method, precision),
)
}
Some(s) if precision > 0 => {
format!(
"{i2:.precision$}{unit_separator}{}",
DisplayableSuffix(s, opts.to),
)
let dec_sep = locale_decimal_separator();
let localize = |s: String| -> String {
if dec_sep == "." {
s
} else {
s.replacen('.', dec_sep, 1)
}
};
Ok(match s {
None => localize(format!(
"{:.precision$}",
round_with_precision(i2, round_method, precision),
)),
Some(s) if precision > 0 => localize(format!(
"{i2:.precision$}{unit_separator}{}",
DisplayableSuffix(s, opts.to),
)),
Some(s) if is_precision_specified => {
format!("{i2:.0}{unit_separator}{}", DisplayableSuffix(s, opts.to))
}
Some(s) if i2.abs() < 10.0 => {
// when there's a single digit before the dot.
format!("{i2:.1}{unit_separator}{}", DisplayableSuffix(s, opts.to))
// single digit before the decimal, like 1.5K
localize(format!(
"{i2:.1}{unit_separator}{}",
DisplayableSuffix(s, opts.to)
))
}
Some(s) => {
format!("{i2:.0}{unit_separator}{}", DisplayableSuffix(s, opts.to))
Expand Down
43 changes: 43 additions & 0 deletions tests/by-util/test_numfmt.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1470,3 +1470,46 @@ fn test_invalid_utf8_input() {
.stdout_is("10\n")
.stderr_is("numfmt: invalid number: '\\377'\n");
}

#[test]
#[cfg_attr(wasi_runner, ignore = "WASI: locale env vars not propagated")]
fn test_locale_fr_output() {
// Output uses the locale separator
new_ucmd!()
.env("LC_ALL", "fr_FR.UTF-8")
.args(&["--to=iec", "1500"])
.succeeds()
.stdout_is("1,5K\n");
}

#[test]
#[cfg_attr(wasi_runner, ignore = "WASI: locale env vars not propagated")]
fn test_locale_fr_input_comma() {
// fr_FR should take '1,5' as a number
new_ucmd!()
.env("LC_ALL", "fr_FR.UTF-8")
.args(&["--format=%.3f", "1,5"])
.succeeds()
.stdout_is("1,500\n");
}

#[test]
#[cfg_attr(wasi_runner, ignore = "WASI: locale env vars not propagated")]
fn test_locale_fr_rejects_period() {
// '.' isn't valid in fr_FR, should bail
new_ucmd!()
.env("LC_ALL", "fr_FR.UTF-8")
.args(&["--format=%.3f", "1.5"])
.fails()
.stderr_contains("invalid");
}

#[test]
fn test_locale_c_uses_period() {
// C locale should still use '.' as usual
new_ucmd!()
.env("LC_ALL", "C")
.args(&["--to=iec", "1500"])
.succeeds()
.stdout_is("1.5K\n");
}
Loading