qubit_progress/model/progress_counter.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 ******************************************************************************/
10use serde::{
11 Deserialize,
12 Serialize,
13};
14
15/// Counters for one metric in a progress event.
16///
17/// A counter is identified by `metric_id`. The corresponding display name and
18/// metric dictionary entry live in [`ProgressSchema`](crate::ProgressSchema).
19/// Counter values are `u64` so they can represent domain quantities such as
20/// bytes, records, objects, or files independently of the current platform's
21/// pointer width.
22///
23/// # Examples
24///
25/// ```
26/// use qubit_progress::ProgressCounter;
27///
28/// let counter = ProgressCounter::new("bytes")
29/// .total(10_000)
30/// .completed(4_000)
31/// .active(1);
32///
33/// assert_eq!(counter.metric_id(), "bytes");
34/// assert_eq!(counter.total_count(), Some(10_000));
35/// assert_eq!(counter.progress_percent(), Some(40.0));
36/// ```
37#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
38pub struct ProgressCounter {
39 /// Identifier of the metric this counter reports.
40 metric_id: String,
41 /// Total work-unit count when known.
42 total_count: Option<u64>,
43 /// Completed work-unit count.
44 completed_count: u64,
45 /// Active work-unit count.
46 active_count: u64,
47 /// Successful work-unit count.
48 succeeded_count: u64,
49 /// Failed work-unit count.
50 failed_count: u64,
51}
52
53impl ProgressCounter {
54 /// Creates a counter for a metric with an unknown total count.
55 ///
56 /// # Parameters
57 ///
58 /// * `metric_id` - Metric identifier declared by the progress schema.
59 ///
60 /// # Returns
61 ///
62 /// A zeroed counter associated with `metric_id`.
63 #[inline]
64 pub fn new(metric_id: &str) -> Self {
65 Self {
66 metric_id: metric_id.to_owned(),
67 total_count: None,
68 completed_count: 0,
69 active_count: 0,
70 succeeded_count: 0,
71 failed_count: 0,
72 }
73 }
74
75 /// Returns a copy configured with the known or unknown total count.
76 ///
77 /// # Parameters
78 ///
79 /// * `total_count` - Total work-unit count, or `None` when unknown.
80 ///
81 /// # Returns
82 ///
83 /// This counter with `total_count` recorded.
84 #[inline]
85 #[must_use]
86 pub const fn with_total_count(mut self, total_count: Option<u64>) -> Self {
87 self.total_count = total_count;
88 self
89 }
90
91 /// Returns a copy configured with a known total count.
92 ///
93 /// # Parameters
94 ///
95 /// * `total_count` - Total work-unit count.
96 ///
97 /// # Returns
98 ///
99 /// This counter with a known total count.
100 #[inline]
101 #[must_use]
102 pub const fn total(self, total_count: u64) -> Self {
103 self.with_total_count(Some(total_count))
104 }
105
106 /// Returns a copy configured with an unknown total count.
107 ///
108 /// # Returns
109 ///
110 /// This counter with no total count.
111 #[inline]
112 #[must_use]
113 pub const fn unknown_total(self) -> Self {
114 self.with_total_count(None)
115 }
116
117 /// Returns a copy configured with the completed count.
118 ///
119 /// # Parameters
120 ///
121 /// * `completed_count` - Number of completed work units.
122 ///
123 /// # Returns
124 ///
125 /// This counter with `completed_count` recorded.
126 #[inline]
127 #[must_use]
128 pub const fn with_completed_count(mut self, completed_count: u64) -> Self {
129 self.completed_count = completed_count;
130 self
131 }
132
133 /// Returns a copy configured with the completed count.
134 ///
135 /// # Parameters
136 ///
137 /// * `completed_count` - Number of completed work units.
138 ///
139 /// # Returns
140 ///
141 /// This counter with `completed_count` recorded.
142 #[inline]
143 #[must_use]
144 pub const fn completed(self, completed_count: u64) -> Self {
145 self.with_completed_count(completed_count)
146 }
147
148 /// Returns a copy configured with the active count.
149 ///
150 /// # Parameters
151 ///
152 /// * `active_count` - Number of currently active work units.
153 ///
154 /// # Returns
155 ///
156 /// This counter with `active_count` recorded.
157 #[inline]
158 #[must_use]
159 pub const fn with_active_count(mut self, active_count: u64) -> Self {
160 self.active_count = active_count;
161 self
162 }
163
164 /// Returns a copy configured with the active count.
165 ///
166 /// # Parameters
167 ///
168 /// * `active_count` - Number of currently active work units.
169 ///
170 /// # Returns
171 ///
172 /// This counter with `active_count` recorded.
173 #[inline]
174 #[must_use]
175 pub const fn active(self, active_count: u64) -> Self {
176 self.with_active_count(active_count)
177 }
178
179 /// Returns a copy configured with the succeeded count.
180 ///
181 /// # Parameters
182 ///
183 /// * `succeeded_count` - Number of successful work units.
184 ///
185 /// # Returns
186 ///
187 /// This counter with `succeeded_count` recorded.
188 #[inline]
189 #[must_use]
190 pub const fn with_succeeded_count(mut self, succeeded_count: u64) -> Self {
191 self.succeeded_count = succeeded_count;
192 self
193 }
194
195 /// Returns a copy configured with the succeeded count.
196 ///
197 /// # Parameters
198 ///
199 /// * `succeeded_count` - Number of successful work units.
200 ///
201 /// # Returns
202 ///
203 /// This counter with `succeeded_count` recorded.
204 #[inline]
205 #[must_use]
206 pub const fn succeeded(self, succeeded_count: u64) -> Self {
207 self.with_succeeded_count(succeeded_count)
208 }
209
210 /// Returns a copy configured with the failed count.
211 ///
212 /// # Parameters
213 ///
214 /// * `failed_count` - Number of failed work units.
215 ///
216 /// # Returns
217 ///
218 /// This counter with `failed_count` recorded.
219 #[inline]
220 #[must_use]
221 pub const fn with_failed_count(mut self, failed_count: u64) -> Self {
222 self.failed_count = failed_count;
223 self
224 }
225
226 /// Returns a copy configured with the failed count.
227 ///
228 /// # Parameters
229 ///
230 /// * `failed_count` - Number of failed work units.
231 ///
232 /// # Returns
233 ///
234 /// This counter with `failed_count` recorded.
235 #[inline]
236 #[must_use]
237 pub const fn failed(self, failed_count: u64) -> Self {
238 self.with_failed_count(failed_count)
239 }
240
241 /// Returns the metric identifier.
242 ///
243 /// # Returns
244 ///
245 /// The id of the metric this counter reports.
246 #[inline]
247 pub fn metric_id(&self) -> &str {
248 self.metric_id.as_str()
249 }
250
251 /// Returns the total work-unit count when known.
252 ///
253 /// # Returns
254 ///
255 /// `Some(total)` for known-total progress, or `None` for open-ended progress.
256 #[inline]
257 pub const fn total_count(&self) -> Option<u64> {
258 self.total_count
259 }
260
261 /// Returns the completed work-unit count.
262 ///
263 /// # Returns
264 ///
265 /// The number of completed work units.
266 #[inline]
267 pub const fn completed_count(&self) -> u64 {
268 self.completed_count
269 }
270
271 /// Returns the active work-unit count.
272 ///
273 /// # Returns
274 ///
275 /// The number of currently active work units.
276 #[inline]
277 pub const fn active_count(&self) -> u64 {
278 self.active_count
279 }
280
281 /// Returns the successful work-unit count.
282 ///
283 /// # Returns
284 ///
285 /// The number of successful work units.
286 #[inline]
287 pub const fn succeeded_count(&self) -> u64 {
288 self.succeeded_count
289 }
290
291 /// Returns the failed work-unit count.
292 ///
293 /// # Returns
294 ///
295 /// The number of failed work units.
296 #[inline]
297 pub const fn failed_count(&self) -> u64 {
298 self.failed_count
299 }
300
301 /// Returns the remaining work-unit count when the total is known.
302 ///
303 /// # Returns
304 ///
305 /// `Some(total - completed - active)` using saturating arithmetic for
306 /// known-total progress, or `None` when the total is unknown.
307 #[inline]
308 pub const fn remaining_count(&self) -> Option<u64> {
309 match self.total_count {
310 Some(total_count) => Some(
311 total_count
312 .saturating_sub(self.completed_count)
313 .saturating_sub(self.active_count),
314 ),
315 None => None,
316 }
317 }
318
319 /// Returns completed progress as a fraction in `0.0..=1.0`.
320 ///
321 /// # Returns
322 ///
323 /// `Some(fraction)` for known-total progress, `Some(1.0)` when the known
324 /// total is zero, or `None` when the total is unknown.
325 #[inline]
326 pub fn progress_fraction(&self) -> Option<f64> {
327 self.total_count.map(|total_count| {
328 if total_count == 0 {
329 1.0
330 } else {
331 (self.completed_count as f64 / total_count as f64).clamp(0.0, 1.0)
332 }
333 })
334 }
335
336 /// Returns completed progress as a percentage in `0.0..=100.0`.
337 ///
338 /// # Returns
339 ///
340 /// `Some(percent)` for known-total progress, or `None` when the total is
341 /// unknown.
342 #[inline]
343 pub fn progress_percent(&self) -> Option<f64> {
344 self.progress_fraction().map(|fraction| fraction * 100.0)
345 }
346}