1use crate::error::FitsError;
9use crate::error::Result;
10use crate::header::Header;
11use crate::keyword::key;
12use crate::table::ColumnData;
13
14#[derive(Debug, Clone, Copy, PartialEq, Eq)]
16pub enum AsciiKind {
17 Char,
19 Integer,
21 Float,
23}
24
25#[derive(Debug, Clone)]
27pub struct AsciiColumn {
28 pub name: Option<String>,
29 pub unit: Option<String>,
30 pub kind: AsciiKind,
31 pub start: usize,
33 pub width: usize,
34 pub decimals: usize,
36 pub tscale: f64,
38 pub tzero: f64,
39 pub null: Option<String>,
41}
42
43#[derive(Debug, Clone)]
45pub struct AsciiTable {
46 pub nrows: usize,
47 pub columns: Vec<AsciiColumn>,
48 row_len: usize,
49 bytes: Vec<u8>,
50}
51
52impl AsciiTable {
53 pub(crate) fn from_data(header: &Header, data: Vec<u8>) -> Result<AsciiTable> {
54 let row_len = header
55 .get_integer("NAXIS1")
56 .ok_or(FitsError::MissingKeyword { name: "NAXIS1" })?
57 .max(0) as usize;
58 let nrows = header
59 .get_integer("NAXIS2")
60 .ok_or(FitsError::MissingKeyword { name: "NAXIS2" })?
61 .max(0) as usize;
62 let tfields = match header.get_integer("TFIELDS") {
65 Some(t) if (0..=999).contains(&t) => t as usize,
66 Some(_) => return Err(FitsError::KeywordOutOfRange { name: "TFIELDS" }),
67 None => return Err(FitsError::MissingKeyword { name: "TFIELDS" }),
68 };
69
70 let mut columns = Vec::with_capacity(tfields);
71 for n in 1..=tfields {
72 let tbcol = header
73 .get_integer(key!("TBCOL{n}").as_str())
74 .ok_or(FitsError::MissingKeyword { name: "TBCOLn" })?;
75 let tform = header
76 .get_text(key!("TFORM{n}").as_str())
77 .ok_or(FitsError::MissingKeyword { name: "TFORMn" })?;
78 let fmt = parse_ascii_tform(tform)?;
79 let start = (tbcol.max(1) - 1) as usize;
80 if start.checked_add(fmt.width).is_none_or(|end| end > row_len) {
84 return Err(FitsError::KeywordOutOfRange { name: "TBCOLn" });
85 }
86 columns.push(AsciiColumn {
87 name: header
88 .get_text(key!("TTYPE{n}").as_str())
89 .map(str::to_string)
90 .filter(|s| !s.is_empty()),
91 unit: header
92 .get_text(key!("TUNIT{n}").as_str())
93 .map(str::to_string)
94 .filter(|s| !s.is_empty()),
95 kind: fmt.kind,
96 start,
97 width: fmt.width,
98 decimals: fmt.decimals,
99 tscale: header.get_real(key!("TSCAL{n}").as_str()).unwrap_or(1.0),
100 tzero: header.get_real(key!("TZERO{n}").as_str()).unwrap_or(0.0),
101 null: header
102 .get_text(key!("TNULL{n}").as_str())
103 .map(|s| s.trim().to_string()),
104 });
105 }
106
107 let total = nrows.checked_mul(row_len).ok_or(FitsError::UnexpectedEof)?;
110 if data.len() < total {
111 return Err(FitsError::UnexpectedEof);
112 }
113 Ok(AsciiTable {
114 nrows,
115 columns,
116 row_len,
117 bytes: data,
118 })
119 }
120
121 pub fn column_index(&self, name: &str) -> Option<usize> {
124 self.columns.iter().position(|c| {
125 c.name
126 .as_deref()
127 .is_some_and(|n| n.eq_ignore_ascii_case(name))
128 })
129 }
130
131 fn column_index_checked(&self, name: &str) -> Result<usize> {
132 self.column_index(name)
133 .ok_or_else(|| FitsError::ColumnNotFound {
134 name: name.to_string(),
135 })
136 }
137
138 pub fn column_by_idx(&self, index: usize) -> Result<AsciiColumnReader<'_>> {
142 if index >= self.columns.len() {
143 return Err(FitsError::ColumnIndexOutOfBounds {
144 index,
145 len: self.columns.len(),
146 });
147 }
148 Ok(AsciiColumnReader { table: self, index })
149 }
150
151 pub fn column_by_name(&self, name: &str) -> Result<AsciiColumnReader<'_>> {
154 let index = self.column_index_checked(name)?;
155 Ok(AsciiColumnReader { table: self, index })
156 }
157
158 fn field(&self, col: &AsciiColumn, r: usize) -> Result<&str> {
163 let row = &self.bytes[r * self.row_len..(r + 1) * self.row_len];
164 let end = (col.start + col.width).min(row.len());
165 let raw = if col.start < end {
166 &row[col.start..end]
167 } else {
168 &[]
169 };
170 let text = std::str::from_utf8(raw).map_err(|_| FitsError::InvalidValue {
171 card: "non-UTF-8 bytes in ASCII-table field".to_string(),
172 })?;
173 Ok(text.trim())
174 }
175}
176
177#[derive(Debug, Clone, Copy)]
182pub struct AsciiColumnReader<'a> {
183 table: &'a AsciiTable,
184 index: usize,
185}
186
187impl<'a> AsciiColumnReader<'a> {
188 pub fn descriptor(&self) -> &'a AsciiColumn {
190 &self.table.columns[self.index]
191 }
192
193 pub fn raw(&self) -> Result<ColumnData> {
198 let table = self.table;
199 let col = self.descriptor();
200 match col.kind {
201 AsciiKind::Char => Ok(ColumnData::Text(
202 (0..table.nrows)
203 .map(|r| Ok(table.field(col, r)?.to_string()))
204 .collect::<Result<_>>()?,
205 )),
206 AsciiKind::Integer => {
207 let mut out = Vec::with_capacity(table.nrows);
208 for r in 0..table.nrows {
209 let s = table.field(col, r)?;
210 out.push(if s.is_empty() || col.is_null(s) {
211 0
212 } else {
213 s.parse().map_err(|_| FitsError::InvalidValue {
214 card: s.to_string(),
215 })?
216 });
217 }
218 Ok(ColumnData::I64(out))
219 }
220 AsciiKind::Float => {
221 let mut out = Vec::with_capacity(table.nrows);
222 for r in 0..table.nrows {
223 let s = table.field(col, r)?;
224 out.push(if s.is_empty() || col.is_null(s) {
225 0.0
226 } else {
227 parse_ascii_float(s, col.decimals).ok_or_else(|| {
228 FitsError::InvalidValue {
229 card: s.to_string(),
230 }
231 })?
232 });
233 }
234 Ok(ColumnData::F64(out))
235 }
236 }
237 }
238
239 pub fn physical(&self) -> Result<Vec<f64>> {
247 let table = self.table;
248 let col = self.descriptor();
249 if col.kind == AsciiKind::Char {
250 return Err(FitsError::NonNumericColumn { code: 'A' });
251 }
252 let mut out = Vec::with_capacity(table.nrows);
253 for r in 0..table.nrows {
254 let s = table.field(col, r)?;
255 if col.is_null(s) {
256 out.push(f64::NAN);
257 continue;
258 }
259 let raw = if s.is_empty() {
260 0.0
261 } else {
262 parse_ascii_float(s, col.decimals).ok_or_else(|| FitsError::InvalidValue {
263 card: s.to_string(),
264 })?
265 };
266 out.push(col.tzero + col.tscale * raw);
267 }
268 Ok(out)
269 }
270}
271
272impl AsciiColumn {
273 fn is_null(&self, field: &str) -> bool {
275 self.null.as_deref() == Some(field)
276 }
277}
278
279fn parse_ascii_float(field: &str, decimals: usize) -> Option<f64> {
283 let (mantissa, exponent) = match split_mantissa_exponent(field) {
284 Some((m, e)) => (m, Some(e)),
285 None => (field, None),
286 };
287 let mut value: f64 = if mantissa.contains('.') || decimals == 0 {
288 mantissa.parse().ok()?
289 } else {
290 mantissa.parse::<f64>().ok()? / 10f64.powi(decimals as i32)
291 };
292 if let Some(e) = exponent {
293 value *= 10f64.powi(e.trim().parse::<i32>().ok()?);
294 }
295 Some(value)
296}
297
298fn split_mantissa_exponent(s: &str) -> Option<(&str, &str)> {
304 if let Some(i) = s.find(['E', 'e', 'D', 'd']) {
305 return Some((&s[..i], &s[i + 1..]));
306 }
307 s.char_indices()
308 .find(|&(i, c)| i > 0 && (c == '+' || c == '-'))
309 .map(|(i, _)| (&s[..i], &s[i..]))
310}
311
312#[derive(Debug, Clone, Copy, PartialEq, Eq)]
314struct AsciiFormat {
315 kind: AsciiKind,
316 width: usize,
317 decimals: usize,
318}
319
320fn parse_ascii_tform(value: &str) -> Result<AsciiFormat> {
322 let s = value.trim();
323 let invalid = || FitsError::InvalidTform {
324 tform: value.to_string(),
325 };
326 let letter = s.bytes().next().ok_or_else(invalid)?;
327 let kind = match letter {
328 b'A' => AsciiKind::Char,
329 b'I' => AsciiKind::Integer,
330 b'F' | b'E' | b'D' => AsciiKind::Float,
331 _ => return Err(invalid()),
332 };
333 let rest = &s[1..];
334 let (width, decimals) = match rest.split_once('.') {
335 Some((w, d)) => (
336 w.trim().parse().map_err(|_| invalid())?,
337 d.trim().parse().map_err(|_| invalid())?,
338 ),
339 None => (rest.trim().parse().map_err(|_| invalid())?, 0),
340 };
341 Ok(AsciiFormat {
342 kind,
343 width,
344 decimals,
345 })
346}
347
348#[cfg(test)]
349mod tests;