Skip to main content

fits_io/header/
value.rs

1use crate::header::card::Card;
2use crate::header::table_null_value::TableNullValue;
3
4/// The value a header card carries, with the comment written beside it.
5///
6/// FITS gives a card one of four kinds of value — an integer, a floating point
7/// number, a logical `T`/`F`, or a quoted string — or no value at all. Build one
8/// from the Rust type it corresponds to and hand it to [`Header::set_card`]:
9///
10/// ```
11/// # use fits_io::header::{Header, Value};
12/// # fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
13/// let mut header = Header::default();
14/// header.set_card("FILTER", "Halpha")?;
15/// header.set_card("GAIN", Value::from(1.5).with_comment("e-/ADU"))?;
16/// # Ok(())
17/// # }
18/// ```
19///
20/// [`Header::set_card`]: crate::header::Header::set_card
21#[derive(Debug, Clone, PartialEq)]
22pub enum Value {
23    /// A whole number, written right-justified against column 30.
24    Integer {
25        /// The number itself.
26        value: i64,
27        /// The comment after the value, if the card carried one.
28        comment: Option<String>,
29    },
30    /// A floating point number, written right-justified against column 30.
31    Float {
32        /// The number itself.
33        value: f64,
34        /// The comment after the value, if the card carried one.
35        comment: Option<String>,
36    },
37    /// A logical, written as the single character `T` or `F`.
38    Logical {
39        /// The truth of it.
40        value: bool,
41        /// The comment after the value, if the card carried one.
42        comment: Option<String>,
43    },
44    /// Text, written in single quotes with any quote inside it doubled.
45    String {
46        /// The text, unquoted and with its doubled quotes read back as one.
47        value: String,
48        /// The comment after the value, if the card carried one.
49        comment: Option<String>,
50    },
51    /// A keyword written with an empty value field, which the standard allows.
52    Undefined,
53    /// A value field this crate could not read, kept as the text it held so
54    /// that nothing is lost.
55    Invalid(String),
56}
57
58impl Value {
59    /// The comment beside this value, or an empty string where there is none.
60    pub fn comment_to_string(&self) -> String {
61        let comment = match self {
62            Value::Integer { comment, .. } => comment,
63            Value::Float { comment, .. } => comment,
64            Value::Logical { comment, .. } => comment,
65            Value::String { comment, .. } => comment,
66            Value::Undefined => &None,
67            Value::Invalid(_) => &None,
68        };
69
70        if let Some(comment) = comment {
71            comment.to_string()
72        } else {
73            "".to_string()
74        }
75    }
76
77    /// This value as the text a card writes it as, without its quotes.
78    ///
79    /// An undefined or unreadable value has no text, and comes back empty.
80    pub fn value_to_string(&self) -> String {
81        match self {
82            Value::Integer { value, .. } => {
83                format!("{}", value)
84            }
85            Value::Float { value, .. } => {
86                format!("{}", value)
87            }
88            Value::Logical { value, .. } => match value {
89                true => "T".to_string(),
90                false => "F".to_string(),
91            },
92            Value::String { value, .. } => value.to_string(),
93            Value::Undefined => "".to_string(),
94            Value::Invalid(_) => "".to_string(),
95        }
96    }
97}
98
99impl From<&Card> for Value {
100    fn from(card: &Card) -> Self {
101        card.clone().into()
102    }
103}
104
105impl From<Card> for Value {
106    fn from(value: Card) -> Self {
107        match value {
108            Card::Author { value, comment } => Value::String { value, comment },
109            Card::Bitpix { value, comment } => Value::Integer {
110                value: value.into(),
111                comment,
112            },
113            Card::Blank { value, comment } => Value::Integer { value, comment },
114            Card::Blocked { value, comment } => Value::Logical { value, comment },
115            Card::BScale { value, comment } => Value::Float { value, comment },
116            Card::BUnit { value, comment } => Value::String { value, comment },
117            Card::BZero { value, comment } => Value::Float { value, comment },
118            Card::CoordinateDeltaN { value, comment, .. } => Value::Float { value, comment },
119            Card::CoordinateRotationN { value, comment, .. } => Value::Float { value, comment },
120            Card::CoordinateReferencePixelN { value, comment, .. } => {
121                Value::Float { value, comment }
122            }
123            Card::CoordinateValueAtPixelN { value, comment, .. } => Value::Float { value, comment },
124            Card::CoordinateAxisNameN { value, comment, .. } => Value::String { value, comment },
125            Card::Comment(_) => Value::Undefined,
126            Card::DataMax { value, comment } => Value::Float { value, comment },
127            Card::DataMin { value, comment } => Value::Float { value, comment },
128            Card::Date { value, comment } => Value::String {
129                value: value.to_rfc3339(),
130                comment,
131            },
132            Card::DateObserved { value, comment } => Value::String {
133                value: value.to_rfc3339(),
134                comment,
135            },
136            Card::End => Value::Undefined,
137            Card::Epoch { value, comment } => Value::Float { value, comment },
138            Card::Equinox { value, comment } => Value::Float { value, comment },
139            Card::Extend { value, comment } => Value::Logical { value, comment },
140            Card::ExtensionLevel { value, comment } => Value::Integer { value, comment },
141            Card::ExtensionName { value, comment } => Value::String { value, comment },
142            Card::ExtensionVersion { value, comment } => Value::Integer { value, comment },
143            Card::GroupCount { value, comment } => Value::Integer { value, comment },
144            Card::Groups { value, comment } => Value::Logical { value, comment },
145            Card::History(_) => Value::Undefined,
146            Card::Instrument { value, comment } => Value::String { value, comment },
147            Card::NAxis { value, comment } => Value::Integer { value, comment },
148            Card::NAxisN { value, comment, .. } => Value::Integer { value, comment },
149            Card::Object { value, comment } => Value::String { value, comment },
150            Card::Observer { value, comment } => Value::String { value, comment },
151            Card::Origin { value, comment } => Value::String { value, comment },
152            Card::ParameterCount { value, comment } => Value::Integer { value, comment },
153            Card::ParameterScalingFactorN { value, comment, .. } => Value::Float { value, comment },
154            Card::ParameterTypeN { value, comment, .. } => Value::String { value, comment },
155            Card::ParameterScalingZeroPointN { value, comment, .. } => {
156                Value::Float { value, comment }
157            }
158            Card::Reference { value, comment } => Value::String { value, comment },
159            Card::Simple { value, comment } => Value::Logical { value, comment },
160            Card::TableColumnN { value, comment, .. } => Value::Integer { value, comment },
161            Card::TableDimensionsN { value, comment, .. } => Value::String { value, comment },
162            Card::TableDisplayFormatN { value, comment, .. } => Value::String { value, comment },
163            Card::Telescope { value, comment } => Value::String { value, comment },
164            Card::TableFields { value, comment } => Value::Integer { value, comment },
165            Card::TableHeap { value, comment } => Value::Integer { value, comment },
166            Card::TableNullValueN { value, comment, .. } => match value {
167                TableNullValue::Integer(value) => Value::Integer { value, comment },
168                TableNullValue::Text(value) => Value::String { value, comment },
169            },
170            Card::TableScalingFactorN { value, comment, .. } => Value::Float { value, comment },
171            Card::TableTypeN { value, comment, .. } => Value::String { value, comment },
172            Card::TableUnitN { value, comment, .. } => Value::String { value, comment },
173            Card::TableScalingZeroPointN { value, comment, .. } => Value::Float { value, comment },
174            Card::Xtension { value, comment } => Value::String {
175                value: value.into(),
176                comment,
177            },
178            Card::FocalLength { value, comment } => Value::Float { value, comment },
179            Card::ExposureTime { value, comment } => Value::Float {
180                value: value.as_secs_f64(),
181                comment,
182            },
183            Card::CCDTemperature { value, comment } => Value::Float { value, comment },
184            Card::BayerPattern { value, comment } => Value::String {
185                value: value.into(),
186                comment,
187            },
188            Card::Value { value, .. } => value,
189            // A continuation carries a piece of the string its own card began.
190            Card::Continuation { string, comment } => match string {
191                Some(value) => Value::String { value, comment },
192                None => Value::Undefined,
193            },
194            Card::Hierarch { value, .. } => value,
195            Card::Space => Value::Undefined,
196            Card::Undefined(_) => Value::Undefined,
197            Card::TableFormatN { value, comment, .. } => Value::String { value, comment },
198            Card::Creator { value, comment } => Value::String { value, comment },
199            Card::SubframeXPositionInBinnedPixels { value, comment } => {
200                Value::Integer { value, comment }
201            }
202            Card::SubframeYPositionInBinnedPixels { value, comment } => {
203                Value::Integer { value, comment }
204            }
205            Card::BinnedPixelsX { value, comment } => Value::Integer { value, comment },
206            Card::BinnedPixelsY { value, comment } => Value::Integer { value, comment },
207            Card::CCDBinnedPixelsX { value, comment } => Value::Integer { value, comment },
208            Card::CCDBinnedPixelsY { value, comment } => Value::Integer { value, comment },
209            Card::PixelSizeXWithBinningInMicrons { value, comment } => {
210                Value::Float { value, comment }
211            }
212            Card::PixelSizeYWithBinningInMicrons { value, comment } => {
213                Value::Float { value, comment }
214            }
215            Card::ImageType { value, comment } => Value::String {
216                value: value.to_string(),
217                comment,
218            },
219            Card::Exposure { value, comment } => Value::Float {
220                value: value.as_secs_f64(),
221                comment,
222            },
223            Card::Ra { value, comment } => Value::Float { value, comment },
224            Card::Dec { value, comment } => Value::Float { value, comment },
225            Card::GuideCam { value, comment } => Value::String { value, comment },
226            Card::FocusPosition { value, comment } => Value::Integer { value, comment },
227            Card::SiteLongitude { value, comment } => Value::Float { value, comment },
228            Card::SiteLatitude { value, comment } => Value::Float { value, comment },
229            Card::ImageWidth { value, comment } => Value::Integer { value, comment },
230            Card::ImageHeight { value, comment } => Value::Integer { value, comment },
231        }
232    }
233}
234
235/// The comment on a value, replaced rather than merged.
236impl Value {
237    /// This value carrying `comment` beside it.
238    ///
239    /// A comment on an [`Value::Undefined`] or [`Value::Invalid`] value has
240    /// nowhere to live and is dropped: neither of them is written with a value
241    /// field for a comment to follow.
242    #[must_use]
243    pub fn with_comment(self, comment: impl Into<String>) -> Self {
244        let comment = Some(comment.into());
245        match self {
246            Value::Integer { value, .. } => Value::Integer { value, comment },
247            Value::Float { value, .. } => Value::Float { value, comment },
248            Value::Logical { value, .. } => Value::Logical { value, comment },
249            Value::String { value, .. } => Value::String { value, comment },
250            other => other,
251        }
252    }
253}
254
255impl From<i64> for Value {
256    fn from(value: i64) -> Self {
257        Value::Integer {
258            value,
259            comment: None,
260        }
261    }
262}
263
264impl From<i32> for Value {
265    fn from(value: i32) -> Self {
266        Value::from(i64::from(value))
267    }
268}
269
270impl From<u32> for Value {
271    fn from(value: u32) -> Self {
272        Value::from(i64::from(value))
273    }
274}
275
276impl From<usize> for Value {
277    fn from(value: usize) -> Self {
278        // A count that will not fit in a FITS integer is saturated rather than
279        // wrapped, which would write a negative length.
280        Value::from(i64::try_from(value).unwrap_or(i64::MAX))
281    }
282}
283
284impl From<f64> for Value {
285    fn from(value: f64) -> Self {
286        Value::Float {
287            value,
288            comment: None,
289        }
290    }
291}
292
293impl From<f32> for Value {
294    fn from(value: f32) -> Self {
295        Value::from(f64::from(value))
296    }
297}
298
299impl From<bool> for Value {
300    fn from(value: bool) -> Self {
301        Value::Logical {
302            value,
303            comment: None,
304        }
305    }
306}
307
308impl From<String> for Value {
309    fn from(value: String) -> Self {
310        Value::String {
311            value,
312            comment: None,
313        }
314    }
315}
316
317impl From<&str> for Value {
318    fn from(value: &str) -> Self {
319        Value::from(value.to_string())
320    }
321}
322
323impl From<&String> for Value {
324    fn from(value: &String) -> Self {
325        Value::from(value.clone())
326    }
327}
328
329/// An absent value is an undefined one: the keyword is written with an empty
330/// value field, which is how FITS spells "this card has no value".
331impl<T: Into<Value>> From<Option<T>> for Value {
332    fn from(value: Option<T>) -> Self {
333        match value {
334            Some(value) => value.into(),
335            None => Value::Undefined,
336        }
337    }
338}