Skip to main content

leap_sec/
builder.rs

1//! Builder for custom leap-second tables.
2//!
3//! Useful for testing with synthetic schedules or for constructing tables
4//! from parsed data sources.
5
6use crate::error::Error;
7use crate::table::{LeapEntryInner, LeapSeconds};
8use crate::types::UtcUnixSeconds;
9
10/// A builder for constructing custom [`LeapSeconds`] tables.
11///
12/// # Example
13///
14/// ```
15/// use leap_sec::prelude::*;
16///
17/// let table = LeapSeconds::builder()
18///     .add(UtcUnixSeconds(63_072_000), 10)   // 1972-01-01
19///     .add(UtcUnixSeconds(78_796_800), 11)   // 1972-07-01
20///     .build()
21///     .unwrap();
22///
23/// let tai = table.utc_to_tai(UtcUnixSeconds(70_000_000)).unwrap();
24/// assert_eq!(tai, TaiSeconds(70_000_010));
25/// ```
26#[derive(Debug, Clone)]
27pub struct LeapSecondsBuilder {
28    entries: Vec<LeapEntryInner>,
29    expires_at: Option<i64>,
30}
31
32impl LeapSecondsBuilder {
33    /// Create a new empty builder.
34    pub const fn new() -> Self {
35        Self {
36            entries: Vec::new(),
37            expires_at: None,
38        }
39    }
40
41    /// Add a leap-second entry.
42    ///
43    /// `utc` is the UTC Unix timestamp at which the offset takes effect.
44    /// `tai_minus_utc` is the cumulative TAI−UTC offset from this point forward.
45    #[must_use]
46    pub fn add(mut self, utc: UtcUnixSeconds, tai_minus_utc: i32) -> Self {
47        self.entries.push(LeapEntryInner {
48            utc_unix: utc.0,
49            tai_minus_utc,
50        });
51        self
52    }
53
54    /// Set an expiration timestamp for the table.
55    #[must_use]
56    pub const fn expires_at(mut self, at: UtcUnixSeconds) -> Self {
57        self.expires_at = Some(at.0);
58        self
59    }
60
61    /// Build the leap-second table.
62    ///
63    /// # Errors
64    ///
65    /// Returns [`Error::InvalidTable`] if:
66    /// - The table is empty
67    /// - Timestamps are not monotonically increasing
68    pub fn build(self) -> Result<LeapSeconds, Error> {
69        if self.entries.is_empty() {
70            return Err(Error::InvalidTable {
71                detail: "table must contain at least one entry",
72            });
73        }
74
75        for w in self.entries.windows(2) {
76            if w[1].utc_unix <= w[0].utc_unix {
77                return Err(Error::InvalidTable {
78                    detail: "timestamps must be monotonically increasing",
79                });
80            }
81        }
82
83        Ok(LeapSeconds::from_owned(self.entries, self.expires_at))
84    }
85}
86
87impl Default for LeapSecondsBuilder {
88    fn default() -> Self {
89        Self::new()
90    }
91}
92
93impl LeapSeconds {
94    /// Create a builder for constructing a custom leap-second table.
95    pub const fn builder() -> LeapSecondsBuilder {
96        LeapSecondsBuilder::new()
97    }
98}