#[must_use]
pub fn encode_ibm_float(value: Option<f64>) -> [u8; 8] {
match value {
None => MISSING_PATTERN,
Some(v) if v.is_nan() => MISSING_PATTERN,
Some(v) => ieee_to_ibm(v),
}
}
#[must_use]
pub fn decode_ibm_float(bytes: &[u8; 8]) -> Option<f64> {
if is_missing_value(bytes) {
return None;
}
Some(ibm_to_ieee(bytes))
}
#[must_use]
pub fn is_missing_value(bytes: &[u8; 8]) -> bool {
let first_byte = bytes[0];
let is_missing_marker = first_byte == 0x2E || first_byte == 0x5F || (0x41..=0x5A).contains(&first_byte);
if is_missing_marker {
bytes[1..].iter().all(|&b| b == 0)
} else {
false
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum SasMissingValue {
Standard,
Special(char),
Underscore,
}
impl SasMissingValue {
#[must_use]
pub const fn to_bytes(self) -> [u8; 8] {
let first_byte = match self {
Self::Standard => 0x2E, Self::Underscore => 0x5F, Self::Special(c) => c as u8, };
[first_byte, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]
}
}
impl std::fmt::Display for SasMissingValue {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Standard => write!(f, "."),
Self::Special(c) => write!(f, ".{}", c),
Self::Underscore => write!(f, "._"),
}
}
}
#[must_use]
pub fn identify_missing_value(bytes: &[u8; 8]) -> Option<SasMissingValue> {
if !is_missing_value(bytes) {
return None;
}
let first_byte = bytes[0];
Some(match first_byte {
0x2E => SasMissingValue::Standard,
0x5F => SasMissingValue::Underscore,
b'A'..=b'Z' => SasMissingValue::Special(first_byte as char),
_ => return None,
})
}
#[must_use]
pub const fn encode_missing_value(missing: SasMissingValue) -> [u8; 8] {
missing.to_bytes()
}
pub mod missing_patterns {
use super::SasMissingValue;
pub const MISSING: [u8; 8] = SasMissingValue::Standard.to_bytes();
pub const MISSING_A: [u8; 8] = SasMissingValue::Special('A').to_bytes();
pub const MISSING_B: [u8; 8] = SasMissingValue::Special('B').to_bytes();
pub const MISSING_C: [u8; 8] = SasMissingValue::Special('C').to_bytes();
pub const MISSING_UNDERSCORE: [u8; 8] = SasMissingValue::Underscore.to_bytes();
}
const MISSING_PATTERN: [u8; 8] = [0x2E, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00];
fn ieee_to_ibm(value: f64) -> [u8; 8] {
if value == 0.0 {
return [0u8; 8];
}
let bits = value.to_bits();
let sign = ((bits >> 63) & 1) as u8;
let ieee_exp = ((bits >> 52) & 0x7FF) as i32;
let ieee_mant = bits & 0x000F_FFFF_FFFF_FFFF;
if ieee_exp == 0x7FF {
return MISSING_PATTERN;
}
let (exp_adj, mant_with_hidden) = if ieee_exp == 0 {
let leading = 52 - ieee_mant.leading_zeros() as i32;
if leading <= 0 {
return [0u8; 8];
}
(-1022 - 52 + leading, ieee_mant << (52 - leading + 1))
} else {
(ieee_exp - 1023, ieee_mant | 0x0010_0000_0000_0000)
};
let mut mant = mant_with_hidden << 3; let mut exp2 = exp_adj + 1;
let shift = ((4 - (exp2 & 3)) & 3) as u32;
if shift > 0 {
mant >>= shift;
exp2 += shift as i32;
}
let ibm_exp = (exp2 / 4) + 64;
if ibm_exp > 127 {
let mut result = [0xFFu8; 8];
result[0] = (sign << 7) | 0x7F;
return result;
}
if ibm_exp < 0 {
return [0u8; 8];
}
let mut final_exp = ibm_exp;
while (mant >> 52) == 0 && final_exp > 0 {
mant <<= 4;
final_exp -= 1;
}
if final_exp <= 0 || mant == 0 {
return [0u8; 8];
}
let mut result = [0u8; 8];
result[0] = (sign << 7) | (final_exp as u8 & 0x7F);
let mant_bytes = mant.to_be_bytes();
result[1..8].copy_from_slice(&mant_bytes[1..8]);
result
}
fn ibm_to_ieee(bytes: &[u8; 8]) -> f64 {
if bytes.iter().all(|&b| b == 0) {
return 0.0;
}
let sign = (bytes[0] >> 7) & 1;
let ibm_exp = (bytes[0] & 0x7F) as i32;
let mut mant: u64 = 0;
for &b in &bytes[1..8] {
mant = (mant << 8) | u64::from(b);
}
if mant == 0 {
return 0.0;
}
let exp2 = 4 * (ibm_exp - 64);
let leading_zeros = mant.leading_zeros();
let bit_pos = 63 - leading_zeros as i32; let shift_needed = bit_pos - 52;
let ieee_mant = if shift_needed > 0 {
(mant >> shift_needed as u32) & 0x000F_FFFF_FFFF_FFFF
} else if shift_needed < 0 {
(mant << (-shift_needed) as u32) & 0x000F_FFFF_FFFF_FFFF
} else {
mant & 0x000F_FFFF_FFFF_FFFF
};
let ieee_exp_raw = exp2 + bit_pos - 56 + 1023;
if ieee_exp_raw >= 2047 {
return if sign == 1 {
f64::NEG_INFINITY
} else {
f64::INFINITY
};
}
if ieee_exp_raw <= 0 {
return 0.0;
}
let bits = (u64::from(sign) << 63) | ((ieee_exp_raw as u64) << 52) | ieee_mant;
f64::from_bits(bits)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_zero() {
let encoded = encode_ibm_float(Some(0.0));
assert_eq!(encoded, [0u8; 8]);
let decoded = decode_ibm_float(&[0u8; 8]);
assert_eq!(decoded, Some(0.0));
}
#[test]
fn test_missing() {
let encoded = encode_ibm_float(None);
assert!(is_missing_value(&encoded));
let decoded = decode_ibm_float(&encoded);
assert!(decoded.is_none());
}
#[test]
fn test_roundtrip_integers() {
for &val in &[1.0, -1.0, 100.0, -100.0, 12345.0] {
let encoded = encode_ibm_float(Some(val));
let decoded = decode_ibm_float(&encoded).unwrap();
let rel_error = if val != 0.0 {
((decoded - val) / val).abs()
} else {
(decoded - val).abs()
};
assert!(
rel_error < 1e-10,
"Failed for {}: got {} (rel error: {})",
val,
decoded,
rel_error
);
}
}
#[test]
fn test_roundtrip_fractions() {
use std::f64::consts::{E, PI};
for &val in &[0.5, 0.25, 0.125, PI, E] {
let encoded = encode_ibm_float(Some(val));
let decoded = decode_ibm_float(&encoded).unwrap();
let rel_error = ((decoded - val) / val).abs();
assert!(
rel_error < 1e-14,
"Failed for {}: got {} (rel error: {})",
val,
decoded,
rel_error
);
}
}
#[test]
fn test_nan_becomes_missing() {
let encoded = encode_ibm_float(Some(f64::NAN));
assert!(is_missing_value(&encoded));
}
}