use std::io::Read;
use crate::constants::{Alignment, Measure, SpssFormat, VarType};
use crate::error::Result;
use crate::io_utils::{self, SavReader};
#[derive(Debug, Clone)]
pub enum MissingValues {
None,
DiscreteNumeric(Vec<f64>),
Range {
low: f64,
high: f64,
},
RangeAndValue {
low: f64,
high: f64,
value: f64,
},
DiscreteString(Vec<Vec<u8>>),
}
#[derive(Debug, Clone)]
#[allow(dead_code)]
pub struct VariableRecord {
pub slot_index: usize,
pub raw_type: i32,
pub short_name: String,
pub long_name: String,
pub label: Option<Vec<u8>>,
pub print_format: Option<SpssFormat>,
pub write_format: Option<SpssFormat>,
pub missing_values: MissingValues,
pub var_type: VarType,
pub is_ghost: bool,
pub measure: Measure,
pub display_width: u32,
pub alignment: Alignment,
pub n_segments: usize,
}
impl VariableRecord {
pub fn parse<R: Read>(reader: &mut SavReader<R>, slot_index: usize) -> Result<VariableRecord> {
let raw_type = reader.read_i32()?;
let has_var_label = reader.read_i32()?;
let n_missing_values = reader.read_i32()?;
let print_packed = reader.read_i32()?;
let write_packed = reader.read_i32()?;
let name_bytes = reader.read_bytes(8)?;
let short_name =
io_utils::bytes_to_string_lossy(io_utils::trim_trailing_padding(&name_bytes));
let (var_type, is_ghost) = match raw_type {
0 => (VarType::Numeric, false),
t if t > 0 => (VarType::String(t as usize), false),
-1 => (VarType::Numeric, true), _ => (VarType::Numeric, true), };
let label = if has_var_label == 1 {
let label_len = reader.read_i32()? as usize;
let padded_len = io_utils::round_up(label_len, 4);
let label_bytes = reader.read_bytes(padded_len)?;
Some(label_bytes[..label_len].to_vec())
} else {
None
};
let missing_values = parse_missing_values(reader, n_missing_values, &var_type)?;
let print_format = SpssFormat::from_packed(print_packed);
let write_format = SpssFormat::from_packed(write_packed);
let display_width = print_format.as_ref().map_or(8, |f| f.width as u32);
Ok(VariableRecord {
slot_index,
raw_type,
short_name: short_name.to_uppercase(),
long_name: short_name.to_uppercase(), label,
print_format,
write_format,
missing_values,
var_type,
is_ghost,
measure: Measure::Unknown,
display_width,
alignment: Alignment::Unknown,
n_segments: 1,
})
}
#[allow(dead_code)]
pub fn n_slots(&self) -> usize {
match &self.var_type {
VarType::Numeric => 1,
VarType::String(width) => {
width.div_ceil(8)
}
}
}
}
fn parse_missing_values<R: Read>(
reader: &mut SavReader<R>,
n_missing: i32,
var_type: &VarType,
) -> Result<MissingValues> {
if n_missing == 0 {
return Ok(MissingValues::None);
}
let abs_n = n_missing.unsigned_abs() as usize;
let is_range = n_missing < 0;
match var_type {
VarType::Numeric => {
let mut values = Vec::with_capacity(abs_n);
for _ in 0..abs_n {
values.push(reader.read_f64()?);
}
if is_range {
match abs_n {
2 => Ok(MissingValues::Range {
low: values[0],
high: values[1],
}),
3 => Ok(MissingValues::RangeAndValue {
low: values[0],
high: values[1],
value: values[2],
}),
_ => Ok(MissingValues::DiscreteNumeric(values)),
}
} else {
Ok(MissingValues::DiscreteNumeric(values))
}
}
VarType::String(_) => {
let mut values = Vec::with_capacity(abs_n);
for _ in 0..abs_n {
let bytes = reader.read_8_bytes()?;
values.push(bytes.to_vec());
}
Ok(MissingValues::DiscreteString(values))
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn make_variable_bytes(var_type: i32, name: &[u8; 8], has_label: bool) -> Vec<u8> {
let mut buf = Vec::new();
buf.extend_from_slice(&var_type.to_le_bytes());
buf.extend_from_slice(&(if has_label { 1_i32 } else { 0_i32 }).to_le_bytes());
buf.extend_from_slice(&0_i32.to_le_bytes());
let print_fmt: i32 = (5 << 16) | (8 << 8) | 2;
buf.extend_from_slice(&print_fmt.to_le_bytes());
buf.extend_from_slice(&print_fmt.to_le_bytes());
buf.extend_from_slice(name);
if has_label {
let label = b"Test label";
let label_len = label.len() as i32;
buf.extend_from_slice(&label_len.to_le_bytes());
buf.extend_from_slice(label);
let padding = io_utils::round_up(label.len(), 4) - label.len();
buf.extend_from_slice(&vec![0u8; padding]);
}
buf
}
#[test]
fn test_parse_numeric_variable() {
let data = make_variable_bytes(0, b"AGE ", false);
let mut reader = SavReader::new(&data[..]);
let var = VariableRecord::parse(&mut reader, 0).unwrap();
assert_eq!(var.short_name, "AGE");
assert_eq!(var.var_type, VarType::Numeric);
assert!(!var.is_ghost);
assert!(var.label.is_none());
assert_eq!(var.print_format.as_ref().unwrap().to_spss_string(), "F8.2");
}
#[test]
fn test_parse_string_variable() {
let data = make_variable_bytes(20, b"NAME ", false);
let mut reader = SavReader::new(&data[..]);
let var = VariableRecord::parse(&mut reader, 0).unwrap();
assert_eq!(var.short_name, "NAME");
assert_eq!(var.var_type, VarType::String(20));
assert!(!var.is_ghost);
}
#[test]
fn test_parse_variable_with_label() {
let data = make_variable_bytes(0, b"SCORE ", true);
let mut reader = SavReader::new(&data[..]);
let var = VariableRecord::parse(&mut reader, 0).unwrap();
assert!(var.label.is_some());
assert_eq!(var.label.as_ref().unwrap(), b"Test label");
}
#[test]
fn test_ghost_variable() {
let data = make_variable_bytes(-1, b" ", false);
let mut reader = SavReader::new(&data[..]);
let var = VariableRecord::parse(&mut reader, 5).unwrap();
assert!(var.is_ghost);
}
}