sort: gnu coreutils compatibility (sort float.sh) (#9839)

* feat(sort): support international decimal separators in numeric sorting

M
This commit is contained in:
mattsu
2026-01-14 06:11:55 +01:00
committed by GitHub
parent d806231866
commit 6b49ff9061
4 changed files with 93 additions and 8 deletions
+34
View File
@@ -502,6 +502,17 @@ version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "645cbb3a84e60b7531617d5ae4e57f7e27308f6445f5abf653209ea76dec8dff"
[[package]]
name = "fixed_decimal"
version = "0.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "35eabf480f94d69182677e37571d3be065822acfafd12f2f085db44fbbcc8e57"
dependencies = [
"displaydoc",
"smallvec",
"writeable",
]
[[package]]
name = "flate2"
version = "1.1.5"
@@ -676,6 +687,27 @@ dependencies = [
"zerovec",
]
[[package]]
name = "icu_decimal"
version = "2.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a38c52231bc348f9b982c1868a2af3195199623007ba2c7650f432038f5b3e8e"
dependencies = [
"fixed_decimal",
"icu_decimal_data",
"icu_locale",
"icu_locale_core",
"icu_provider",
"writeable",
"zerovec",
]
[[package]]
name = "icu_decimal_data"
version = "2.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2905b4044eab2dd848fe84199f9195567b63ab3a93094711501363f63546fef7"
[[package]]
name = "icu_locale"
version = "2.1.1"
@@ -1731,7 +1763,9 @@ dependencies = [
"glob",
"hex",
"icu_collator",
"icu_decimal",
"icu_locale",
"icu_provider",
"itertools",
"libc",
"md-5",
+6 -1
View File
@@ -34,7 +34,12 @@ self_cell = { workspace = true }
tempfile = { workspace = true }
thiserror = { workspace = true }
unicode-width = { workspace = true }
uucore = { workspace = true, features = ["fs", "parser-size", "version-cmp"] }
uucore = { workspace = true, features = [
"fs",
"parser-size",
"version-cmp",
"i18n-decimal",
] }
fluent = { workspace = true }
[target.'cfg(unix)'.dependencies]
+27 -7
View File
@@ -47,6 +47,7 @@ use uucore::error::{FromIo, strip_errno};
use uucore::error::{UError, UResult, USimpleError, UUsageError};
use uucore::extendedbigdecimal::ExtendedBigDecimal;
use uucore::format_usage;
use uucore::i18n::decimal::locale_decimal_separator;
use uucore::line_ending::LineEnding;
use uucore::parser::num_parser::{ExtendedParser, ExtendedParserError};
use uucore::parser::parse_size::{ParseSizeError, Parser};
@@ -106,6 +107,14 @@ mod options {
const DECIMAL_PT: u8 = b'.';
fn locale_decimal_pt() -> u8 {
match locale_decimal_separator().as_bytes().first().copied() {
Some(b'.') => b'.',
Some(b',') => b',',
_ => DECIMAL_PT,
}
}
const NEGATIVE: &u8 = &b'-';
const POSITIVE: &u8 = &b'+';
@@ -683,8 +692,8 @@ impl<'a> Line<'a> {
}
SortMode::GeneralNumeric => {
let initial_selection = &self.line[selection.clone()];
let leading = get_leading_gen(initial_selection);
let decimal_pt = locale_decimal_pt();
let leading = get_leading_gen(initial_selection, decimal_pt);
// Shorten selection to leading.
selection.start += leading.start;
@@ -1072,7 +1081,11 @@ impl FieldSelector {
Selection::WithNumInfo(range_str, info)
} else if self.settings.mode == SortMode::GeneralNumeric {
// Parse this number as BigDecimal, as this is the requirement for general numeric sorting.
Selection::AsBigDecimal(general_bd_parse(&range_str[get_leading_gen(range_str)]))
let decimal_pt = locale_decimal_pt();
Selection::AsBigDecimal(general_bd_parse(
&range_str[get_leading_gen(range_str, decimal_pt)],
decimal_pt,
))
} else {
// This is not a numeric sort, so we don't need a NumCache.
Selection::Str(range_str)
@@ -2491,7 +2504,7 @@ fn ascii_case_insensitive_cmp(a: &[u8], b: &[u8]) -> Ordering {
// scientific notation, so we strip those lines only after the end of the following numeric string.
// For example, 5e10KFD would be 5e10 or 5x10^10 and +10000HFKJFK would become 10000.
#[allow(clippy::cognitive_complexity)]
fn get_leading_gen(inp: &[u8]) -> Range<usize> {
fn get_leading_gen(inp: &[u8], decimal_pt: u8) -> Range<usize> {
let trimmed = inp.trim_ascii_start();
let leading_whitespace_len = inp.len() - trimmed.len();
@@ -2529,7 +2542,7 @@ fn get_leading_gen(inp: &[u8]) -> Range<usize> {
continue;
}
if c == DECIMAL_PT && !had_decimal_pt && !had_e_notation {
if c == decimal_pt && !had_decimal_pt && !had_e_notation {
had_decimal_pt = true;
continue;
}
@@ -2572,9 +2585,16 @@ pub enum GeneralBigDecimalParseResult {
/// Parse the beginning string into a [`GeneralBigDecimalParseResult`].
/// Using a [`GeneralBigDecimalParseResult`] instead of [`ExtendedBigDecimal`] is necessary to correctly order floats.
#[inline(always)]
fn general_bd_parse(a: &[u8]) -> GeneralBigDecimalParseResult {
fn general_bd_parse(a: &[u8], decimal_pt: u8) -> GeneralBigDecimalParseResult {
let parsed_bytes = (decimal_pt != DECIMAL_PT).then(|| {
a.iter()
.map(|&b| if b == decimal_pt { DECIMAL_PT } else { b })
.collect::<Vec<_>>()
});
let input = parsed_bytes.as_deref().unwrap_or(a);
// The string should be valid ASCII to be parsed.
let Ok(a) = std::str::from_utf8(a) else {
let Ok(a) = std::str::from_utf8(input) else {
return GeneralBigDecimalParseResult::Invalid;
};
+26
View File
@@ -1631,6 +1631,32 @@ fn test_g_float() {
.stdout_is(output);
}
#[test]
fn test_g_float_locale_decimal_separator() {
let Ok(locale_fr_utf8) = env::var("LOCALE_FR_UTF8") else {
return;
};
if locale_fr_utf8 == "none" {
return;
}
let ts = TestScenario::new("sort");
ts.ucmd()
.env("LC_ALL", &locale_fr_utf8)
.args(&["-g", "--stable"])
.pipe_in("1,9\n1,10\n")
.succeeds()
.stdout_is("1,10\n1,9\n");
ts.ucmd()
.env("LC_ALL", &locale_fr_utf8)
.args(&["-g", "--stable"])
.pipe_in("1.9\n1.10\n")
.succeeds()
.stdout_is("1.10\n1.9\n");
}
#[test]
// Test misc numbers ("'a" is not interpreted as literal, trailing text is ignored...)
fn test_g_misc() {