Skip to main content

rs_teststand/expression/
decimal_point.rs

1//! Which decimal separator expression text uses.
2
3/// How a decimal point is written when converting expression text
4/// (`DecimalPointLocalizationOptions`).
5///
6/// Expression text is not locale-neutral. `1.5` and `1,5` are the same number
7/// written for different stations, so converting between the stored form and
8/// the form an operator reads has to say which convention is meant.
9///
10/// There is no zero variant, deliberately: the engine numbers these from one
11/// and rejects zero with `TS_Err_ValueIsInvalidOrOutOfRange`. Passing a raw
12/// integer is what makes that mistake easy, which is why these members take
13/// this type instead.
14#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
15#[non_exhaustive]
16#[expect(
17    clippy::enum_variant_names,
18    reason = "the shared Use prefix is the vendor's own: DecimalPoint_UsePeriod, \
19              DecimalPoint_UseComma. Stripping it to satisfy the lint would leave \
20              names that no longer match the reference a reader is holding."
21)]
22pub enum DecimalPointLocalizationOption {
23    /// Follow the station's TestStand preference.
24    UsePreference,
25    /// Follow the operating system's regional setting.
26    UseSystemSetting,
27    /// Always a period, whatever the station is set to.
28    UsePeriod,
29    /// Always a comma, whatever the station is set to.
30    UseComma,
31}
32
33impl DecimalPointLocalizationOption {
34    /// Maps the engine's number onto an option.
35    ///
36    /// # Errors
37    /// The raw value, when it is one this build does not name.
38    pub const fn from_bits(bits: i32) -> Result<Self, i32> {
39        match bits {
40            1 => Ok(Self::UsePreference),
41            2 => Ok(Self::UseSystemSetting),
42            3 => Ok(Self::UsePeriod),
43            4 => Ok(Self::UseComma),
44            other => Err(other),
45        }
46    }
47
48    /// The number the engine expects.
49    #[must_use]
50    pub const fn bits(self) -> i32 {
51        match self {
52            Self::UsePreference => 1,
53            Self::UseSystemSetting => 2,
54            Self::UsePeriod => 3,
55            Self::UseComma => 4,
56        }
57    }
58}
59
60#[cfg(test)]
61mod tests {
62    use super::DecimalPointLocalizationOption;
63
64    #[test]
65    fn every_option_round_trips_through_the_engine_numbering() {
66        for option in [
67            DecimalPointLocalizationOption::UsePreference,
68            DecimalPointLocalizationOption::UseSystemSetting,
69            DecimalPointLocalizationOption::UsePeriod,
70            DecimalPointLocalizationOption::UseComma,
71        ] {
72            assert_eq!(
73                DecimalPointLocalizationOption::from_bits(option.bits()),
74                Ok(option),
75            );
76        }
77    }
78
79    #[test]
80    fn zero_is_rejected_rather_than_silently_accepted() {
81        // The engine numbers these from one. Zero reaching the engine returns
82        // TS_Err_ValueIsInvalidOrOutOfRange, measured, so it must not map to a
83        // variant here either.
84        assert_eq!(DecimalPointLocalizationOption::from_bits(0), Err(0));
85    }
86}