Skip to main content

fit/transforms/
scale_offset.rs

1//! Profile-level Scale/Offset transformation.
2//!
3//! Per protocol ยง3.2:
4//!
5//! ```text
6//!   physical = raw_value / scale - offset
7//!   raw_value = round((physical + offset) * scale)
8//! ```
9//!
10//! `scale` and `offset` come from the field's Profile metadata. `None`
11//! everywhere is treated as the identity.
12
13/// Apply scale/offset to a raw numeric value, producing a physical (`f64`)
14/// quantity. `None` for either parameter is the identity.
15#[inline]
16pub fn apply(raw: f64, scale: Option<f64>, offset: Option<f64>) -> f64 {
17    let scale = scale.unwrap_or(1.0);
18    let offset = offset.unwrap_or(0.0);
19    if scale == 1.0 && offset == 0.0 {
20        // Skip the divide entirely so the result preserves any integer-exact
21        // value (e.g. `heart_rate` 126 stays exactly 126.0).
22        raw
23    } else {
24        raw / scale - offset
25    }
26}
27
28/// Inverse of [`apply`] โ€” used by the encoder (M8) to round-trip user-space
29/// physical values back to wire-space integers.
30#[inline]
31pub fn unapply(physical: f64, scale: Option<f64>, offset: Option<f64>) -> f64 {
32    let scale = scale.unwrap_or(1.0);
33    let offset = offset.unwrap_or(0.0);
34    (physical + offset) * scale
35}
36
37/// True when scale/offset are *both* the identity (and so applying would be
38/// a no-op). Useful for hot-path skip checks.
39#[inline]
40pub fn is_identity(scale: Option<f64>, offset: Option<f64>) -> bool {
41    scale.unwrap_or(1.0) == 1.0 && offset.unwrap_or(0.0) == 0.0
42}
43
44#[cfg(test)]
45mod tests {
46    use super::*;
47
48    #[test]
49    fn no_scale_no_offset_is_identity() {
50        assert_eq!(apply(126.0, None, None), 126.0);
51        assert_eq!(apply(126.0, Some(1.0), Some(0.0)), 126.0);
52    }
53
54    #[test]
55    fn speed_scale_1000() {
56        // raw = 3000 (mm/s wire), scale = 1000, offset = 0 โ†’ 3.0 m/s
57        assert_eq!(apply(3000.0, Some(1000.0), None), 3.0);
58    }
59
60    #[test]
61    fn altitude_scale_5_offset_500() {
62        // raw = 10435, scale = 5, offset = 500 โ†’ 10435/5 - 500 = 1587.0 m
63        assert_eq!(apply(10435.0, Some(5.0), Some(500.0)), 1587.0);
64    }
65
66    #[test]
67    fn unapply_round_trips_apply() {
68        let raw = 10435.0;
69        let scale = Some(5.0);
70        let offset = Some(500.0);
71        let physical = apply(raw, scale, offset);
72        assert_eq!(unapply(physical, scale, offset), raw);
73    }
74
75    #[test]
76    fn is_identity_classifies_correctly() {
77        assert!(is_identity(None, None));
78        assert!(is_identity(Some(1.0), Some(0.0)));
79        assert!(!is_identity(Some(1000.0), None));
80        assert!(!is_identity(None, Some(500.0)));
81    }
82}