Skip to main content

qubit_clock/meter/
format.rs

1/*******************************************************************************
2 *
3 *    Copyright (c) 2025 - 2026 Haixing Hu.
4 *
5 *    SPDX-License-Identifier: Apache-2.0
6 *
7 *    Licensed under the Apache License, Version 2.0.
8 *
9 ******************************************************************************/
10//! Duration and speed formatting utilities.
11//!
12//! This module provides functions to format durations and speeds into
13//! human-readable strings.
14//!
15
16/// Formats a duration in milliseconds into a human-readable string.
17///
18/// The format adapts based on the duration:
19/// - Less than 1 second: "X ms"
20/// - Less than 1 minute: "X.Ys" (rounded to 1 decimal place)
21/// - Less than 1 hour: "Xm Ys" (rounded to the nearest second)
22/// - 1 hour or more: "Xh Ym Zs" (rounded to the nearest second)
23///
24/// # Arguments
25///
26/// * `millis` - The duration in milliseconds
27///
28/// # Returns
29///
30/// A human-readable string representation of the duration
31///
32/// # Examples
33///
34/// ```
35/// use qubit_clock::meter::format_duration_millis;
36///
37/// assert_eq!(format_duration_millis(500), "500 ms");
38/// assert_eq!(format_duration_millis(1500), "1.5s");
39/// assert_eq!(format_duration_millis(65000), "1m 5s");
40/// assert_eq!(format_duration_millis(3665000), "1h 1m 5s");
41/// ```
42pub fn format_duration_millis(millis: i64) -> String {
43    format_duration_millis_i128(i128::from(millis))
44}
45
46/// Formats a value stored in tenths using the specified unit suffix.
47fn format_tenths(value_tenths: i128, unit: &str) -> String {
48    let whole = value_tenths / 10;
49    let tenths = value_tenths % 10;
50    if tenths > 0 {
51        format!("{}.{}{}", whole, tenths, unit)
52    } else {
53        format!("{}{}", whole, unit)
54    }
55}
56
57/// Divides a non-negative value by a positive divisor using half-up rounding.
58fn div_round_half_up(value: i128, divisor: i128) -> i128 {
59    let quotient = value / divisor;
60    let remainder = value % divisor;
61    if remainder >= divisor - remainder {
62        quotient.saturating_add(1)
63    } else {
64        quotient
65    }
66}
67
68/// Formats a non-negative duration expressed as whole seconds.
69fn format_duration_seconds(total_seconds: i128) -> String {
70    let hours = total_seconds / 3600;
71    let minutes = (total_seconds % 3600) / 60;
72    let seconds = total_seconds % 60;
73
74    if hours > 0 {
75        if minutes > 0 && seconds > 0 {
76            format!("{}h {}m {}s", hours, minutes, seconds)
77        } else if minutes > 0 {
78            format!("{}h {}m", hours, minutes)
79        } else if seconds > 0 {
80            format!("{}h {}s", hours, seconds)
81        } else {
82            format!("{}h", hours)
83        }
84    } else if minutes > 0 {
85        if seconds > 0 {
86            format!("{}m {}s", minutes, seconds)
87        } else {
88            format!("{}m", minutes)
89        }
90    } else {
91        format!("{}s", seconds)
92    }
93}
94
95/// Formats a millisecond duration stored as `i128`.
96///
97/// This helper keeps [`format_duration_nanos`] from truncating very large
98/// nanosecond values before formatting them.
99fn format_duration_millis_i128(millis: i128) -> String {
100    if millis < 0 {
101        return "0 ms".to_string();
102    }
103
104    if millis < 1000 {
105        return format!("{} ms", millis);
106    }
107
108    if millis < 60_000 {
109        let tenths = div_round_half_up(millis, 100);
110        if tenths >= 600 {
111            return format_duration_seconds(tenths / 10);
112        }
113        return format_tenths(tenths, "s");
114    }
115
116    format_duration_seconds(div_round_half_up(millis, 1000))
117}
118
119/// Formats a duration in nanoseconds into a human-readable string.
120///
121/// The format adapts based on the duration:
122/// - Less than 1 microsecond: "X ns"
123/// - Less than 1 millisecond: "X.Y μs" (rounded to 1 decimal place)
124/// - Less than 1 second: "X.Y ms" (rounded to 1 decimal place)
125/// - Less than 1 minute: "X.Ys" (rounded to 1 decimal place)
126/// - 1 minute or more: "Xm Ys" or "Xh Ym Zs" (rounded to the nearest second)
127///
128/// # Arguments
129///
130/// * `nanos` - The duration in nanoseconds
131///
132/// # Returns
133///
134/// A human-readable string representation of the duration
135///
136/// # Examples
137///
138/// ```
139/// use qubit_clock::meter::format_duration_nanos;
140///
141/// assert_eq!(format_duration_nanos(500), "500 ns");
142/// assert_eq!(format_duration_nanos(1500), "1.5 μs");
143/// assert_eq!(format_duration_nanos(1500000), "1.5 ms");
144/// assert_eq!(format_duration_nanos(1500000000), "1.5s");
145/// ```
146pub fn format_duration_nanos(nanos: i128) -> String {
147    if nanos < 0 {
148        return "0 ns".to_string();
149    }
150
151    if nanos < 1000 {
152        return format!("{} ns", nanos);
153    }
154
155    if nanos < 1_000_000 {
156        let tenths = div_round_half_up(nanos, 100);
157        if tenths >= 10_000 {
158            return format_duration_nanos(tenths * 100);
159        }
160        format_tenths(tenths, " μs")
161    } else if nanos < 1_000_000_000 {
162        let tenths = div_round_half_up(nanos, 100_000);
163        if tenths >= 10_000 {
164            return format_duration_nanos(tenths * 100_000);
165        }
166        format_tenths(tenths, " ms")
167    } else if nanos < 60_000_000_000 {
168        let tenths = div_round_half_up(nanos, 100_000_000);
169        if tenths >= 600 {
170            return format_duration_seconds(tenths / 10);
171        }
172        format_tenths(tenths, "s")
173    } else {
174        format_duration_seconds(div_round_half_up(nanos, 1_000_000_000))
175    }
176}
177
178/// Formats a speed value with a unit suffix.
179///
180/// The speed is formatted with 2 decimal places. If the speed is NaN or
181/// infinite, returns "N/A".
182///
183/// # Arguments
184///
185/// * `speed` - The speed value
186/// * `unit` - The unit suffix (e.g., "/s", "/m")
187///
188/// # Returns
189///
190/// A formatted string like "123.45/s" or "N/A"
191///
192/// # Examples
193///
194/// ```
195/// use qubit_clock::meter::format_speed;
196///
197/// assert_eq!(format_speed(123.456, "/s"), "123.46/s");
198/// assert_eq!(format_speed(0.0, "/m"), "0.00/m");
199/// assert_eq!(format_speed(f64::NAN, "/s"), "N/A");
200/// ```
201pub fn format_speed(speed: f64, unit: &str) -> String {
202    if speed.is_nan() || speed.is_infinite() {
203        "N/A".to_string()
204    } else {
205        format!("{:.2}{}", speed, unit)
206    }
207}