Skip to main content

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}