use std::io::Write;
use indexmap::IndexMap;
use super::layout::{CaseLayout, WriteVariable};
use crate::constants::*;
use crate::error::Result;
use crate::io_utils::{self, SavWriteExt};
use crate::metadata::{MissingSpec, MrType, SpssMetadata, Value};
use crate::variable::MissingValues;
pub(super) fn current_date_time() -> (String, String) {
use std::time::{SystemTime, UNIX_EPOCH};
let secs = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_secs();
let secs_per_day = 86400u64;
let time_of_day = secs % secs_per_day;
let hh = time_of_day / 3600;
let mm = (time_of_day % 3600) / 60;
let ss = time_of_day % 60;
let mut days = (secs / secs_per_day) as i64;
let mut year = 1970i32;
loop {
let days_in_year = if is_leap(year) { 366 } else { 365 };
if days < days_in_year {
break;
}
days -= days_in_year;
year += 1;
}
let months_days: [i64; 12] = if is_leap(year) {
[31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
} else {
[31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
};
let mut month = 0usize;
for (i, &md) in months_days.iter().enumerate() {
if days < md {
month = i;
break;
}
days -= md;
}
let day = days + 1;
const MONTH_ABBR: [&str; 12] = [
"Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec",
];
let yy = year % 100;
let date = format!("{:02} {} {:02}", day, MONTH_ABBR[month], yy);
let time = format!("{:02}:{:02}:{:02}", hh, mm, ss);
(date, time)
}
fn is_leap(y: i32) -> bool {
(y % 4 == 0 && y % 100 != 0) || y % 400 == 0
}
pub(super) fn write_header<W: Write>(
w: &mut W,
layout: &CaseLayout,
meta: &SpssMetadata,
compression: Compression,
nrows: i32,
) -> Result<()> {
let magic = if compression == Compression::Zlib {
b"$FL3"
} else {
b"$FL2"
};
w.write_all(magic)?;
let product = format!("@(#) SPSS DATA FILE ambers {}", env!("CARGO_PKG_VERSION"));
w.write_fixed_string(&product, 60)?;
w.write_i32_le(2)?;
w.write_i32_le(layout.slots_per_row as i32)?;
w.write_i32_le(compression.to_i32())?;
let weight_index = meta
.weight_variable
.as_ref()
.and_then(|wv| {
layout
.write_vars
.iter()
.scan(0usize, |slot, var| {
let this_slot = *slot;
*slot += var.total_slots();
Some((var, this_slot))
})
.find(|(v, _)| v.long_name == *wv)
.map(|(_, slot)| (slot + 1) as i32) })
.unwrap_or(0);
w.write_i32_le(weight_index)?;
w.write_i32_le(nrows)?;
w.write_f64_le(DEFAULT_BIAS)?;
let (date_now, time_now) = current_date_time();
if let Some((date_part, time_part)) =
crate::metadata::parse_iso_to_spss_parts(&meta.creation_time)
{
w.write_fixed_string(&date_part, 9)?;
w.write_fixed_string(&time_part, 8)?;
} else {
w.write_fixed_string(&date_now, 9)?;
w.write_fixed_string(&time_now, 8)?;
}
w.write_fixed_string(&meta.file_label, 64)?;
w.write_zero_padding(3)?;
Ok(())
}
pub(super) fn write_variable_records<W: Write>(w: &mut W, layout: &CaseLayout) -> Result<()> {
for rec in &layout.slot_records {
w.write_i32_le(RECORD_TYPE_VARIABLE)?;
w.write_i32_le(rec.raw_type)?;
let has_label = if rec.label.is_some() && !rec.is_ghost {
1
} else {
0
};
w.write_i32_le(has_label)?;
let n_missing = missing_count(&rec.missing_values);
w.write_i32_le(n_missing)?;
if rec.is_ghost {
w.write_i32_le(0)?;
w.write_i32_le(0)?;
} else {
w.write_i32_le(rec.print_format.to_packed())?;
w.write_i32_le(rec.write_format.to_packed())?;
}
if rec.is_ghost {
w.write_all(&[b' '; 8])?;
} else {
w.write_fixed_string(&rec.short_name, 8)?;
}
if has_label == 1 {
let label_bytes = rec.label.as_ref().unwrap();
let label_len = label_bytes.len();
w.write_i32_le(label_len as i32)?;
w.write_all(label_bytes)?;
let padded_len = io_utils::round_up(label_len, 4);
if padded_len > label_len {
w.write_zero_padding(padded_len - label_len)?;
}
}
write_missing_values(w, &rec.missing_values)?;
}
Ok(())
}
fn missing_count(mv: &MissingValues) -> i32 {
match mv {
MissingValues::None => 0,
MissingValues::DiscreteNumeric(vals) => vals.len() as i32,
MissingValues::Range { .. } => -2,
MissingValues::RangeAndValue { .. } => -3,
MissingValues::DiscreteString(vals) => vals.len() as i32,
}
}
fn write_missing_values<W: Write>(w: &mut W, mv: &MissingValues) -> Result<()> {
match mv {
MissingValues::None => {}
MissingValues::DiscreteNumeric(vals) => {
for &v in vals {
w.write_f64_le(v)?;
}
}
MissingValues::Range { low, high } => {
w.write_f64_le(*low)?;
w.write_f64_le(*high)?;
}
MissingValues::RangeAndValue { low, high, value } => {
w.write_f64_le(*low)?;
w.write_f64_le(*high)?;
w.write_f64_le(*value)?;
}
MissingValues::DiscreteString(vals) => {
for v in vals {
let mut buf = [b' '; 8];
let len = v.len().min(8);
buf[..len].copy_from_slice(&v[..len]);
w.write_all(&buf)?;
}
}
}
Ok(())
}
pub(super) fn write_value_label_records<W: Write>(
w: &mut W,
layout: &CaseLayout,
meta: &SpssMetadata,
) -> Result<()> {
let mut var_slot_indices: Vec<(usize, &WriteVariable)> = Vec::new();
let mut slot_idx = 0usize;
for var in &layout.write_vars {
var_slot_indices.push((slot_idx, var));
slot_idx += var.total_slots();
}
for &(slot, var) in &var_slot_indices {
let labels = match meta.variable_value_labels.get(&var.long_name) {
Some(labels) if !labels.is_empty() => labels,
_ => continue,
};
let is_long_string = matches!(&var.var_type, VarType::String(w) if *w > 8);
if is_long_string {
continue;
}
w.write_i32_le(RECORD_TYPE_VALUE_LABEL)?;
w.write_i32_le(labels.len() as i32)?;
for (val, label) in labels {
match val {
Value::Numeric(v) => {
w.write_f64_le(*v)?;
}
Value::String(s) => {
let mut buf = [b' '; 8];
let bytes = s.as_bytes();
let len = bytes.len().min(8);
buf[..len].copy_from_slice(&bytes[..len]);
w.write_all(&buf)?;
}
}
let label_bytes = label.as_bytes();
let label_len = label_bytes.len().min(120); w.write_all(&[label_len as u8])?;
w.write_all(&label_bytes[..label_len])?;
let total = 1 + label_len;
let padded = io_utils::round_up(total, 8);
if padded > total {
w.write_zero_padding(padded - total)?;
}
}
w.write_i32_le(RECORD_TYPE_VALUE_LABEL_VARS)?;
w.write_i32_le(1)?; w.write_i32_le((slot + 1) as i32)?; }
Ok(())
}
pub(super) fn write_document_record<W: Write>(w: &mut W, meta: &SpssMetadata) -> Result<()> {
if meta.notes.is_empty() {
return Ok(());
}
let mut lines: Vec<String> = Vec::new();
for note in &meta.notes {
lines.push(note.clone());
}
w.write_i32_le(RECORD_TYPE_DOCUMENT)?;
w.write_i32_le(lines.len() as i32)?;
for line in &lines {
w.write_fixed_string(line, 80)?;
}
Ok(())
}
fn write_info_record_header<W: Write>(
w: &mut W,
subtype: i32,
elem_size: i32,
count: i32,
) -> Result<()> {
w.write_i32_le(RECORD_TYPE_INFO)?;
w.write_i32_le(subtype)?;
w.write_i32_le(elem_size)?;
w.write_i32_le(count)?;
Ok(())
}
pub(super) fn write_info_integer<W: Write>(w: &mut W, _compression: Compression) -> Result<()> {
write_info_record_header(w, INFO_INTEGER, 4, 8)?;
w.write_i32_le(28)?; w.write_i32_le(0)?; w.write_i32_le(0)?; w.write_i32_le(-1)?; w.write_i32_le(1)?; w.write_i32_le(1)?; w.write_i32_le(2)?; w.write_i32_le(65001)?; Ok(())
}
pub(super) fn write_info_float<W: Write>(w: &mut W) -> Result<()> {
write_info_record_header(w, INFO_FLOAT, 8, 3)?;
w.write_f64_le(f64::from_bits(SYSMIS_BITS))?;
w.write_f64_le(f64::from_bits(HIGHEST_BITS))?;
w.write_f64_le(f64::from_bits(LOWEST_BITS))?;
Ok(())
}
pub(super) fn write_info_var_display<W: Write>(w: &mut W, layout: &CaseLayout) -> Result<()> {
let n_vars = layout.slot_records.iter().filter(|r| !r.is_ghost).count();
write_info_record_header(w, INFO_VAR_DISPLAY, 4, (n_vars * 3) as i32)?;
let mut var_idx = 0;
for rec in &layout.slot_records {
if rec.is_ghost {
continue;
}
let (measure, display_width, alignment) = if var_idx < layout.write_vars.len() {
let var = &layout.write_vars[var_idx];
if rec.short_name == var.short_name {
var_idx += 1;
(var.measure, var.display_width, var.alignment)
} else {
(Measure::Unknown, 255, Alignment::Left)
}
} else {
(Measure::Unknown, 8, Alignment::Left)
};
w.write_i32_le(measure.to_i32())?;
w.write_i32_le(display_width as i32)?;
w.write_i32_le(alignment.to_i32())?;
}
Ok(())
}
pub(super) fn write_info_long_names<W: Write>(w: &mut W, layout: &CaseLayout) -> Result<()> {
let mut payload = String::new();
for (short, long) in &layout.short_to_long {
if !payload.is_empty() {
payload.push('\t');
}
payload.push_str(short);
payload.push('=');
payload.push_str(long);
}
if payload.is_empty() {
return Ok(());
}
write_info_record_header(w, INFO_LONG_NAMES, 1, payload.len() as i32)?;
w.write_all(payload.as_bytes())?;
Ok(())
}
pub(super) fn write_info_very_long_strings<W: Write>(w: &mut W, layout: &CaseLayout) -> Result<()> {
if layout.very_long_strings.is_empty() {
return Ok(());
}
let mut payload = String::new();
for (short_name, width) in &layout.very_long_strings {
payload.push_str(short_name);
payload.push('=');
payload.push_str(&format!("{:05}", width));
payload.push('\0');
payload.push('\t');
}
write_info_record_header(w, INFO_VERY_LONG_STRINGS, 1, payload.len() as i32)?;
w.write_all(payload.as_bytes())?;
Ok(())
}
pub(super) fn write_info_encoding<W: Write>(w: &mut W) -> Result<()> {
let encoding = b"UTF-8";
write_info_record_header(w, INFO_ENCODING, 1, encoding.len() as i32)?;
w.write_all(encoding)?;
Ok(())
}
pub(super) fn write_info_long_string_labels<W: Write>(
w: &mut W,
layout: &CaseLayout,
meta: &SpssMetadata,
) -> Result<()> {
let mut entries: Vec<(&str, usize, &IndexMap<Value, String>)> = Vec::new();
for var in &layout.write_vars {
if matches!(&var.var_type, VarType::String(w) if *w > 8)
&& let Some(labels) = meta.variable_value_labels.get(&var.long_name)
&& !labels.is_empty()
{
entries.push((&var.long_name, var.storage_width, labels));
}
}
if entries.is_empty() {
return Ok(());
}
let mut payload = Vec::new();
for (var_name, var_width, labels) in &entries {
let name_bytes = var_name.as_bytes();
payload.extend_from_slice(&(name_bytes.len() as i32).to_le_bytes());
payload.extend_from_slice(name_bytes);
payload.extend_from_slice(&(*var_width as i32).to_le_bytes());
payload.extend_from_slice(&(labels.len() as i32).to_le_bytes());
for (val, label) in *labels {
let val_str = match val {
Value::String(s) => s.clone(),
Value::Numeric(n) => n.to_string(),
};
let val_bytes = val_str.as_bytes();
let padded_len = *var_width;
payload.extend_from_slice(&(padded_len as i32).to_le_bytes());
let copy_len = val_bytes.len().min(padded_len);
payload.extend_from_slice(&val_bytes[..copy_len]);
if copy_len < padded_len {
payload.extend(std::iter::repeat_n(b' ', padded_len - copy_len));
}
let label_bytes = label.as_bytes();
payload.extend_from_slice(&(label_bytes.len() as i32).to_le_bytes());
payload.extend_from_slice(label_bytes);
}
}
write_info_record_header(w, INFO_LONG_STRING_LABELS, 1, payload.len() as i32)?;
w.write_all(&payload)?;
Ok(())
}
pub(super) fn write_info_long_string_missing<W: Write>(
w: &mut W,
layout: &CaseLayout,
meta: &SpssMetadata,
) -> Result<()> {
let mut entries: Vec<(&str, &Vec<MissingSpec>)> = Vec::new();
for var in &layout.write_vars {
if matches!(&var.var_type, VarType::String(w) if *w > 8)
&& let Some(specs) = meta.variable_missing_values.get(&var.long_name)
&& !specs.is_empty()
{
entries.push((&var.short_name, specs));
}
}
if entries.is_empty() {
return Ok(());
}
let mut payload = Vec::new();
for (var_name, specs) in &entries {
let name_bytes = var_name.as_bytes();
payload.extend_from_slice(&(name_bytes.len() as i32).to_le_bytes());
payload.extend_from_slice(name_bytes);
let n_missing: u8 = specs.len() as u8;
payload.push(n_missing);
payload.extend_from_slice(&8_i32.to_le_bytes());
for spec in *specs {
if let MissingSpec::StringValue(s) = spec {
let val_bytes = s.as_bytes();
let mut buf = [b' '; 8];
let len = val_bytes.len().min(8);
buf[..len].copy_from_slice(&val_bytes[..len]);
payload.extend_from_slice(&buf);
}
}
}
write_info_record_header(w, INFO_LONG_STRING_MISSING, 1, payload.len() as i32)?;
w.write_all(&payload)?;
Ok(())
}
pub(super) fn write_info_mr_sets<W: Write>(
w: &mut W,
meta: &SpssMetadata,
layout: &CaseLayout,
) -> Result<()> {
if meta.mr_sets.is_empty() {
return Ok(());
}
let long_to_short: IndexMap<String, String> = layout
.short_to_long
.iter()
.map(|(short, long)| (long.clone(), short.clone()))
.collect();
let mut payload = String::new();
for (name, mr) in &meta.mr_sets {
payload.push('$');
payload.push_str(name);
payload.push('=');
match mr.mr_type {
MrType::MultipleDichotomy => {
payload.push('D');
if let Some(ref cv) = mr.counted_value {
payload.push_str(&cv.len().to_string());
payload.push(' ');
payload.push_str(cv);
} else {
payload.push_str("1 1");
}
}
MrType::MultipleCategory => {
payload.push('C');
}
}
payload.push(' ');
let label_bytes = mr.label.len();
payload.push_str(&label_bytes.to_string());
payload.push(' ');
payload.push_str(&mr.label);
for var_long in &mr.variables {
let short = long_to_short.get(var_long).unwrap_or(var_long);
payload.push(' ');
payload.push_str(short);
}
payload.push('\n');
}
write_info_record_header(w, INFO_MR_SETS, 1, payload.len() as i32)?;
w.write_all(payload.as_bytes())?;
Ok(())
}
pub(super) fn write_info_mr_sets_v2<W: Write>(w: &mut W, meta: &SpssMetadata) -> Result<()> {
if meta.mr_sets.is_empty() {
return Ok(());
}
let mut payload = String::new();
for (name, mr) in &meta.mr_sets {
payload.push('$');
payload.push_str(name);
payload.push('=');
match mr.mr_type {
MrType::MultipleDichotomy => {
payload.push('D');
if let Some(ref cv) = mr.counted_value {
payload.push_str(&cv.len().to_string());
payload.push(' ');
payload.push_str(cv);
} else {
payload.push_str("1 1");
}
}
MrType::MultipleCategory => {
payload.push('C');
}
}
payload.push(' ');
let label_bytes = mr.label.len();
payload.push_str(&label_bytes.to_string());
payload.push(' ');
payload.push_str(&mr.label);
for var in &mr.variables {
payload.push(' ');
payload.push_str(var);
}
payload.push('\n');
}
write_info_record_header(w, INFO_MR_SETS_V2, 1, payload.len() as i32)?;
w.write_all(payload.as_bytes())?;
Ok(())
}
pub(super) fn write_info_var_attributes<W: Write>(w: &mut W, meta: &SpssMetadata) -> Result<()> {
if meta.variable_roles.is_empty() && meta.variable_attributes.is_empty() {
return Ok(());
}
let mut all_vars: IndexMap<&str, ()> = IndexMap::new();
for name in meta.variable_roles.keys() {
all_vars.insert(name.as_str(), ());
}
for name in meta.variable_attributes.keys() {
all_vars.insert(name.as_str(), ());
}
let mut payload = String::new();
for (long_name, _) in &all_vars {
if !payload.is_empty() {
payload.push('/');
}
payload.push_str(long_name);
payload.push(':');
if let Some(attrs) = meta.variable_attributes.get(*long_name) {
for (attr_name, values) in attrs {
if values.is_empty() {
continue; }
payload.push_str(attr_name);
payload.push('(');
for val in values {
payload.push('\'');
payload.push_str(val);
payload.push_str("'\n");
}
payload.push(')');
}
}
if let Some(role) = meta.variable_roles.get(*long_name) {
payload.push_str("$@Role('");
payload.push_str(role.to_code());
payload.push_str("'\n)");
}
}
write_info_record_header(w, INFO_VAR_ATTRIBUTES, 1, payload.len() as i32)?;
w.write_all(payload.as_bytes())?;
Ok(())
}
pub(super) fn write_dict_termination<W: Write>(w: &mut W) -> Result<()> {
w.write_i32_le(RECORD_TYPE_DICT_TERMINATION)?;
w.write_i32_le(0)?; Ok(())
}