use crate::ascii_table::{AsciiColumnFormat, AsciiFieldDefinition, AsciiTable};
use crate::bin_table::Value;
use crate::header::card::Card;
use crate::header::{Bitpix, ExtensionType, Header, TableNullValue};
pub(crate) fn encode(value: &Value, format: AsciiColumnFormat, null: Option<&str>) -> String {
let width = format.bytes_len();
let text = render(value, format, null);
match format {
AsciiColumnFormat::Character(_) => {
format!("{:<width$}", truncated(&text, width), width = width)
}
_ if text.len() > width => " ".repeat(width),
_ => format!("{:>width$}", text, width = width),
}
}
pub(crate) fn render(value: &Value, format: AsciiColumnFormat, null: Option<&str>) -> String {
match (value, format) {
(Value::Null, _) => null.unwrap_or("").to_string(),
(value, AsciiColumnFormat::Character(_)) => match value {
Value::String(text) => text.clone(),
other => display(other),
},
(value, AsciiColumnFormat::Integer(_)) => match as_integer(value) {
Some(number) => number.to_string(),
None => String::new(),
},
(value, AsciiColumnFormat::Fixed(_, decimals)) => match as_float(value) {
Some(number) => format!("{:.*}", decimals, number),
None => String::new(),
},
(value, AsciiColumnFormat::Exponential(_, decimals)) => match as_float(value) {
Some(number) => format!("{:.*E}", decimals, number),
None => String::new(),
},
(value, AsciiColumnFormat::Double(_, decimals)) => match as_float(value) {
Some(number) => format!("{:.*E}", decimals, number).replace('E', "D"),
None => String::new(),
},
}
}
fn truncated(text: &str, width: usize) -> &str {
match text.char_indices().nth(width) {
Some((index, _)) => &text[..index],
None => text,
}
}
fn display(value: &Value) -> String {
match as_integer(value) {
Some(number) => number.to_string(),
None => match as_float(value) {
Some(number) => number.to_string(),
None => String::new(),
},
}
}
fn as_integer(value: &Value) -> Option<i64> {
Some(match value {
Value::U8(values) => *values.first()? as i64,
Value::I8(values) => *values.first()? as i64,
Value::U16(values) => *values.first()? as i64,
Value::I16(values) => *values.first()? as i64,
Value::U32(values) => *values.first()? as i64,
Value::I32(values) => *values.first()? as i64,
Value::I64(values) => *values.first()?,
Value::U64(values) => *values.first()? as i64,
_ => return None,
})
}
fn as_float(value: &Value) -> Option<f64> {
match value {
Value::F32(values) => values.first().map(|value| *value as f64),
Value::F64(values) => values.first().copied(),
other => as_integer(other).map(|value| value as f64),
}
}
pub(crate) fn apply_to_header(table: &AsciiTable, header: &mut Header) {
crate::bin_table::write::clear_column_cards(header);
header.set(Card::Xtension {
value: ExtensionType::AsciiTable,
comment: None,
});
header.set(Card::Bitpix {
value: Bitpix::U8,
comment: None,
});
header.set(Card::NAxis {
value: 2,
comment: None,
});
header.set(Card::NAxisN {
index: 0,
value: table.bytes_per_row() as i64,
comment: Some("width of a row in characters".into()),
});
header.set(Card::NAxisN {
index: 1,
value: table.len() as i64,
comment: Some("number of rows".into()),
});
header.set(Card::ParameterCount {
value: 0,
comment: None,
});
header.set(Card::GroupCount {
value: 1,
comment: None,
});
header.set(Card::TableFields {
value: table.field_definitions().len() as i64,
comment: None,
});
for (index, field) in table.field_definitions().iter().enumerate() {
apply_column(header, index, field);
}
}
fn apply_column(header: &mut Header, index: usize, field: &AsciiFieldDefinition) {
header.set(Card::TableFormatN {
index,
value: String::from(field.format),
comment: None,
});
header.set(Card::TableColumnN {
index,
value: field.offset as i64 + 1,
comment: None,
});
if !field.name.is_empty() {
header.set(Card::TableTypeN {
index,
value: field.name.clone(),
comment: None,
});
}
if let Some(scale) = field.scale {
header.set(Card::TableScalingFactorN {
index,
value: scale,
comment: None,
});
}
if let Some(zero) = field.zero {
header.set(Card::TableScalingZeroPointN {
index,
value: zero,
comment: None,
});
}
if let Some(null) = &field.null {
header.set(Card::TableNullValueN {
index,
value: TableNullValue::Text(null.clone()),
comment: Some("text marking an undefined entry".into()),
});
}
}
#[cfg(test)]
mod tests {
use super::encode;
use crate::ascii_table::AsciiColumnFormat;
use crate::bin_table::Value;
#[test]
fn numbers_are_right_justified_in_their_field() {
assert_eq!(
encode(&Value::I64(vec![42]), AsciiColumnFormat::Integer(6), None),
" 42"
);
assert_eq!(
encode(
&Value::F64(vec![-12.5]),
AsciiColumnFormat::Fixed(8, 2),
None
),
" -12.50"
);
}
#[test]
fn text_is_left_justified_and_padded() {
assert_eq!(
encode(
&Value::String("M31".into()),
AsciiColumnFormat::Character(6),
None
),
"M31 "
);
}
#[test]
fn a_double_uses_the_d_exponent_marker() {
let written = encode(
&Value::F64(vec![150.0]),
AsciiColumnFormat::Double(12, 4),
None,
);
assert!(written.contains('D'), "got {written:?}");
assert!(!written.contains('E'), "got {written:?}");
}
#[test]
fn an_undefined_entry_writes_its_null_text() {
assert_eq!(
encode(&Value::Null, AsciiColumnFormat::Integer(6), Some("---")),
" ---"
);
assert_eq!(
encode(&Value::Null, AsciiColumnFormat::Integer(6), None),
" "
);
}
#[test]
fn a_number_too_wide_for_its_field_is_left_blank() {
assert_eq!(
encode(
&Value::I64(vec![1234567]),
AsciiColumnFormat::Integer(4),
None
),
" "
);
}
#[test]
fn text_too_wide_for_its_field_is_cut() {
assert_eq!(
encode(
&Value::String("Betelgeuse".into()),
AsciiColumnFormat::Character(4),
None
),
"Bete"
);
}
}