Skip to main content

fits_well/header/
value.rs

1/// The typed value of a valued keyword record (§4.2).
2///
3/// The three string-ish "no real value" cases of §4.2.1 are kept distinct: a
4/// quoted empty/null string is [`Value::Text`] (possibly empty), whereas a blank
5/// value field with no quotes is [`Value::Undefined`].
6#[derive(Debug, Clone, PartialEq)]
7pub enum Value {
8    /// `T` or `F`.
9    Logical(bool),
10    Integer(i64),
11    Real(f64),
12    /// A character string, already unescaped (`''` → `'`) with insignificant
13    /// trailing spaces removed.
14    Text(String),
15    ComplexInteger {
16        re: i64,
17        im: i64,
18    },
19    ComplexReal {
20        re: f64,
21        im: f64,
22    },
23    /// A value indicator was present but the field was blank.
24    Undefined,
25}
26
27impl Value {
28    pub fn as_logical(&self) -> Option<bool> {
29        match self {
30            Value::Logical(b) => Some(*b),
31            _ => None,
32        }
33    }
34
35    pub fn as_integer(&self) -> Option<i64> {
36        match self {
37            Value::Integer(i) => Some(*i),
38            // A mandatory-integer keyword written in real form (e.g. `NAXIS = 2.0`):
39            // accept an integral value rather than reporting it absent, which would
40            // silently mis-size the data unit. Non-integral reals stay `None`.
41            Value::Real(r) if r.fract() == 0.0 => Some(*r as i64),
42            _ => None,
43        }
44    }
45
46    /// The value as `f64`, widening an [`Value::Integer`] to a real.
47    pub fn as_real(&self) -> Option<f64> {
48        match self {
49            Value::Real(r) => Some(*r),
50            Value::Integer(i) => Some(*i as f64),
51            _ => None,
52        }
53    }
54
55    pub fn as_text(&self) -> Option<&str> {
56        match self {
57            Value::Text(s) => Some(s),
58            _ => None,
59        }
60    }
61}
62
63impl From<bool> for Value {
64    fn from(b: bool) -> Self {
65        Value::Logical(b)
66    }
67}
68
69impl From<i64> for Value {
70    fn from(i: i64) -> Self {
71        Value::Integer(i)
72    }
73}
74
75impl From<i32> for Value {
76    fn from(i: i32) -> Self {
77        Value::Integer(i as i64)
78    }
79}
80
81impl From<f64> for Value {
82    fn from(r: f64) -> Self {
83        Value::Real(r)
84    }
85}
86
87impl From<&str> for Value {
88    fn from(s: &str) -> Self {
89        Value::Text(s.to_string())
90    }
91}
92
93impl From<String> for Value {
94    fn from(s: String) -> Self {
95        Value::Text(s)
96    }
97}
98
99#[cfg(test)]
100mod tests {
101    use super::*;
102
103    #[test]
104    fn accessors_only_match_their_own_variant() {
105        assert_eq!(Value::Logical(true).as_logical(), Some(true));
106        assert_eq!(Value::Logical(true).as_integer(), None);
107
108        assert_eq!(Value::Integer(42).as_integer(), Some(42));
109        assert_eq!(Value::Integer(42).as_logical(), None);
110
111        assert_eq!(Value::Text("x".into()).as_text(), Some("x"));
112        assert_eq!(Value::Text("x".into()).as_integer(), None);
113
114        assert_eq!(Value::Undefined.as_text(), None);
115        assert_eq!(Value::Undefined.as_real(), None);
116    }
117
118    #[test]
119    fn as_real_widens_integers_but_not_other_types() {
120        assert_eq!(Value::Real(1.5).as_real(), Some(1.5));
121        assert_eq!(Value::Integer(3).as_real(), Some(3.0));
122        assert_eq!(Value::Logical(true).as_real(), None);
123    }
124
125    #[test]
126    fn from_conversions_pick_the_right_variant() {
127        assert_eq!(Value::from(true), Value::Logical(true));
128        assert_eq!(Value::from(16_i32), Value::Integer(16));
129        assert_eq!(Value::from(16_i64), Value::Integer(16));
130        assert_eq!(Value::from(1.5_f64), Value::Real(1.5));
131        assert_eq!(Value::from("hi"), Value::Text("hi".into()));
132        assert_eq!(Value::from(String::from("hi")), Value::Text("hi".into()));
133    }
134}