mirror of
https://github.com/typst/typst
synced 2025-05-14 04:56:26 +08:00
Fix duration formatting precision (#5082)
This commit is contained in:
parent
9a71e7263d
commit
04df1264ef
@ -5,7 +5,7 @@ use std::time::{Duration, Instant};
|
||||
|
||||
use codespan_reporting::term;
|
||||
use codespan_reporting::term::termcolor::WriteColor;
|
||||
use typst::utils::format;
|
||||
use typst::utils::format_duration;
|
||||
use typst_kit::download::{DownloadState, Downloader, Progress};
|
||||
|
||||
use crate::terminal::{self, TermOut};
|
||||
@ -62,9 +62,7 @@ pub fn display_download_progress(
|
||||
|
||||
let total_downloaded = as_bytes_unit(state.total_downloaded);
|
||||
let speed_h = as_throughput_unit(speed);
|
||||
let elapsed = format::time_starting_with_seconds(
|
||||
&Instant::now().saturating_duration_since(state.start_time),
|
||||
);
|
||||
let elapsed = Instant::now().saturating_duration_since(state.start_time);
|
||||
|
||||
match state.content_len {
|
||||
Some(content_len) => {
|
||||
@ -72,20 +70,26 @@ pub fn display_download_progress(
|
||||
let remaining = content_len - state.total_downloaded;
|
||||
|
||||
let download_size = as_bytes_unit(content_len);
|
||||
let eta =
|
||||
format::time_starting_with_seconds(&Duration::from_secs(if speed == 0 {
|
||||
let eta = Duration::from_secs(if speed == 0 {
|
||||
0
|
||||
} else {
|
||||
(remaining / speed) as u64
|
||||
}));
|
||||
});
|
||||
|
||||
writeln!(
|
||||
out,
|
||||
"{total_downloaded} / {download_size} ({percent:3.0} %) {speed_h} in {elapsed} ETA: {eta}",
|
||||
"{total_downloaded} / {download_size} ({percent:3.0} %) \
|
||||
{speed_h} in {elapsed} ETA: {eta}",
|
||||
elapsed = format_duration(elapsed),
|
||||
eta = format_duration(eta),
|
||||
)?;
|
||||
}
|
||||
None => writeln!(
|
||||
out,
|
||||
"Total downloaded: {total_downloaded} Speed: {speed_h} Elapsed: {elapsed}",
|
||||
"Total downloaded: {total_downloaded} \
|
||||
Speed: {speed_h} \
|
||||
Elapsed: {elapsed}",
|
||||
elapsed = format_duration(elapsed),
|
||||
)?,
|
||||
};
|
||||
Ok(())
|
||||
|
@ -11,7 +11,7 @@ use ecow::eco_format;
|
||||
use notify::{Event, RecommendedWatcher, RecursiveMode, Watcher as _};
|
||||
use same_file::is_same_file;
|
||||
use typst::diag::{bail, StrResult};
|
||||
use typst::utils::format;
|
||||
use typst::utils::format_duration;
|
||||
|
||||
use crate::args::{CompileCommand, Input, Output};
|
||||
use crate::compile::compile_once;
|
||||
@ -296,18 +296,14 @@ impl Status {
|
||||
}
|
||||
|
||||
fn message(&self) -> String {
|
||||
match self {
|
||||
match *self {
|
||||
Self::Compiling => "compiling ...".into(),
|
||||
Self::Success(duration) => {
|
||||
format!(
|
||||
"compiled successfully in {}",
|
||||
format::time_starting_with_milliseconds(duration, 2)
|
||||
)
|
||||
format!("compiled successfully in {}", format_duration(duration))
|
||||
}
|
||||
Self::PartialSuccess(duration) => {
|
||||
format!("compiled with warnings in {}", format_duration(duration))
|
||||
}
|
||||
Self::PartialSuccess(duration) => format!(
|
||||
"compiled with warnings in {}",
|
||||
format::time_starting_with_milliseconds(duration, 2)
|
||||
),
|
||||
Self::Error => "compiled with errors".into(),
|
||||
}
|
||||
}
|
||||
|
@ -8,7 +8,7 @@ use typst::foundations::{repr, Capturer, CastInfo, Repr, Value};
|
||||
use typst::layout::Length;
|
||||
use typst::model::Document;
|
||||
use typst::syntax::{ast, LinkedNode, Side, Source, SyntaxKind};
|
||||
use typst::utils::{round_2, Numeric};
|
||||
use typst::utils::{round_with_precision, Numeric};
|
||||
use typst::World;
|
||||
|
||||
use crate::{analyze_expr, analyze_labels, plain_docs_sentence, summarize_font_family};
|
||||
@ -142,10 +142,10 @@ fn length_tooltip(length: Length) -> Option<Tooltip> {
|
||||
length.em.is_zero().then(|| {
|
||||
Tooltip::Code(eco_format!(
|
||||
"{}pt = {}mm = {}cm = {}in",
|
||||
round_2(length.abs.to_pt()),
|
||||
round_2(length.abs.to_mm()),
|
||||
round_2(length.abs.to_cm()),
|
||||
round_2(length.abs.to_inches())
|
||||
round_with_precision(length.abs.to_pt(), 2),
|
||||
round_with_precision(length.abs.to_mm(), 2),
|
||||
round_with_precision(length.abs.to_cm(), 2),
|
||||
round_with_precision(length.abs.to_inches(), 2),
|
||||
))
|
||||
})
|
||||
}
|
||||
|
94
crates/typst-utils/src/duration.rs
Normal file
94
crates/typst-utils/src/duration.rs
Normal file
@ -0,0 +1,94 @@
|
||||
use std::fmt::{self, Display, Formatter, Write};
|
||||
use std::time::Duration;
|
||||
|
||||
use super::round_with_precision;
|
||||
|
||||
/// Formats a duration with a precision suitable for human display.
|
||||
pub fn format_duration(duration: Duration) -> impl Display {
|
||||
DurationDisplay(duration)
|
||||
}
|
||||
|
||||
/// Displays a `Duration`.
|
||||
struct DurationDisplay(Duration);
|
||||
|
||||
impl Display for DurationDisplay {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
|
||||
let mut space = false;
|
||||
macro_rules! piece {
|
||||
($($tts:tt)*) => {
|
||||
if std::mem::replace(&mut space, true) {
|
||||
f.write_char(' ')?;
|
||||
}
|
||||
write!(f, $($tts)*)?;
|
||||
};
|
||||
}
|
||||
|
||||
let secs = self.0.as_secs();
|
||||
let (mins, secs) = (secs / 60, (secs % 60));
|
||||
let (hours, mins) = (mins / 60, (mins % 60));
|
||||
let (days, hours) = ((hours / 24), (hours % 24));
|
||||
|
||||
if days > 0 {
|
||||
piece!("{days} d");
|
||||
}
|
||||
|
||||
if hours > 0 {
|
||||
piece!("{hours} h");
|
||||
}
|
||||
|
||||
if mins > 0 {
|
||||
piece!("{mins} min");
|
||||
}
|
||||
|
||||
// No need to display anything more than minutes at this point.
|
||||
if days > 0 || hours > 0 {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let order = |exp| 1000u64.pow(exp);
|
||||
let nanos = secs * order(3) + self.0.subsec_nanos() as u64;
|
||||
let fract = |exp| round_with_precision(nanos as f64 / order(exp) as f64, 2);
|
||||
|
||||
if nanos == 0 || self.0 > Duration::from_secs(1) {
|
||||
// For durations > 5 min, we drop the fractional part.
|
||||
if self.0 > Duration::from_secs(300) {
|
||||
piece!("{secs} s");
|
||||
} else {
|
||||
piece!("{} s", fract(3));
|
||||
}
|
||||
} else if self.0 > Duration::from_millis(1) {
|
||||
piece!("{} ms", fract(2));
|
||||
} else if self.0 > Duration::from_micros(1) {
|
||||
piece!("{} µs", fract(1));
|
||||
} else {
|
||||
piece!("{} ns", fract(0));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[track_caller]
|
||||
fn test(duration: Duration, expected: &str) {
|
||||
assert_eq!(format_duration(duration).to_string(), expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_format_duration() {
|
||||
test(Duration::from_secs(1000000), "11 d 13 h 46 min");
|
||||
test(Duration::from_secs(3600 * 24), "1 d");
|
||||
test(Duration::from_secs(3600), "1 h");
|
||||
test(Duration::from_secs(3600 + 240), "1 h 4 min");
|
||||
test(Duration::from_secs_f64(364.77), "6 min 4 s");
|
||||
test(Duration::from_secs_f64(264.776), "4 min 24.78 s");
|
||||
test(Duration::from_secs(3), "3 s");
|
||||
test(Duration::from_secs_f64(2.8492), "2.85 s");
|
||||
test(Duration::from_micros(734), "734 µs");
|
||||
test(Duration::from_micros(294816), "294.82 ms");
|
||||
test(Duration::from_nanos(1), "1 ns");
|
||||
}
|
||||
}
|
@ -1,256 +0,0 @@
|
||||
//! Different formatting functions.
|
||||
|
||||
use std::time::Duration;
|
||||
|
||||
/// Returns value with `n` digits after floating point where `n` is `precision`.
|
||||
/// Standard rounding rules apply (if `n+1`th digit >= 5, round up).
|
||||
///
|
||||
/// If rounding the `value` will have no effect (e.g., it's infinite or NaN),
|
||||
/// returns `value` unchanged.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// # use typst_utils::format::round_with_precision;
|
||||
/// let rounded = round_with_precision(-0.56553, 2);
|
||||
/// assert_eq!(-0.57, rounded);
|
||||
/// ```
|
||||
pub fn round_with_precision(value: f64, precision: u8) -> f64 {
|
||||
// Don't attempt to round the float if that wouldn't have any effect.
|
||||
// This includes infinite or NaN values, as well as integer values
|
||||
// with a filled mantissa (which can't have a fractional part).
|
||||
// Rounding with a precision larger than the amount of digits that can be
|
||||
// effectively represented would also be a no-op. Given that, the check
|
||||
// below ensures we won't proceed if `|value| >= 2^53` or if
|
||||
// `precision >= 15`, which also ensures the multiplication by `offset`
|
||||
// won't return `inf`, since `2^53 * 10^15` (larger than any possible
|
||||
// `value * offset` multiplication) does not.
|
||||
if value.is_infinite()
|
||||
|| value.is_nan()
|
||||
|| value.abs() >= (1_i64 << f64::MANTISSA_DIGITS) as f64
|
||||
|| precision as u32 >= f64::DIGITS
|
||||
{
|
||||
return value;
|
||||
}
|
||||
let offset = 10_f64.powi(precision.into());
|
||||
assert!((value * offset).is_finite(), "{value} * {offset} is not finite!");
|
||||
(value * offset).round() / offset
|
||||
}
|
||||
|
||||
/// Returns `(days, hours, minutes, seconds, milliseconds, microseconds)`.
|
||||
fn get_duration_parts(duration: &Duration) -> (u64, u8, u8, u8, u16, u16) {
|
||||
// In practice we probably don't need nanoseconds.
|
||||
let micros = duration.as_micros();
|
||||
let (millis, micros) = (micros / 1000, (micros % 1000) as u16);
|
||||
let (sec, millis) = (millis / 1000, (millis % 1000) as u16);
|
||||
let (mins, sec) = (sec / 60, (sec % 60) as u8);
|
||||
let (hours, mins) = (mins / 60, (mins % 60) as u8);
|
||||
let (days, hours) = ((hours / 24) as u64, (hours % 24) as u8);
|
||||
(days, hours, mins, sec, millis, micros)
|
||||
}
|
||||
|
||||
/// Format string using `days`, `hours`, `minutes`, `seconds`.
|
||||
fn format_dhms(days: u64, hours: u8, minutes: u8, seconds: u8) -> String {
|
||||
match (days, hours, minutes, seconds) {
|
||||
(0, 0, 0, s) => format!("{s:2} s"),
|
||||
(0, 0, m, s) => format!("{m:2} m {s:2} s"),
|
||||
(0, h, m, s) => format!("{h:2} h {m:2} m {s:2} s"),
|
||||
(d, h, m, s) => format!("{d:3} d {h:2} h {m:2} m {s:2} s"),
|
||||
}
|
||||
}
|
||||
|
||||
/// Format string starting with number of seconds and going bigger from there.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// # use std::time::Duration;
|
||||
/// # use typst_utils::format::time_starting_with_seconds;
|
||||
/// let duration1 = time_starting_with_seconds(&Duration::from_secs(0));
|
||||
/// assert_eq!(" 0 s", &duration1);
|
||||
///
|
||||
/// let duration2 = time_starting_with_seconds(&Duration::from_secs(
|
||||
/// 24 * 60 * 60 * 100 + // days
|
||||
/// 60 * 60 * 10 + // hours
|
||||
/// 60 * 10 + // minutes
|
||||
/// 10 // seconds
|
||||
/// ));
|
||||
/// assert_eq!("100 d 10 h 10 m 10 s", &duration2);
|
||||
/// ```
|
||||
pub fn time_starting_with_seconds(duration: &Duration) -> String {
|
||||
let (days, hours, minutes, seconds, _, _) = get_duration_parts(duration);
|
||||
format_dhms(days, hours, minutes, seconds)
|
||||
}
|
||||
|
||||
/// Format string starting with number of milliseconds and going bigger
|
||||
/// from there. `precision` is how many digits of microseconds
|
||||
/// from floating point to the right will be preserved (with rounding).
|
||||
/// Keep in mind that this function will always remove all trailing zeros
|
||||
/// for microseconds.
|
||||
///
|
||||
/// Note: if duration is 1 second or longer, then output will be identical
|
||||
/// to [time_starting_with_seconds], which also means that precision,
|
||||
/// number of milliseconds and microseconds will not be used.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// # use std::time::Duration;
|
||||
/// # use typst_utils::format::time_starting_with_milliseconds;
|
||||
/// let duration1 = time_starting_with_milliseconds(&Duration::from_micros(
|
||||
/// 123 * 1000 + // milliseconds
|
||||
/// 456 // microseconds
|
||||
/// ), 2);
|
||||
/// assert_eq!("123.46 ms", &duration1);
|
||||
///
|
||||
/// let duration2 = time_starting_with_milliseconds(&Duration::from_micros(
|
||||
/// 123 * 1000 // milliseconds
|
||||
/// ), 2);
|
||||
/// assert_eq!("123 ms", &duration2);
|
||||
///
|
||||
/// let duration3 = time_starting_with_milliseconds(&Duration::from_secs(1), 2);
|
||||
/// assert_eq!(" 1 s", &duration3);
|
||||
/// ```
|
||||
pub fn time_starting_with_milliseconds(duration: &Duration, precision: u8) -> String {
|
||||
let (d, h, m, s, ms, mcs) = get_duration_parts(duration);
|
||||
match (d, h, m, s) {
|
||||
(0, 0, 0, 0) => {
|
||||
let ms_mcs = ms as f64 + mcs as f64 / 1000.0;
|
||||
format!("{} ms", round_with_precision(ms_mcs, precision))
|
||||
}
|
||||
(d, h, m, s) => format_dhms(d, h, m, s),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_round_with_precision_0() {
|
||||
let round = |value| round_with_precision(value, 0);
|
||||
assert_eq!(0.0, round(0.0));
|
||||
assert_eq!(-0.0, round(-0.0));
|
||||
assert_eq!(0.0, round(0.4));
|
||||
assert_eq!(-0.0, round(-0.4));
|
||||
assert_eq!(1.0, round(0.56453));
|
||||
assert_eq!(-1.0, round(-0.56453));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_round_with_precision_1() {
|
||||
let round = |value| round_with_precision(value, 1);
|
||||
assert_eq!(0.0, round(0.0));
|
||||
assert_eq!(-0.0, round(-0.0));
|
||||
assert_eq!(0.4, round(0.4));
|
||||
assert_eq!(-0.4, round(-0.4));
|
||||
assert_eq!(0.4, round(0.44));
|
||||
assert_eq!(-0.4, round(-0.44));
|
||||
assert_eq!(0.6, round(0.56453));
|
||||
assert_eq!(-0.6, round(-0.56453));
|
||||
assert_eq!(1.0, round(0.96453));
|
||||
assert_eq!(-1.0, round(-0.96453));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_round_with_precision_2() {
|
||||
let round = |value| round_with_precision(value, 2);
|
||||
assert_eq!(0.0, round(0.0));
|
||||
assert_eq!(-0.0, round(-0.0));
|
||||
assert_eq!(0.4, round(0.4));
|
||||
assert_eq!(-0.4, round(-0.4));
|
||||
assert_eq!(0.44, round(0.44));
|
||||
assert_eq!(-0.44, round(-0.44));
|
||||
assert_eq!(0.44, round(0.444));
|
||||
assert_eq!(-0.44, round(-0.444));
|
||||
assert_eq!(0.57, round(0.56553));
|
||||
assert_eq!(-0.57, round(-0.56553));
|
||||
assert_eq!(1.0, round(0.99553));
|
||||
assert_eq!(-1.0, round(-0.99553));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_round_with_precision_fuzzy() {
|
||||
let round = |value| round_with_precision(value, 0);
|
||||
assert_eq!(f64::INFINITY, round(f64::INFINITY));
|
||||
assert_eq!(f64::NEG_INFINITY, round(f64::NEG_INFINITY));
|
||||
assert!(round(f64::NAN).is_nan());
|
||||
|
||||
let max_int = (1_i64 << f64::MANTISSA_DIGITS) as f64;
|
||||
let f64_digits = f64::DIGITS as u8;
|
||||
|
||||
// max
|
||||
assert_eq!(max_int, round(max_int));
|
||||
assert_eq!(0.123456, round_with_precision(0.123456, f64_digits));
|
||||
assert_eq!(max_int, round_with_precision(max_int, f64_digits));
|
||||
|
||||
// max - 1
|
||||
assert_eq!(max_int - 1f64, round(max_int - 1f64));
|
||||
assert_eq!(0.123456, round_with_precision(0.123456, f64_digits - 1));
|
||||
assert_eq!(max_int - 1f64, round_with_precision(max_int - 1f64, f64_digits));
|
||||
assert_eq!(max_int, round_with_precision(max_int, f64_digits - 1));
|
||||
assert_eq!(max_int - 1f64, round_with_precision(max_int - 1f64, f64_digits - 1));
|
||||
}
|
||||
|
||||
fn duration_from_milli_micro(milliseconds: u16, microseconds: u16) -> Duration {
|
||||
let microseconds = microseconds as u64;
|
||||
let milliseconds = 1000 * milliseconds as u64;
|
||||
Duration::from_micros(milliseconds + microseconds)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_time_starting_with_seconds() {
|
||||
let f = |duration| time_starting_with_seconds(&duration);
|
||||
fn duration(days: u16, hours: u8, minutes: u8, seconds: u8) -> Duration {
|
||||
let seconds = seconds as u64;
|
||||
let minutes = 60 * minutes as u64;
|
||||
let hours = 60 * 60 * hours as u64;
|
||||
let days = 24 * 60 * 60 * days as u64;
|
||||
Duration::from_secs(days + hours + minutes + seconds)
|
||||
}
|
||||
assert_eq!(" 0 s", &f(duration(0, 0, 0, 0)));
|
||||
assert_eq!("59 s", &f(duration(0, 0, 0, 59)));
|
||||
assert_eq!(" 1 m 12 s", &f(duration(0, 0, 1, 12)));
|
||||
assert_eq!("59 m 0 s", &f(duration(0, 0, 59, 0)));
|
||||
assert_eq!(" 5 h 1 m 2 s", &f(duration(0, 5, 1, 2)));
|
||||
assert_eq!(" 1 d 0 h 0 m 0 s", &f(duration(1, 0, 0, 0)));
|
||||
assert_eq!(" 69 d 0 h 0 m 0 s", &f(duration(69, 0, 0, 0)));
|
||||
assert_eq!("100 d 10 h 10 m 10 s", &f(duration(100, 10, 10, 10)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_time_as_ms_with_precision_1() {
|
||||
let f = |duration| time_starting_with_milliseconds(&duration, 1);
|
||||
let duration = duration_from_milli_micro;
|
||||
assert_eq!("123.5 ms", &f(duration(123, 456)));
|
||||
assert_eq!("123.5 ms", &f(duration(123, 455)));
|
||||
assert_eq!("123.4 ms", &f(duration(123, 445)));
|
||||
assert_eq!("123.4 ms", &f(duration(123, 440)));
|
||||
assert_eq!("123.1 ms", &f(duration(123, 100)));
|
||||
assert_eq!("123 ms", &f(duration(123, 0)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_time_as_ms_with_precision_2() {
|
||||
let f = |duration| time_starting_with_milliseconds(&duration, 2);
|
||||
let duration = duration_from_milli_micro;
|
||||
assert_eq!("123.46 ms", &f(duration(123, 456)));
|
||||
assert_eq!("123.46 ms", &f(duration(123, 455)));
|
||||
assert_eq!("123.45 ms", &f(duration(123, 454)));
|
||||
assert_eq!("123.45 ms", &f(duration(123, 450)));
|
||||
assert_eq!("123.1 ms", &f(duration(123, 100)));
|
||||
assert_eq!("123 ms", &f(duration(123, 0)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_time_as_ms_with_precision_3() {
|
||||
let f = |duration| time_starting_with_milliseconds(&duration, 3);
|
||||
let duration = duration_from_milli_micro;
|
||||
assert_eq!("123.456 ms", &f(duration(123, 456)));
|
||||
assert_eq!("123.455 ms", &f(duration(123, 455)));
|
||||
assert_eq!("123.454 ms", &f(duration(123, 454)));
|
||||
assert_eq!("123.45 ms", &f(duration(123, 450)));
|
||||
assert_eq!("123.1 ms", &f(duration(123, 100)));
|
||||
assert_eq!("123 ms", &f(duration(123, 0)));
|
||||
}
|
||||
}
|
@ -1,20 +1,23 @@
|
||||
//! Utilities for Typst.
|
||||
|
||||
pub mod fat;
|
||||
pub mod format;
|
||||
|
||||
#[macro_use]
|
||||
mod macros;
|
||||
mod bitset;
|
||||
mod deferred;
|
||||
mod duration;
|
||||
mod hash;
|
||||
mod pico;
|
||||
mod round;
|
||||
mod scalar;
|
||||
|
||||
pub use self::bitset::{BitSet, SmallBitSet};
|
||||
pub use self::deferred::Deferred;
|
||||
pub use self::duration::format_duration;
|
||||
pub use self::hash::LazyHash;
|
||||
pub use self::pico::PicoStr;
|
||||
pub use self::round::round_with_precision;
|
||||
pub use self::scalar::Scalar;
|
||||
|
||||
use std::fmt::{Debug, Formatter};
|
||||
@ -298,8 +301,3 @@ pub trait Numeric:
|
||||
/// Whether `self` consists only of finite parts.
|
||||
fn is_finite(self) -> bool;
|
||||
}
|
||||
|
||||
/// Round a float to two decimal places.
|
||||
pub fn round_2(value: f64) -> f64 {
|
||||
(value * 100.0).round() / 100.0
|
||||
}
|
||||
|
105
crates/typst-utils/src/round.rs
Normal file
105
crates/typst-utils/src/round.rs
Normal file
@ -0,0 +1,105 @@
|
||||
/// Returns value with `n` digits after floating point where `n` is `precision`.
|
||||
/// Standard rounding rules apply (if `n+1`th digit >= 5, round up).
|
||||
///
|
||||
/// If rounding the `value` will have no effect (e.g., it's infinite or NaN),
|
||||
/// returns `value` unchanged.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// # use typst_utils::round_with_precision;
|
||||
/// let rounded = round_with_precision(-0.56553, 2);
|
||||
/// assert_eq!(-0.57, rounded);
|
||||
/// ```
|
||||
pub fn round_with_precision(value: f64, precision: u8) -> f64 {
|
||||
// Don't attempt to round the float if that wouldn't have any effect.
|
||||
// This includes infinite or NaN values, as well as integer values
|
||||
// with a filled mantissa (which can't have a fractional part).
|
||||
// Rounding with a precision larger than the amount of digits that can be
|
||||
// effectively represented would also be a no-op. Given that, the check
|
||||
// below ensures we won't proceed if `|value| >= 2^53` or if
|
||||
// `precision >= 15`, which also ensures the multiplication by `offset`
|
||||
// won't return `inf`, since `2^53 * 10^15` (larger than any possible
|
||||
// `value * offset` multiplication) does not.
|
||||
if value.is_infinite()
|
||||
|| value.is_nan()
|
||||
|| value.abs() >= (1_i64 << f64::MANTISSA_DIGITS) as f64
|
||||
|| precision as u32 >= f64::DIGITS
|
||||
{
|
||||
return value;
|
||||
}
|
||||
let offset = 10_f64.powi(precision.into());
|
||||
assert!((value * offset).is_finite(), "{value} * {offset} is not finite!");
|
||||
(value * offset).round() / offset
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_round_with_precision_0() {
|
||||
let round = |value| round_with_precision(value, 0);
|
||||
assert_eq!(0.0, round(0.0));
|
||||
assert_eq!(-0.0, round(-0.0));
|
||||
assert_eq!(0.0, round(0.4));
|
||||
assert_eq!(-0.0, round(-0.4));
|
||||
assert_eq!(1.0, round(0.56453));
|
||||
assert_eq!(-1.0, round(-0.56453));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_round_with_precision_1() {
|
||||
let round = |value| round_with_precision(value, 1);
|
||||
assert_eq!(0.0, round(0.0));
|
||||
assert_eq!(-0.0, round(-0.0));
|
||||
assert_eq!(0.4, round(0.4));
|
||||
assert_eq!(-0.4, round(-0.4));
|
||||
assert_eq!(0.4, round(0.44));
|
||||
assert_eq!(-0.4, round(-0.44));
|
||||
assert_eq!(0.6, round(0.56453));
|
||||
assert_eq!(-0.6, round(-0.56453));
|
||||
assert_eq!(1.0, round(0.96453));
|
||||
assert_eq!(-1.0, round(-0.96453));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_round_with_precision_2() {
|
||||
let round = |value| round_with_precision(value, 2);
|
||||
assert_eq!(0.0, round(0.0));
|
||||
assert_eq!(-0.0, round(-0.0));
|
||||
assert_eq!(0.4, round(0.4));
|
||||
assert_eq!(-0.4, round(-0.4));
|
||||
assert_eq!(0.44, round(0.44));
|
||||
assert_eq!(-0.44, round(-0.44));
|
||||
assert_eq!(0.44, round(0.444));
|
||||
assert_eq!(-0.44, round(-0.444));
|
||||
assert_eq!(0.57, round(0.56553));
|
||||
assert_eq!(-0.57, round(-0.56553));
|
||||
assert_eq!(1.0, round(0.99553));
|
||||
assert_eq!(-1.0, round(-0.99553));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_round_with_precision_fuzzy() {
|
||||
let round = |value| round_with_precision(value, 0);
|
||||
assert_eq!(f64::INFINITY, round(f64::INFINITY));
|
||||
assert_eq!(f64::NEG_INFINITY, round(f64::NEG_INFINITY));
|
||||
assert!(round(f64::NAN).is_nan());
|
||||
|
||||
let max_int = (1_i64 << f64::MANTISSA_DIGITS) as f64;
|
||||
let f64_digits = f64::DIGITS as u8;
|
||||
|
||||
// max
|
||||
assert_eq!(max_int, round(max_int));
|
||||
assert_eq!(0.123456, round_with_precision(0.123456, f64_digits));
|
||||
assert_eq!(max_int, round_with_precision(max_int, f64_digits));
|
||||
|
||||
// max - 1
|
||||
assert_eq!(max_int - 1f64, round(max_int - 1f64));
|
||||
assert_eq!(0.123456, round_with_precision(0.123456, f64_digits - 1));
|
||||
assert_eq!(max_int - 1f64, round_with_precision(max_int - 1f64, f64_digits));
|
||||
assert_eq!(max_int, round_with_precision(max_int, f64_digits - 1));
|
||||
assert_eq!(max_int - 1f64, round_with_precision(max_int - 1f64, f64_digits - 1));
|
||||
}
|
||||
}
|
@ -10,6 +10,7 @@ use crate::eval::ops;
|
||||
use crate::foundations::{cast, func, Decimal, IntoValue, Module, Scope, Value};
|
||||
use crate::layout::{Angle, Fr, Length, Ratio};
|
||||
use crate::syntax::{Span, Spanned};
|
||||
use crate::utils::round_with_precision;
|
||||
|
||||
/// A module with calculation definitions.
|
||||
pub fn module() -> Module {
|
||||
@ -743,10 +744,9 @@ pub fn round(
|
||||
) -> DecNum {
|
||||
match value {
|
||||
DecNum::Int(n) => DecNum::Int(n),
|
||||
DecNum::Float(n) => DecNum::Float(crate::utils::format::round_with_precision(
|
||||
n,
|
||||
digits.saturating_as::<u8>(),
|
||||
)),
|
||||
DecNum::Float(n) => {
|
||||
DecNum::Float(round_with_precision(n, digits.saturating_as::<u8>()))
|
||||
}
|
||||
DecNum::Decimal(n) => DecNum::Decimal(n.round(digits)),
|
||||
}
|
||||
}
|
||||
|
@ -3,7 +3,7 @@
|
||||
use ecow::{eco_format, EcoString};
|
||||
|
||||
use crate::foundations::{func, Str, Value};
|
||||
use crate::utils::format::round_with_precision;
|
||||
use crate::utils::round_with_precision;
|
||||
|
||||
/// The Unicode minus sign.
|
||||
pub const MINUS_SIGN: &str = "\u{2212}";
|
||||
|
Loading…
x
Reference in New Issue
Block a user