@@ -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}
0 commit comments