Skip to content

Commit e21a9bd

Browse files
sjhddhclaude
andcommitted
date: fix strftime flags and widths on %N (nanoseconds)
GNU date treats %N width as precision (number of fractional-second digits) and its zeros as significant content rather than padding. Previously uutils ignored flags/widths on %N, producing incorrect output (e.g. `%-N` at @0 gave "0" instead of "000000000"). Add special handling for the %N specifier in apply_modifiers: - %-N: preserve all digits (zeros are content, not padding) - %3N: truncate to 3 fractional digits - %_3N: truncate to 3 digits, replace trailing zeros with spaces - %_N: full 9 digits, trailing zeros replaced with spaces Un-ignore the existing integration test for issue #11658. Closes #11658 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent cfc6457 commit e21a9bd

2 files changed

Lines changed: 137 additions & 1 deletion

File tree

src/uu/date/src/format_modifiers.rs

Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -331,6 +331,68 @@ fn strip_default_padding(value: &str) -> String {
331331
value.to_string()
332332
}
333333

334+
/// Returns true if the specifier is `%N` (nanoseconds), which needs special
335+
/// treatment: width controls precision (number of fractional digits) rather
336+
/// than minimum field width, and the digit zeros are significant content,
337+
/// not padding.
338+
fn is_nanosecond_specifier(specifier: &str) -> bool {
339+
specifier.chars().last() == Some('N')
340+
}
341+
342+
/// Apply modifiers specifically for the `%N` (nanoseconds) specifier.
343+
///
344+
/// Unlike other numeric specifiers, `%N` treats width as precision
345+
/// (number of fractional-second digits) and its zeros are significant
346+
/// content, not padding.
347+
///
348+
/// GNU behaviour:
349+
/// - `%N` → all 9 digits (e.g. "000000000")
350+
/// - `%-N` → all 9 digits unchanged (zeros are content, not padding)
351+
/// - `%3N` → first 3 digits, zero-padded on the right (e.g. "000")
352+
/// - `%_3N` → first 3 digits, trailing zeros replaced with spaces (e.g. "0 ")
353+
/// - `%_N` → all 9 digits, trailing zeros replaced with spaces
354+
fn apply_nanosecond_modifiers(
355+
value: &str,
356+
no_pad: bool,
357+
underscore_flag: bool,
358+
pad_char: char,
359+
width: usize,
360+
explicit_width: bool,
361+
) -> Result<String, FormatError> {
362+
let default_width = 9;
363+
let precision = if explicit_width { width } else { default_width };
364+
365+
// Truncate or extend to the requested precision
366+
let mut result: String = if precision <= value.len() {
367+
value[..precision].to_string()
368+
} else {
369+
// Extend with trailing zeros to requested precision
370+
let mut s = value.to_string();
371+
s.extend(std::iter::repeat_n('0', precision - value.len()));
372+
s
373+
};
374+
375+
if no_pad {
376+
// `-` flag on %N: the zeros in nanoseconds are significant content,
377+
// not padding, so return the digits unchanged.
378+
} else if underscore_flag || pad_char == ' ' {
379+
// `_` flag: replace trailing zeros with spaces
380+
let trimmed = result.trim_end_matches('0');
381+
let content_len = if trimmed.is_empty() { 1 } else { trimmed.len() };
382+
let trailing_spaces = precision - content_len;
383+
if trimmed.is_empty() {
384+
result = "0".to_string();
385+
} else {
386+
result = trimmed.to_string();
387+
}
388+
result.extend(std::iter::repeat_n(' ', trailing_spaces));
389+
}
390+
// Otherwise (default '0' padding or no flags): result already has the
391+
// right number of zero-padded digits from the truncation/extension above.
392+
393+
Ok(result)
394+
}
395+
334396
/// Apply width and flag modifiers to a formatted value.
335397
///
336398
/// The specifier inside `parsed` (e.g., "d", "B", "Y") determines the default
@@ -409,6 +471,13 @@ fn apply_modifiers(value: &str, parsed: &ParsedSpec<'_>) -> Result<String, Forma
409471
}
410472
}
411473

