Skip to main content

fits_io/bin_table/
bin_table.rs

1use crate::bin_table::{FieldDefinition, Row, Value};
2use crate::header::Header;
3#[cfg(feature = "rayon")]
4use rayon::iter::{IntoParallelIterator, ParallelIterator};
5use std::error::Error;
6
7/// The decoded contents of a binary table extension.
8#[derive(Debug, Clone, Default)]
9pub struct BinTable {
10    /// The HDU's whole data section: the rows, then the heap.
11    data: Vec<u8>,
12    field_definitions: Vec<FieldDefinition>,
13    rows: usize,
14    bytes_per_row: usize,
15    /// Where the heap starts within `data`, from THEAP or the end of the rows.
16    heap_offset: usize,
17}
18
19impl BinTable {
20    /// Appends a row, one value per column.
21    ///
22    /// This is how to build a table without going through `serde` — for a
23    /// complex column, say, which no Rust type maps onto on its own.
24    ///
25    /// # Errors
26    ///
27    /// Returns an error when `values` is not one per column: a short row would
28    /// leave the columns after it holding whatever the padding happened to be.
29    pub fn push_row(&mut self, values: &[Value]) -> Result<(), Box<dyn Error + Send + Sync>> {
30        if values.len() != self.field_definitions.len() {
31            return Err(format!(
32                "This table has {} columns, but the row has {} values",
33                self.field_definitions.len(),
34                values.len()
35            )
36            .into());
37        }
38
39        // The columns were measured when the table was made, so a row is always
40        // the same width.
41        if self.bytes_per_row == 0 {
42            self.bytes_per_row = self
43                .field_definitions
44                .iter()
45                .map(|field| field.format.bytes_len())
46                .sum();
47        }
48
49        for (field, value) in self.field_definitions.iter().zip(values) {
50            crate::bin_table::encode::encode(value, field.format, &mut self.data);
51        }
52
53        self.rows += 1;
54        self.heap_offset = self.data.len();
55
56        Ok(())
57    }
58
59    /// An empty table with the given columns.
60    pub fn new(field_definitions: Vec<FieldDefinition>) -> Self {
61        Self {
62            data: vec![],
63            field_definitions,
64            rows: 0,
65            bytes_per_row: 0,
66            heap_offset: 0,
67        }
68    }
69
70    /// Builds a table from rows that are already encoded.
71    ///
72    /// `data` is the rows followed by the heap, and `heap_offset` says where the
73    /// one ends and the other begins. A table with no variable length array
74    /// columns has an empty heap, so its offset is the end of the rows.
75    #[cfg(feature = "serde")]
76    pub(crate) fn from_parts(
77        field_definitions: Vec<FieldDefinition>,
78        data: Vec<u8>,
79        bytes_per_row: usize,
80        rows: usize,
81        heap_offset: usize,
82    ) -> Self {
83        Self {
84            heap_offset: heap_offset.min(data.len()),
85            data,
86            field_definitions,
87            bytes_per_row,
88            rows,
89        }
90    }
91
92    /// The columns this table is made of.
93    pub fn field_definitions(&self) -> &[FieldDefinition] {
94        &self.field_definitions
95    }
96
97    /// The width of one row in bytes, as NAXIS1 gives it.
98    pub fn bytes_per_row(&self) -> usize {
99        self.bytes_per_row
100    }
101
102    /// Reads a table out of a header and its data section.
103    pub fn from_u8(header: &Header, data: Vec<u8>) -> Result<Self, Box<dyn Error + Send + Sync>> {
104        if header.naxis() == Some(2) {
105            let bytes_per_row = header
106                .naxis_n(0)
107                .ok_or("Table header is missing its NAXIS1 card")?;
108            let rows = header
109                .naxis_n(1)
110                .ok_or("Table header is missing its NAXIS2 card")?;
111            let bytes_per_row = usize::try_from(bytes_per_row)
112                .map_err(|_| format!("NAXIS1 must not be negative, but was {}", bytes_per_row))?;
113            let rows = usize::try_from(rows)
114                .map_err(|_| format!("NAXIS2 must not be negative, but was {}", rows))?;
115
116            let expected = rows
117                .checked_mul(bytes_per_row)
118                .ok_or("Table dimensions overflow the address space")?;
119            if data.len() < expected {
120                return Err(format!(
121                    "Data vec is too short, expected {} bytes, but data was only {} bytes long",
122                    expected,
123                    data.len()
124                )
125                .into());
126            }
127
128            let field_definitions = FieldDefinition::all_from_header(header)?;
129            Ok(Self {
130                heap_offset: header.table_heap_offset().min(data.len()),
131                data,
132                field_definitions,
133                bytes_per_row,
134                rows,
135            })
136        } else {
137            Err("Only two dimensions are supported.".into())
138        }
139    }
140
141    /// The row at `row`, or `None` past the end of the table.
142    pub fn row(&'_ self, row: usize) -> Option<Row<'_>> {
143        if row < self.rows {
144            let offset = self.bytes_per_row * row;
145            let data = &self.data[offset..offset + self.bytes_per_row];
146            Some(Row::new(&self.field_definitions, data, self.heap()))
147        } else {
148            None
149        }
150    }
151
152    /// The whole data section: the rows, and then the heap.
153    pub fn data(&self) -> &[u8] {
154        &self.data
155    }
156
157    /// The size of the heap, which is what a table's PCOUNT card records.
158    pub fn heap_len(&self) -> usize {
159        self.data.len().saturating_sub(self.heap_offset)
160    }
161
162    /// The table's heap: the values that variable length array columns point at.
163    ///
164    /// Empty for a table without such columns, which is most of them.
165    pub fn heap(&self) -> &[u8] {
166        &self.data[self.heap_offset..]
167    }
168
169    /// Every row of the table, in order.
170    pub fn rows(&'_ self) -> impl Iterator<Item = Row<'_>> + '_ {
171        (0..(self.rows)).filter_map(move |row| self.row(row))
172    }
173
174    /// Every row of the table, decoded in parallel.
175    #[cfg(feature = "rayon")]
176    pub fn rows_parallel(&'_ self) -> impl ParallelIterator<Item = Row<'_>> + '_ {
177        (0..(self.rows))
178            .into_par_iter()
179            .filter_map(move |row| self.row(row))
180    }
181
182    /// The number of rows in this table.
183    pub fn len(&self) -> usize {
184        self.rows
185    }
186
187    /// Whether the table has no rows at all.
188    pub fn is_empty(&self) -> bool {
189        self.rows == 0
190    }
191}