Skip to content

Commit

Permalink
fix: dont round off floats while formatting
Browse files Browse the repository at this point in the history
  • Loading branch information
dracarys18 committed Apr 20, 2024
1 parent 38beb4a commit 4fd623a
Show file tree
Hide file tree
Showing 3 changed files with 91 additions and 63 deletions.
111 changes: 55 additions & 56 deletions tests/formatting.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ use std::io;
use time::format_description::well_known::iso8601::{DateKind, OffsetPrecision, TimePrecision};
use time::format_description::well_known::{iso8601, Iso8601, Rfc2822, Rfc3339};
use time::format_description::{self, BorrowedFormatItem, OwnedFormatItem};
use time::formatting::FloatNum;
use time::macros::{date, datetime, format_description as fd, offset, time};
use time::{OffsetDateTime, Time};

Expand Down Expand Up @@ -109,12 +110,10 @@ fn iso_8601() -> time::Result<()> {
};
}

assert!(
std::panic::catch_unwind(|| {
let _unused = datetime!(2021-01-02 03:04:05 UTC).format(&Iso8601::PARSING);
})
.is_err()
);
assert!(std::panic::catch_unwind(|| {
let _unused = datetime!(2021-01-02 03:04:05 UTC).format(&Iso8601::PARSING);
})
.is_err());
assert_eq!(
datetime!(-123_456-01-02 03:04:05 UTC).format(
&Iso8601::<
Expand Down Expand Up @@ -266,20 +265,16 @@ fn format_time() -> time::Result<()> {
time!(13:02:03.456_789_012).format(format_description)?,
output
);
assert!(
time!(13:02:03.456_789_012)
.format_into(&mut io::sink(), format_description)
.is_ok()
);
assert!(time!(13:02:03.456_789_012)
.format_into(&mut io::sink(), format_description)
.is_ok());
assert_eq!(
time!(13:02:03.456_789_012).format(&OwnedFormatItem::from(format_description))?,
output
);
assert!(
time!(13:02:03.456_789_012)
.format_into(&mut io::sink(), &OwnedFormatItem::from(format_description))
.is_ok()
);
assert!(time!(13:02:03.456_789_012)
.format_into(&mut io::sink(), &OwnedFormatItem::from(format_description))
.is_ok());
}

assert_eq!(
Expand Down Expand Up @@ -376,20 +371,16 @@ fn format_date() -> time::Result<()> {

for &(format_description, output) in &format_output {
assert_eq!(date!(2019 - 12 - 31).format(format_description)?, output);
assert!(
date!(2019 - 12 - 31)
.format_into(&mut io::sink(), format_description)
.is_ok()
);
assert!(date!(2019 - 12 - 31)
.format_into(&mut io::sink(), format_description)
.is_ok());
assert_eq!(
date!(2019 - 12 - 31).format(&OwnedFormatItem::from(format_description))?,
output
);
assert!(
date!(2019 - 12 - 31)
.format_into(&mut io::sink(), &OwnedFormatItem::from(format_description))
.is_ok()
);
assert!(date!(2019 - 12 - 31)
.format_into(&mut io::sink(), &OwnedFormatItem::from(format_description))
.is_ok());
}

Ok(())
Expand Down Expand Up @@ -437,20 +428,16 @@ fn format_offset() -> time::Result<()> {

for &(value, format_description, output) in &value_format_output {
assert_eq!(value.format(format_description)?, output);
assert!(
value
.format_into(&mut io::sink(), format_description)
.is_ok()
);
assert!(value
.format_into(&mut io::sink(), format_description)
.is_ok());
assert_eq!(
value.format(&OwnedFormatItem::from(format_description))?,
output
);
assert!(
value
.format_into(&mut io::sink(), &OwnedFormatItem::from(format_description))
.is_ok()
);
assert!(value
.format_into(&mut io::sink(), &OwnedFormatItem::from(format_description))
.is_ok());
}

Ok(())
Expand Down Expand Up @@ -480,20 +467,16 @@ fn format_pdt() -> time::Result<()> {
datetime!(1970-01-01 0:00).format(format_description)?,
"1970-01-01 00:00:00.0"
);
assert!(
datetime!(1970-01-01 0:00)
.format_into(&mut io::sink(), format_description)
.is_ok()
);
assert!(datetime!(1970-01-01 0:00)
.format_into(&mut io::sink(), format_description)
.is_ok());
assert_eq!(
datetime!(1970-01-01 0:00).format(&OwnedFormatItem::from(format_description))?,
"1970-01-01 00:00:00.0"
);
assert!(
datetime!(1970-01-01 0:00)
.format_into(&mut io::sink(), &OwnedFormatItem::from(format_description))
.is_ok()
);
assert!(datetime!(1970-01-01 0:00)
.format_into(&mut io::sink(), &OwnedFormatItem::from(format_description))
.is_ok());

Ok(())
}
Expand Down Expand Up @@ -521,20 +504,16 @@ fn format_odt() -> time::Result<()> {
datetime!(1970-01-01 0:00 UTC).format(&format_description)?,
"1970-01-01 00:00:00.0 +00:00:00"
);
assert!(
datetime!(1970-01-01 0:00 UTC)
.format_into(&mut io::sink(), &format_description)
.is_ok()
);
assert!(datetime!(1970-01-01 0:00 UTC)
.format_into(&mut io::sink(), &format_description)
.is_ok());
assert_eq!(
datetime!(1970-01-01 0:00 UTC).format(&OwnedFormatItem::from(&format_description))?,
"1970-01-01 00:00:00.0 +00:00:00"
);
assert!(
datetime!(1970-01-01 0:00 UTC)
.format_into(&mut io::sink(), &OwnedFormatItem::from(format_description))
.is_ok()
);
assert!(datetime!(1970-01-01 0:00 UTC)
.format_into(&mut io::sink(), &OwnedFormatItem::from(format_description))
.is_ok());

Ok(())
}
Expand Down Expand Up @@ -785,3 +764,23 @@ fn unix_timestamp() -> time::Result<()> {

Ok(())
}

#[test]
fn test_float_formatting() -> time::Result<()> {
let num: FloatNum = 2.99999999.into();
let width = 3;

// Less than digits after decimal places
let precision = 3;
assert_eq!(format!("{num:0>width$.precision$}"), "2.999");

// More than digits after decimal places
let precision = 10;
assert_eq!(format!("{num:0>width$.precision$}"), "2.9999999900");

// Equal digits after decimal places
let precision = 8;
assert_eq!(format!("{num:0>width$.precision$}"), "2.99999999");

Ok(())
}
6 changes: 3 additions & 3 deletions time/src/formatting/iso8601.rs
Original file line number Diff line number Diff line change
Expand Up @@ -90,23 +90,23 @@ pub(super) fn format_time<const CONFIG: EncodedConfig>(
+ (minutes as f64) / Minute::per(Hour) as f64
+ (seconds as f64) / Second::per(Hour) as f64
+ (nanoseconds as f64) / Nanosecond::per(Hour) as f64;
format_float(output, hours, 2, decimal_digits)?;
format_float(output, hours.into(), 2, decimal_digits)?;
}
TimePrecision::Minute { decimal_digits } => {
bytes += format_number_pad_zero::<2>(output, hours)?;
bytes += write_if(output, Iso8601::<CONFIG>::USE_SEPARATORS, b":")?;
let minutes = (minutes as f64)
+ (seconds as f64) / Second::per(Minute) as f64
+ (nanoseconds as f64) / Nanosecond::per(Minute) as f64;
bytes += format_float(output, minutes, 2, decimal_digits)?;
bytes += format_float(output, minutes.into(), 2, decimal_digits)?;
}
TimePrecision::Second { decimal_digits } => {
bytes += format_number_pad_zero::<2>(output, hours)?;
bytes += write_if(output, Iso8601::<CONFIG>::USE_SEPARATORS, b":")?;
bytes += format_number_pad_zero::<2>(output, minutes)?;
bytes += write_if(output, Iso8601::<CONFIG>::USE_SEPARATORS, b":")?;
let seconds = (seconds as f64) + (nanoseconds as f64) / Nanosecond::per(Second) as f64;
bytes += format_float(output, seconds, 2, decimal_digits)?;
bytes += format_float(output, seconds.into(), 2, decimal_digits)?;
}
}

Expand Down
37 changes: 33 additions & 4 deletions time/src/formatting/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

pub(crate) mod formattable;
mod iso8601;
use core::num::NonZeroU8;
use core::{fmt, num::NonZeroU8};
use std::io;

use num_conv::prelude::*;
Expand Down Expand Up @@ -40,6 +40,31 @@ const WEEKDAY_NAMES: [&[u8]; 7] = [
b"Sunday",
];

/// Wrapper struct for f64 with custom Display formatter
#[derive(Debug, Clone, Copy)]
pub struct FloatNum(f64);

impl From<f64> for FloatNum {
fn from(num: f64) -> Self {
Self(num)
}
}

impl core::fmt::Display for FloatNum {
fn fmt(&self, fmt: &mut fmt::Formatter) -> Result<(), fmt::Error> {
if let Some(precision) = fmt.precision() {
let precision: i32 = precision.try_into().map_err(|_| fmt::Error)?;

// Truncate the decimal points up to the precision
let trunc_num = 10.0_f64.powi(precision);
let num_to_format = f64::trunc(self.0 * trunc_num) / trunc_num;

return num_to_format.fmt(fmt);
}
self.0.fmt(fmt)

Check warning on line 64 in time/src/formatting/mod.rs

View check run for this annotation

Codecov / codecov/patch

time/src/formatting/mod.rs#L63-L64

Added lines #L63 - L64 were not covered by tests
}
}

/// Write all bytes to the output, returning the number of bytes written.
pub(crate) fn write(output: &mut impl io::Write, bytes: &[u8]) -> io::Result<usize> {
output.write_all(bytes)?;
Expand All @@ -48,7 +73,11 @@ pub(crate) fn write(output: &mut impl io::Write, bytes: &[u8]) -> io::Result<usi

/// If `pred` is true, write all bytes to the output, returning the number of bytes written.
pub(crate) fn write_if(output: &mut impl io::Write, pred: bool, bytes: &[u8]) -> io::Result<usize> {
if pred { write(output, bytes) } else { Ok(0) }
if pred {
write(output, bytes)
} else {
Ok(0)
}
}

/// If `pred` is true, write `true_bytes` to the output. Otherwise, write `false_bytes`.
Expand All @@ -67,7 +96,7 @@ pub(crate) fn write_if_else(
/// with zeroes to the left if necessary.
pub(crate) fn format_float(
output: &mut impl io::Write,
value: f64,
value: FloatNum,
digits_before_decimal: u8,
digits_after_decimal: Option<NonZeroU8>,
) -> io::Result<usize> {
Expand All @@ -79,7 +108,7 @@ pub(crate) fn format_float(
Ok(width)
}
None => {
let value = value.trunc() as u64;
let value = value.0.trunc() as u64;
let width = digits_before_decimal.extend();
write!(output, "{value:0>width$}")?;
Ok(width)
Expand Down

0 comments on commit 4fd623a

Please sign in to comment.