474+
// Special handling for %N (nanoseconds): width controls precision
475+
// (number of fractional digits), not minimum field width, and the
476+
// digit zeros are significant content rather than padding.
477+
if is_nanosecond_specifier(specifier) {
478+
return apply_nanosecond_modifiers(&result, no_pad, underscore_flag, pad_char, width, explicit_width);
479+
}
480+
412481
// If no_pad flag is active, suppress all padding and return
413482
if no_pad {
414483
return Ok(strip_default_padding(&result));
@@ -1027,4 +1096,72 @@ mod tests {
10271096
assert_eq!(has_gnu_modifiers(input), *expected, "input = {input:?}");
10281097
}
10291098
}
1099+
1100+
#[test]
1101+
fn test_nanosecond_width_and_flags() {
1102+
// %N: nanoseconds at epoch 0 → "000000000" (9 digits, all zeros)
1103+
use jiff::Timestamp;
1104+
1105+
let ts = Timestamp::from_second(0).unwrap();
1106+
let date = ts.to_zoned(TimeZone::UTC);
1107+
let config = get_config();
1108+
1109+
// %N without modifiers: full 9-digit nanoseconds
1110+
let result = format_with_modifiers(&date, "%N", &config).unwrap();
1111+
assert_eq!(result, "000000000");
1112+
1113+
// %-N: no-pad flag should NOT strip zeros (they are content)
1114+
let result = format_with_modifiers(&date, "%-N", &config).unwrap();
1115+
assert_eq!(result, "000000000", "GNU: %-N at @0 should be '000000000'");
1116+
1117+
// %_3N: space-pad, width 3 → truncate to 3 digits, trailing zeros → spaces
1118+
let result = format_with_modifiers(&date, "%_3N", &config).unwrap();
1119+
assert_eq!(result, "0 ", "GNU: %_3N at @0 should be '0 '");
1120+
1121+
// %3N: width 3 → truncate to 3 digits
1122+
let result = format_with_modifiers(&date, "%3N", &config).unwrap();
1123+
assert_eq!(result, "000", "GNU: %3N at @0 should be '000'");
1124+
1125+
// %_N: space-pad without width → 9 digits, trailing zeros → spaces
1126+
let result = format_with_modifiers(&date, "%_N", &config).unwrap();
1127+
assert_eq!(result, "0 ", "GNU: %_N at @0 should be '0' + 8 spaces");
1128+
}
1129+
1130+
#[test]
1131+
fn test_nanosecond_with_nonzero_nanos() {
1132+
use jiff::Timestamp;
1133+
1134+
// 1.123456789 seconds since epoch → nanoseconds = 123456789
1135+
let ts = Timestamp::new(1, 123_456_789).unwrap();
1136+
let date = ts.to_zoned(TimeZone::UTC);
1137+
let config = get_config();
1138+
1139+
// %N: full 9-digit nanoseconds
1140+
let result = format_with_modifiers(&date, "%N", &config).unwrap();
1141+
assert_eq!(result, "123456789");
1142+
1143+
// %3N: first 3 digits
1144+
let result = format_with_modifiers(&date, "%3N", &config).unwrap();
1145+
assert_eq!(result, "123");
1146+
1147+
// %-N: no-pad, all 9 digits shown
1148+
let result = format_with_modifiers(&date, "%-N", &config).unwrap();
1149+
assert_eq!(result, "123456789");
1150+
1151+
// %_3N: first 3 digits, trailing zeros → spaces (no trailing zeros here)
1152+
let result = format_with_modifiers(&date, "%_3N", &config).unwrap();
1153+
assert_eq!(result, "123");
1154+
1155+
// Test with trailing zeros: 1.120000000
1156+
let ts2 = Timestamp::new(1, 120_000_000).unwrap();
1157+
let date2 = ts2.to_zoned(TimeZone::UTC);
1158+
1159+
// %_N: trailing zeros become spaces
1160+
let result = format_with_modifiers(&date2, "%_N", &config).unwrap();
1161+
assert_eq!(result, "12 ");
1162+
1163+
// %_3N: first 3 digits "120", trailing zero becomes space
1164+
let result = format_with_modifiers(&date2, "%_3N", &config).unwrap();
1165+
assert_eq!(result, "12 ");
1166+
}
10301167
}

tests/by-util/test_date.rs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1903,7 +1903,6 @@ fn test_date_strftime_case_flag_on_alt_ampm() {
19031903
}
19041904

19051905
#[test]
1906-
#[ignore = "https://github.com/uutils/coreutils/issues/11658 — GNU date applies flags/widths to `%N` (nanoseconds); uutils ignores/mishandles them."]
19071906
fn test_date_strftime_n_width_and_flags() {
19081907
// `%_3N` should space-pad nanoseconds to width 3. GNU outputs `0 `; uutils outputs `0`.
19091908
new_ucmd!()

0 commit comments

Comments
 (0)