use super::*;
use crate::reader::FitsReader;
use crate::writer::AsciiWriteColumn;
use crate::writer::FitsWriter;
use std::io::Cursor;
#[test]
fn parses_ascii_tform_codes() {
let fmt = |kind, width, decimals| AsciiFormat {
kind,
width,
decimals,
};
assert_eq!(parse_ascii_tform("A8").unwrap(), fmt(AsciiKind::Char, 8, 0));
assert_eq!(
parse_ascii_tform("I10").unwrap(),
fmt(AsciiKind::Integer, 10, 0)
);
assert_eq!(
parse_ascii_tform("F8.2").unwrap(),
fmt(AsciiKind::Float, 8, 2)
);
assert_eq!(
parse_ascii_tform("E15.7").unwrap(),
fmt(AsciiKind::Float, 15, 7)
);
assert_eq!(
parse_ascii_tform("D25.17").unwrap(),
fmt(AsciiKind::Float, 25, 17)
);
assert!(parse_ascii_tform("Z3").is_err());
}
#[test]
fn decodes_hand_built_ascii_rows() {
let mut header = Header::new();
header
.set("XTENSION", "TABLE")
.set("BITPIX", 8)
.set("NAXIS", 2)
.set("NAXIS1", 10)
.set("NAXIS2", 2)
.set("PCOUNT", 0)
.set("GCOUNT", 1)
.set("TFIELDS", 2)
.set("TBCOL1", 1)
.set("TFORM1", "A4")
.set("TTYPE1", "NAME")
.set("TBCOL2", 5)
.set("TFORM2", "I6")
.set("TTYPE2", "COUNT");
let data = b"abc 123def -45".to_vec(); let table = AsciiTable::from_data(&header, data).unwrap();
assert_eq!(table.nrows, 2);
assert_eq!(table.columns[1].start, 4);
assert_eq!(
table.column_by_idx(0).unwrap().raw().unwrap(),
ColumnData::Text(vec!["abc".into(), "def".into()])
);
assert_eq!(
table.column_by_idx(1).unwrap().raw().unwrap(),
ColumnData::I64(vec![123, -45])
);
assert_eq!(
table.column_by_name("count").unwrap().raw().unwrap(),
ColumnData::I64(vec![123, -45])
);
assert_eq!(
table.column_by_name("COUNT").unwrap().physical().unwrap(),
vec![123.0, -45.0]
);
assert!(matches!(
table.column_by_name("missing"),
Err(FitsError::ColumnNotFound { .. })
));
}
#[test]
fn applies_tscal_tzero_and_maps_tnull_to_nan() {
let mut header = Header::new();
header
.set("XTENSION", "TABLE")
.set("BITPIX", 8)
.set("NAXIS", 2)
.set("NAXIS1", 6)
.set("NAXIS2", 2)
.set("PCOUNT", 0)
.set("GCOUNT", 1)
.set("TFIELDS", 1)
.set("TBCOL1", 1)
.set("TFORM1", "I6")
.set("TSCAL1", 2.0)
.set("TZERO1", 10.0)
.set("TNULL1", "***");
let data = b" 123 ***".to_vec();
let table = AsciiTable::from_data(&header, data).unwrap();
assert_eq!(
table.column_by_idx(0).unwrap().raw().unwrap(),
ColumnData::I64(vec![123, 0])
);
let phys = table.column_by_idx(0).unwrap().physical().unwrap();
assert_eq!(phys[0], 256.0); assert!(phys[1].is_nan());
}
#[test]
fn implicit_decimal_point_scales_by_ten_to_the_d() {
let mut header = Header::new();
header
.set("XTENSION", "TABLE")
.set("BITPIX", 8)
.set("NAXIS", 2)
.set("NAXIS1", 8)
.set("NAXIS2", 2)
.set("PCOUNT", 0)
.set("GCOUNT", 1)
.set("TFIELDS", 1)
.set("TBCOL1", 1)
.set("TFORM1", "F8.3");
let data = b" 12345 12.345".to_vec(); let table = AsciiTable::from_data(&header, data).unwrap();
assert_eq!(
table.column_by_idx(0).unwrap().raw().unwrap(),
ColumnData::F64(vec![12.345, 12.345])
);
}
#[test]
fn ascii_column_index_is_case_insensitive() {
let mut header = Header::new();
header
.set("XTENSION", "TABLE")
.set("BITPIX", 8)
.set("NAXIS", 2)
.set("NAXIS1", 4)
.set("NAXIS2", 1)
.set("PCOUNT", 0)
.set("GCOUNT", 1)
.set("TFIELDS", 1)
.set("TBCOL1", 1)
.set("TFORM1", "I4")
.set("TTYPE1", "Count");
let table = AsciiTable::from_data(&header, b" 7".to_vec()).unwrap();
assert_eq!(table.column_index("COUNT"), Some(0));
assert_eq!(table.column_index("count"), Some(0));
}
#[test]
fn ascii_table_round_trips_through_write_and_read() {
let columns = vec![
AsciiWriteColumn {
name: "NAME".into(),
unit: None,
data: ColumnData::Text(vec!["alpha".into(), "beta".into()]),
width: 6,
decimals: 0,
tscale: None,
tzero: None,
tnull: None,
},
AsciiWriteColumn {
name: "N".into(),
unit: Some("count".into()),
data: ColumnData::I64(vec![7, -3]),
width: 5,
decimals: 0,
tscale: None,
tzero: None,
tnull: None,
},
AsciiWriteColumn {
name: "X".into(),
unit: None,
data: ColumnData::F64(vec![1.5, -2.25]),
width: 8,
decimals: 2,
tscale: None,
tzero: None,
tnull: None,
},
];
let mut w = FitsWriter::new(Cursor::new(Vec::new()));
w.write_ascii_table(2, &columns).unwrap();
let mut r = FitsReader::open(Cursor::new(w.into_inner().into_inner())).unwrap();
assert_eq!(r.hdus.len(), 2); assert_eq!(r.hdus[1].kind, crate::HduKind::AsciiTable);
let t = r.read_ascii_table(1).unwrap();
assert_eq!(
t.column_by_idx(0).unwrap().raw().unwrap(),
ColumnData::Text(vec!["alpha".into(), "beta".into()])
);
assert_eq!(
t.column_by_idx(1).unwrap().raw().unwrap(),
ColumnData::I64(vec![7, -3])
);
assert_eq!(
t.column_by_idx(2).unwrap().raw().unwrap(),
ColumnData::F64(vec![1.5, -2.25])
);
}
#[test]
fn signed_exponent_without_letter_parses_as_fortran_real() {
let approx = |got: Option<f64>, want: f64| {
let g = got.expect("should parse");
assert!((g - want).abs() < 1e-12, "got {g}, want {want}");
};
approx(parse_ascii_float("3.14159-2", 5), 0.0314159);
approx(parse_ascii_float("2.5+3", 1), 2500.0);
approx(parse_ascii_float("-3.0-1", 1), -0.3);
approx(parse_ascii_float("-12", 3), -0.012);
approx(parse_ascii_float("1.5E2", 1), 150.0);
approx(parse_ascii_float("1.5D-2", 1), 0.015);
assert_eq!(
split_mantissa_exponent("3.14159-2"),
Some(("3.14159", "-2"))
);
assert_eq!(split_mantissa_exponent("-3.0-1"), Some(("-3.0", "-1")));
assert_eq!(split_mantissa_exponent("1.5E2"), Some(("1.5", "2")));
assert_eq!(split_mantissa_exponent("1.5D-2"), Some(("1.5", "-2")));
assert_eq!(split_mantissa_exponent("123"), None);
}
#[test]
fn reads_a_column_with_a_bare_sign_exponent_field() {
let mut header = Header::new();
header
.set("XTENSION", "TABLE")
.set("BITPIX", 8)
.set("NAXIS", 2)
.set("NAXIS1", 12)
.set("NAXIS2", 1)
.set("PCOUNT", 0)
.set("GCOUNT", 1)
.set("TFIELDS", 1)
.set("TBCOL1", 1)
.set("TFORM1", "E12.5");
let data = b" 3.14159-2".to_vec(); let table = AsciiTable::from_data(&header, data).unwrap();
match table.column_by_idx(0).unwrap().raw().unwrap() {
ColumnData::F64(v) => assert!((v[0] - 0.0314159).abs() < 1e-12, "{}", v[0]),
other => panic!("expected F64, got {other:?}"),
}
}
#[test]
fn ascii_write_emits_tscal_tzero_tnull_and_round_trips() {
let columns = vec![
AsciiWriteColumn {
name: "RAW".into(),
unit: None,
data: ColumnData::I64(vec![5, 10]),
width: 6,
decimals: 0,
tscale: Some(2.0),
tzero: Some(100.0),
tnull: None,
},
AsciiWriteColumn {
name: "FLUX".into(),
unit: None,
data: ColumnData::F64(vec![1.5, f64::NAN]),
width: 10,
decimals: 3,
tscale: None,
tzero: None,
tnull: Some("NULL".into()),
},
];
let mut w = FitsWriter::new(Cursor::new(Vec::new()));
w.write_ascii_table(2, &columns).unwrap();
let mut r = FitsReader::open(Cursor::new(w.into_inner().into_inner())).unwrap();
assert_eq!(r.hdus[1].header.get_real("TSCAL1"), Some(2.0));
assert_eq!(r.hdus[1].header.get_real("TZERO1"), Some(100.0));
assert_eq!(r.hdus[1].header.get_text("TNULL2"), Some("NULL"));
let t = r.read_ascii_table(1).unwrap();
assert_eq!(
t.column_by_idx(0).unwrap().raw().unwrap(),
ColumnData::I64(vec![5, 10])
);
assert_eq!(
t.column_by_idx(0).unwrap().physical().unwrap(),
vec![110.0, 120.0]
);
let flux = t.column_by_idx(1).unwrap().physical().unwrap();
assert_eq!(flux[0], 1.5);
assert!(flux[1].is_nan());
}
#[test]
fn ascii_tfields_beyond_999_is_rejected() {
let mut header = Header::new();
header
.set("XTENSION", "TABLE")
.set("BITPIX", 8)
.set("NAXIS", 2)
.set("NAXIS1", 0)
.set("NAXIS2", 0)
.set("PCOUNT", 0)
.set("GCOUNT", 1)
.set("TFIELDS", 1000);
assert!(matches!(
AsciiTable::from_data(&header, vec![]),
Err(FitsError::KeywordOutOfRange { name: "TFIELDS" })
));
}
#[test]
fn ascii_row_count_times_width_overflow_is_rejected() {
let mut header = Header::new();
header
.set("XTENSION", "TABLE")
.set("BITPIX", 8)
.set("NAXIS", 2)
.set("NAXIS1", 8)
.set("NAXIS2", 3_000_000_000_000_000_000i64)
.set("PCOUNT", 0)
.set("GCOUNT", 1)
.set("TFIELDS", 1)
.set("TBCOL1", 1)
.set("TFORM1", "I8");
assert!(matches!(
AsciiTable::from_data(&header, vec![0u8; 8]),
Err(FitsError::UnexpectedEof)
));
}