Skip to main content

fit/
datetime.rs

1//! FIT timestamp conversions.
2//!
3//! FIT stores absolute time as a `u32` count of seconds since
4//! **1989-12-31 00:00:00 UTC** — 631,065,600 seconds *after* the Unix epoch.
5//! The `chrono`-based helpers ([`fit_to_datetime`] / [`datetime_to_fit`]) are
6//! only compiled when the `chrono` feature is enabled.
7
8#[cfg(feature = "chrono")]
9use chrono::{DateTime, Utc};
10
11/// Seconds between Unix epoch (`1970-01-01`) and FIT epoch (`1989-12-31`).
12pub const FIT_EPOCH_OFFSET_SECS: i64 = 631_065_600;
13
14/// Convert a FIT timestamp (seconds since FIT epoch) into a UTC datetime.
15///
16/// Returns `None` only for the unrepresentable case where adding the offset
17/// overflows `i64::MAX` — practically impossible for `u32` inputs.
18#[cfg(feature = "chrono")]
19pub fn fit_to_datetime(fit_seconds: u32) -> Option<DateTime<Utc>> {
20    let unix = i64::from(fit_seconds).checked_add(FIT_EPOCH_OFFSET_SECS)?;
21    DateTime::from_timestamp(unix, 0)
22}
23
24/// Inverse of [`fit_to_datetime`]. Returns `None` if the datetime is before
25/// the FIT epoch or beyond `u32::MAX` seconds after it (≈ year 2125).
26#[cfg(feature = "chrono")]
27pub fn datetime_to_fit(dt: DateTime<Utc>) -> Option<u32> {
28    let secs = dt.timestamp().checked_sub(FIT_EPOCH_OFFSET_SECS)?;
29    if (0..=i64::from(u32::MAX)).contains(&secs) {
30        Some(secs as u32)
31    } else {
32        None
33    }
34}
35
36#[cfg(all(test, feature = "chrono"))]
37mod tests {
38    use super::*;
39    use chrono::TimeZone;
40
41    #[test]
42    fn fit_epoch_is_1989_12_31_utc() {
43        let dt = fit_to_datetime(0).unwrap();
44        assert_eq!(dt, Utc.with_ymd_and_hms(1989, 12, 31, 0, 0, 0).unwrap());
45    }
46
47    #[test]
48    fn known_value_round_trips() {
49        let dt = fit_to_datetime(995_749_880).unwrap();
50        assert_eq!(dt.timestamp(), 995_749_880 + FIT_EPOCH_OFFSET_SECS);
51        let back = datetime_to_fit(dt).unwrap();
52        assert_eq!(back, 995_749_880);
53    }
54
55    #[test]
56    fn datetime_before_fit_epoch_is_none() {
57        let dt = Utc.with_ymd_and_hms(1970, 1, 1, 0, 0, 0).unwrap();
58        assert_eq!(datetime_to_fit(dt), None);
59    }
60
61    #[test]
62    fn unix_offset_is_correct() {
63        let unix_dt = DateTime::from_timestamp(FIT_EPOCH_OFFSET_SECS, 0).unwrap();
64        assert_eq!(
65            unix_dt,
66            Utc.with_ymd_and_hms(1989, 12, 31, 0, 0, 0).unwrap()
67        );
68    }
69}