Skip to main content

fits_io/bin_table/
field_definition.rs

1use crate::bin_table::Value;
2use crate::header::{Header, TableColumnFormat};
3use std::error::Error;
4
5/// Parses a TDIMn card's `(a,b,c)` shape.
6///
7/// A shape that does not parse is dropped rather than raised: TDIMn is a hint
8/// about how to fold a column that reads perfectly well without it, so a
9/// malformed one should not cost the caller the whole table.
10fn parse_dimensions(value: &str) -> Vec<usize> {
11    let Some(inner) = value
12        .trim()
13        .strip_prefix('(')
14        .and_then(|value| value.strip_suffix(')'))
15    else {
16        return Vec::new();
17    };
18
19    inner
20        .split(',')
21        .map(|axis| axis.trim().parse::<usize>())
22        .collect::<Result<Vec<_>, _>>()
23        .unwrap_or_default()
24}
25
26/// Everything the header says about one binary table column.
27///
28/// A column is more than its TFORMn type: TSCALn and TZEROn describe a linear
29/// transform from the stored entry to the physical value it stands for, and
30/// TNULLn names the stored entry that means "undefined". [`decode`] applies all
31/// three, so callers see physical values rather than raw ones.
32///
33/// [`decode`]: FieldDefinition::decode
34#[derive(Debug, Clone, PartialEq)]
35pub struct FieldDefinition {
36    /// TFORMn: this column's type and repeat count.
37    pub format: TableColumnFormat,
38    /// Byte offset of this column from the start of a row.
39    pub offset: usize,
40    /// TTYPEn, or an empty string for a column that carries no name.
41    pub name: String,
42    /// TSCALn: the multiplier applied to a stored entry.
43    pub scale: Option<f64>,
44    /// TZEROn: the offset added after scaling.
45    pub zero: Option<f64>,
46    /// TNULLn: the stored entry that marks an undefined value.
47    pub null: Option<i64>,
48    /// TDIMn: the shape this column's entry has, fastest-varying axis first.
49    ///
50    /// Empty for a column with no TDIMn card, which is a plain run of
51    /// [`TableColumnFormat::len`] elements. [`Value`] holds the elements flat
52    /// either way; this says how to fold them.
53    pub dimensions: Vec<usize>,
54}
55
56impl FieldDefinition {
57    /// Reads every column of `header` in order, resolving each one's offset from
58    /// the widths of the columns before it.
59    pub fn all_from_header(header: &Header) -> Result<Vec<Self>, Box<dyn Error + Send + Sync>> {
60        let table_fields = header
61            .table_fields()
62            .ok_or("Table header is missing its TFIELDS card")?;
63        let table_fields = usize::try_from(table_fields)
64            .map_err(|_| format!("TFIELDS must not be negative, but was {}", table_fields))?;
65
66        let mut offset = 0;
67
68        (0..table_fields)
69            .map(|index| {
70                // TFORMn is mandatory: without it the column width, and therefore
71                // every following column offset, is unknown.
72                let format = header.table_column_format(index).ok_or_else(|| {
73                    format!("Table header is missing its TFORM{} card", index + 1)
74                })?;
75
76                // TTYPEn is optional. An unnamed column still occupies its bytes,
77                // it just cannot be looked up by name.
78                let name = header
79                    .table_column_type(index)
80                    .unwrap_or_default()
81                    .to_string();
82
83                let field = Self {
84                    format,
85                    offset,
86                    name,
87                    scale: header.table_scaling_factor(index),
88                    zero: header.table_scaling_zero_point(index),
89                    null: header
90                        .table_null_value(index)
91                        .and_then(|null| null.as_integer()),
92                    dimensions: header
93                        .table_dimensions(index)
94                        .map(parse_dimensions)
95                        .unwrap_or_default(),
96                };
97                offset += format.bytes_len();
98
99                Ok::<_, Box<dyn Error + Send + Sync>>(field)
100            })
101            .collect()
102    }
103
104    /// Decodes this column out of `row`, which must be a whole row of the table.
105    ///
106    /// `heap` is the table's heap, which only a variable length array column
107    /// reads from; pass an empty slice for a table that has none.
108    pub fn decode(&self, row: &[u8], heap: &[u8]) -> crate::Result<Value> {
109        let data = row.get(self.offset..).ok_or_else(|| {
110            crate::Error::DeserializationError(format!(
111                "Column {} starts at byte {} of a {} byte row",
112                self.name,
113                self.offset,
114                row.len()
115            ))
116        })?;
117
118        let value = self.format.parse_into_value(data, heap)?;
119
120        if self.is_null(&value) {
121            return Ok(Value::Null);
122        }
123
124        Ok(self.scaled(value))
125    }
126
127    /// Whether every element of `value` is the TNULLn entry.
128    ///
129    /// TNULLn applies only to the integer columns; floating point columns say
130    /// "undefined" with a NaN, which needs no header to interpret. A column
131    /// entry of repeat count `r` holds `r` values that are each null or not, and
132    /// [`Value`] has no way to say "the third element is undefined", so only an
133    /// entry that is null throughout is reported as such. In practice TNULLn is
134    /// used with scalar columns, where the two are the same thing.
135    fn is_null(&self, value: &Value) -> bool {
136        let Some(null) = self.null else {
137            return false;
138        };
139
140        fn all_equal<T: Copy + Into<i64>>(values: &[T], null: i64) -> bool {
141            !values.is_empty() && values.iter().all(|value| (*value).into() == null)
142        }
143
144        match value {
145            Value::U8(values) => all_equal(values, null),
146            Value::I8(values) => all_equal(values, null),
147            Value::U16(values) => all_equal(values, null),
148            Value::I16(values) => all_equal(values, null),
149            Value::U32(values) => all_equal(values, null),
150            Value::I32(values) => all_equal(values, null),
151            Value::I64(values) => all_equal(values, null),
152            _ => false,
153        }
154    }
155
156    /// Applies TSCALn and TZEROn to a decoded entry.
157    fn scaled(&self, value: Value) -> Value {
158        let scale = self.scale.unwrap_or(1.0);
159        let zero = self.zero.unwrap_or(0.0);
160
161        if scale == 1.0 && zero == 0.0 {
162            return value;
163        }
164
165        // The FITS standard has no unsigned integer TFORMn codes. Unsigned
166        // columns are instead written as the signed type of the same width with
167        // a TZEROn of half its range, so that reading them back as a float would
168        // both lose precision and lose the type. Recognise that pairing and
169        // return the integers the file actually means.
170        //
171        // The conversion has to *add* the offset, which for a half-range TZEROn
172        // is the same as flipping the top bit. Reinterpreting the stored bit
173        // pattern instead leaves every value out by half the range: a `1I`
174        // column with TZERO 32768 storing -32767 means 1, not 32769.
175        if scale == 1.0 {
176            match (&value, zero) {
177                (Value::U8(values), -128.0) => {
178                    return Value::I8(
179                        values
180                            .iter()
181                            .map(|v| v.wrapping_sub(1 << 7) as i8)
182                            .collect(),
183                    );
184                }
185                (Value::I16(values), 32768.0) => {
186                    return Value::U16(
187                        values
188                            .iter()
189                            .map(|v| (*v as u16).wrapping_add(1 << 15))
190                            .collect(),
191                    );
192                }
193                (Value::I32(values), 2147483648.0) => {
194                    return Value::U32(
195                        values
196                            .iter()
197                            .map(|v| (*v as u32).wrapping_add(1 << 31))
198                            .collect(),
199                    );
200                }
201                (Value::I64(values), 9223372036854775808.0) => {
202                    return Value::U64(
203                        values
204                            .iter()
205                            .map(|v| (*v as u64).wrapping_add(1 << 63))
206                            .collect(),
207                    );
208                }
209                _ => {}
210            }
211        }
212
213        let apply = |raw: f64| zero + scale * raw;
214
215        match value {
216            Value::U8(values) => Value::F64(values.into_iter().map(|v| apply(v as f64)).collect()),
217            Value::I8(values) => Value::F64(values.into_iter().map(|v| apply(v as f64)).collect()),
218            Value::U16(values) => Value::F64(values.into_iter().map(|v| apply(v as f64)).collect()),
219            Value::I16(values) => Value::F64(values.into_iter().map(|v| apply(v as f64)).collect()),
220            Value::U32(values) => Value::F64(values.into_iter().map(|v| apply(v as f64)).collect()),
221            Value::I32(values) => Value::F64(values.into_iter().map(|v| apply(v as f64)).collect()),
222            Value::I64(values) => Value::F64(values.into_iter().map(|v| apply(v as f64)).collect()),
223            Value::U64(values) => Value::F64(values.into_iter().map(|v| apply(v as f64)).collect()),
224            Value::F32(values) => Value::F64(values.into_iter().map(|v| apply(v as f64)).collect()),
225            Value::F64(values) => Value::F64(values.into_iter().map(apply).collect()),
226
227            // Both components of a complex value scale by the same factor.
228            Value::C32(values) => Value::C32(
229                values
230                    .into_iter()
231                    .map(|(re, im)| (apply(re as f64) as f32, apply(im as f64) as f32))
232                    .collect(),
233            ),
234            Value::M64(values) => Value::M64(
235                values
236                    .into_iter()
237                    .map(|(re, im)| (apply(re), apply(im)))
238                    .collect(),
239            ),
240
241            // Scaling is meaningless for the non-numeric columns.
242            value @ (Value::String(_)
243            | Value::StringArray(_)
244            | Value::Boolean(_)
245            | Value::Bit { .. }
246            | Value::Null) => value,
247        }
248    }
249}
250
251#[cfg(test)]
252mod tests {
253    use super::parse_dimensions;
254
255    #[test]
256    fn a_tdim_card_gives_the_shape_of_a_column_entry() {
257        assert_eq!(parse_dimensions("(2,3)"), vec![2, 3]);
258        assert_eq!(parse_dimensions("(4, 5, 6)"), vec![4, 5, 6]);
259        assert_eq!(parse_dimensions(" (7) "), vec![7]);
260    }
261
262    #[test]
263    fn a_malformed_tdim_card_is_dropped_rather_than_raised() {
264        // The column reads fine without its shape, so a bad TDIMn must not cost
265        // the caller the table.
266        assert!(parse_dimensions("2,3").is_empty());
267        assert!(parse_dimensions("(2,x)").is_empty());
268        assert!(parse_dimensions("").is_empty());
269    }
270}