Skip to content

Commit a77848f

Browse files
authored
printf: Fix extra padding (#6548)
1 parent f175a0a commit a77848f

File tree

3 files changed

+200
-30
lines changed

3 files changed

+200
-30
lines changed

src/uucore/src/lib/features/format/num_format.rs

Lines changed: 71 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
//! Utilities for formatting numbers in various formats
77
8+
use std::cmp::min;
89
use std::io::Write;
910

1011
use super::{
@@ -77,22 +78,16 @@ pub struct SignedInt {
7778
impl Formatter for SignedInt {
7879
type Input = i64;
7980

80-
fn fmt(&self, mut writer: impl Write, x: Self::Input) -> std::io::Result<()> {
81-
if x >= 0 {
82-
match self.positive_sign {
83-
PositiveSign::None => Ok(()),
84-
PositiveSign::Plus => write!(writer, "+"),
85-
PositiveSign::Space => write!(writer, " "),
86-
}?;
87-
}
81+
fn fmt(&self, writer: impl Write, x: Self::Input) -> std::io::Result<()> {
82+
let s = if self.precision > 0 {
83+
format!("{:0>width$}", x.abs(), width = self.precision)
84+
} else {
85+
x.abs().to_string()
86+
};
8887

89-
let s = format!("{:0width$}", x, width = self.precision);
88+
let sign_indicator = get_sign_indicator(self.positive_sign, &x);
9089

91-
match self.alignment {
92-
NumberAlignment::Left => write!(writer, "{s:<width$}", width = self.width),
93-
NumberAlignment::RightSpace => write!(writer, "{s:>width$}", width = self.width),
94-
NumberAlignment::RightZero => write!(writer, "{s:0>width$}", width = self.width),
95-
}
90+
write_output(writer, sign_indicator, s, self.width, self.alignment)
9691
}
9792

9893
fn try_from_spec(s: Spec) -> Result<Self, FormatError> {
@@ -244,16 +239,8 @@ impl Default for Float {
244239
impl Formatter for Float {
245240
type Input = f64;
246241

247-
fn fmt(&self, mut writer: impl Write, x: Self::Input) -> std::io::Result<()> {
248-
if x.is_sign_positive() {
249-
match self.positive_sign {
250-
PositiveSign::None => Ok(()),
251-
PositiveSign::Plus => write!(writer, "+"),
252-
PositiveSign::Space => write!(writer, " "),
253-
}?;
254-
}
255-
256-
let s = if x.is_finite() {
242+
fn fmt(&self, writer: impl Write, x: Self::Input) -> std::io::Result<()> {
243+
let mut s = if x.is_finite() {
257244
match self.variant {
258245
FloatVariant::Decimal => {
259246
format_float_decimal(x, self.precision, self.force_decimal)
@@ -272,11 +259,13 @@ impl Formatter for Float {
272259
format_float_non_finite(x, self.case)
273260
};
274261

275-
match self.alignment {
276-
NumberAlignment::Left => write!(writer, "{s:<width$}", width = self.width),
277-
NumberAlignment::RightSpace => write!(writer, "{s:>width$}", width = self.width),
278-
NumberAlignment::RightZero => write!(writer, "{s:0>width$}", width = self.width),
279-
}
262+
// The format function will parse `x` together with its sign char,
263+
// which should be placed in `sign_indicator`. So drop it here
264+
s = if x < 0. { s[1..].to_string() } else { s };
265+
266+
let sign_indicator = get_sign_indicator(self.positive_sign, &x);
267+
268+
write_output(writer, sign_indicator, s, self.width, self.alignment)
280269
}
281270

282271
fn try_from_spec(s: Spec) -> Result<Self, FormatError>
@@ -326,6 +315,18 @@ impl Formatter for Float {
326315
}
327316
}
328317

318+
fn get_sign_indicator<T: PartialOrd + Default>(sign: PositiveSign, x: &T) -> String {
319+
if *x >= T::default() {
320+
match sign {
321+
PositiveSign::None => String::new(),
322+
PositiveSign::Plus => String::from("+"),
323+
PositiveSign::Space => String::from(" "),
324+
}
325+
} else {
326+
String::from("-")
327+
}
328+
}
329+
329330
fn format_float_non_finite(f: f64, case: Case) -> String {
330331
debug_assert!(!f.is_finite());
331332
let mut s = format!("{f}");
@@ -501,6 +502,47 @@ fn strip_fractional_zeroes_and_dot(s: &mut String) {
501502
}
502503
}
503504

505+
fn write_output(
506+
mut writer: impl Write,
507+
sign_indicator: String,
508+
mut s: String,
509+
width: usize,
510+
alignment: NumberAlignment,
511+
) -> std::io::Result<()> {
512+
// Take length of `sign_indicator`, which could be 0 or 1, into consideration when padding
513+
// by storing remaining_width indicating the actual width needed.
514+
// Using min() because self.width could be 0, 0usize - 1usize should be avoided
515+
let remaining_width = width - min(width, sign_indicator.len());
516+
match alignment {
517+
NumberAlignment::Left => write!(
518+
writer,
519+
"{sign_indicator}{s:<width$}",
520+
width = remaining_width
521+
),
522+
NumberAlignment::RightSpace => {
523+
let is_sign = sign_indicator.starts_with('-') || sign_indicator.starts_with('+'); // When sign_indicator is in ['-', '+']
524+
if is_sign && remaining_width > 0 {
525+
// Make sure sign_indicator is just next to number, e.g. "% +5.1f" 1 ==> $ +1.0
526+
s = sign_indicator + s.as_str();
527+
write!(writer, "{s:>width$}", width = remaining_width + 1) // Since we now add sign_indicator and s together, plus 1
528+
} else {
529+
write!(
530+
writer,
531+
"{sign_indicator}{s:>width$}",
532+
width = remaining_width
533+
)
534+
}
535+
}
536+
NumberAlignment::RightZero => {
537+
write!(
538+
writer,
539+
"{sign_indicator}{s:0>width$}",
540+
width = remaining_width
541+
)
542+
}
543+
}
544+
}
545+
504546
#[cfg(test)]
505547
mod test {
506548
use crate::format::num_format::{Case, ForceDecimal};

src/uucore/src/lib/features/format/spec.rs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -252,7 +252,11 @@ impl Spec {
252252
} else {
253253
Case::Lowercase
254254
},
255-
alignment,
255+
alignment: if flags.zero && !flags.minus {
256+
NumberAlignment::RightZero // float should always try to zero pad despite the precision
257+
} else {
258+
alignment
259+
},
256260
positive_sign,
257261
},
258262
_ => return Err(&start[..index]),

tests/by-util/test_printf.rs

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -792,3 +792,127 @@ fn float_invalid_precision_fails() {
792792
.fails()
793793
.stderr_is("printf: invalid precision: '2147483648'\n");
794794
}
795+
796+
// The following padding-tests test for the cases in which flags in ['0', ' '] are given.
797+
// For integer, only try to pad when no precision is given, while
798+
// for float, always try to pad
799+
#[test]
800+
fn space_padding_with_space_test() {
801+
// Check if printf gives an extra space in the beginning
802+
new_ucmd!()
803+
.args(&["% 3d", "1"])
804+
.succeeds()
805+
.stdout_only(" 1");
806+
}
807+
808+
#[test]
809+
fn zero_padding_with_space_test() {
810+
new_ucmd!()
811+
.args(&["% 03d", "1"])
812+
.succeeds()
813+
.stdout_only(" 01");
814+
}
815+
816+
#[test]
817+
fn zero_padding_with_plus_test() {
818+
new_ucmd!()
819+
.args(&["%+04d", "1"])
820+
.succeeds()
821+
.stdout_only("+001");
822+
}
823+
824+
#[test]
825+
fn negative_zero_padding_test() {
826+
new_ucmd!()
827+
.args(&["%03d", "-1"])
828+
.succeeds()
829+
.stdout_only("-01");
830+
}
831+
832+
#[test]
833+
fn negative_zero_padding_with_space_test() {
834+
new_ucmd!()
835+
.args(&["% 03d", "-1"])
836+
.succeeds()
837+
.stdout_only("-01");
838+
}
839+
840+
#[test]
841+
fn float_with_zero_precision_should_pad() {
842+
new_ucmd!()
843+
.args(&["%03.0f", "-1"])
844+
.succeeds()
845+
.stdout_only("-01");
846+
}
847+
848+
#[test]
849+
fn precision_check() {
850+
new_ucmd!()
851+
.args(&["%.3d", "1"])
852+
.succeeds()
853+
.stdout_only("001");
854+
}
855+
856+
#[test]
857+
fn space_padding_with_precision() {
858+
new_ucmd!()
859+
.args(&["%4.3d", "1"])
860+
.succeeds()
861+
.stdout_only(" 001");
862+
}
863+
864+
#[test]
865+
fn float_zero_padding_with_precision() {
866+
new_ucmd!()
867+
.args(&["%04.1f", "1"])
868+
.succeeds()
869+
.stdout_only("01.0");
870+
}
871+
872+
#[test]
873+
fn float_space_padding_with_precision() {
874+
new_ucmd!()
875+
.args(&["%4.1f", "1"])
876+
.succeeds()
877+
.stdout_only(" 1.0");
878+
}
879+
880+
#[test]
881+
fn negative_float_zero_padding_with_precision() {
882+
new_ucmd!()
883+
.args(&["%05.1f", "-1"])
884+
.succeeds()
885+
.stdout_only("-01.0");
886+
}
887+
888+
#[test]
889+
fn float_default_precision_space_padding() {
890+
new_ucmd!()
891+
.args(&["%10f", "1"])
892+
.succeeds()
893+
.stdout_only(" 1.000000");
894+
}
895+
896+
#[test]
897+
fn float_default_precision_zero_padding() {
898+
new_ucmd!()
899+
.args(&["%010f", "1"])
900+
.succeeds()
901+
.stdout_only("001.000000");
902+
}
903+
904+
#[test]
905+
fn flag_position_space_padding() {
906+
new_ucmd!()
907+
.args(&["% +3.1d", "1"])
908+
.succeeds()
909+
.stdout_only(" +1");
910+
}
911+
912+
#[test]
913+
fn float_flag_position_space_padding() {
914+
new_ucmd!()
915+
.args(&["% +5.1f", "1"])
916+
.succeeds()
917+
.stdout_only(" +1.0");
918+
}

0 commit comments

Comments
 (0)