use crate::ole::binary;
use crate::ole::xls::error::{XlsError, XlsResult};
use crate::ole::xls::records::{XlsEncoding, FormulaValue};
use zerocopy::{FromBytes, LE, U16};
pub fn parse_short_string(data: &[u8], encoding: &XlsEncoding) -> XlsResult<String> {
if data.is_empty() {
return Ok(String::new());
}
let len = data[0] as usize;
if data.len() < 1 + len {
return Err(XlsError::InvalidLength {
expected: 1 + len,
found: data.len(),
});
}
let string_data = &data[1..1 + len];
encoding.decode(string_data)
}
pub fn parse_string_record(data: &[u8], encoding: &XlsEncoding) -> XlsResult<String> {
if data.len() < 3 {
return Err(XlsError::InvalidLength {
expected: 3,
found: data.len(),
});
}
let len = binary::read_u16_le_at(data, 0)? as usize;
let flags = data[2];
let high_byte = (flags & 0x01) != 0;
let offset = 3;
if data.len() < offset + len {
return Err(XlsError::InvalidLength {
expected: offset + len,
found: data.len(),
});
}
let string_data = &data[offset..offset + len];
if matches!(encoding, XlsEncoding::Utf16Le) && high_byte {
if !len.is_multiple_of(2) {
return Err(XlsError::Encoding("Invalid UTF-16 string length".to_string()));
}
let utf16_data: Vec<u16> = string_data.chunks_exact(2)
.map(|chunk| U16::<LE>::read_from_bytes(chunk)
.map(|v| v.get())
.unwrap_or(0))
.collect();
Ok(String::from_utf16(&utf16_data)
.map_err(|e| XlsError::Encoding(format!("UTF-16 decoding error: {}", e)))?)
} else {
Ok(String::from_utf8(string_data.to_vec())
.unwrap_or_else(|_| String::from_utf8_lossy(string_data).into_owned()))
}
}
#[allow(dead_code)]
pub fn parse_unicode_string(data: &[u8], encoding: &XlsEncoding) -> XlsResult<(String, usize)> {
if data.len() < 3 {
return Err(XlsError::InvalidLength {
expected: 3,
found: data.len(),
});
}
let cch = binary::read_u16_le_at(data, 0)? as usize;
let flags = data[2];
let mut offset = 3;
let high_byte = (flags & 0x01) != 0;
if (flags & 0x08) != 0 {
if data.len() < offset + 2 {
return Err(XlsError::InvalidLength {
expected: offset + 2,
found: data.len(),
});
}
let c_run = binary::read_u16_le_at(data, offset)?;
offset += 2 + 4 * c_run as usize; }
if (flags & 0x04) != 0 {
if data.len() < offset + 4 {
return Err(XlsError::InvalidLength {
expected: offset + 4,
found: data.len(),
});
}
let cb_ext_rst = binary::read_u32_le_at(data, offset)?;
offset += 4 + cb_ext_rst as usize; }
if data.len() < offset + cch {
return Err(XlsError::InvalidLength {
expected: offset + cch,
found: data.len(),
});
}
let string_data = &data[offset..offset + cch];
let consumed = offset + cch;
let string = if matches!(encoding, XlsEncoding::Utf16Le) && high_byte {
if !cch.is_multiple_of(2) {
return Err(XlsError::Encoding("Invalid UTF-16 string length".to_string()));
}
let utf16_data: Vec<u16> = string_data.chunks_exact(2)
.map(|chunk| U16::<LE>::read_from_bytes(chunk)
.map(|v| v.get())
.unwrap_or(0))
.collect();
String::from_utf16(&utf16_data)
.map_err(|e| XlsError::Encoding(format!("UTF-16 decoding error: {}", e)))?
} else {
String::from_utf8(string_data.to_vec())
.unwrap_or_else(|_| String::from_utf8_lossy(string_data).into_owned())
};
Ok((string, consumed))
}
pub fn rk_to_f64(rk: u32) -> f64 {
let d100 = (rk & 0x02) != 0;
let is_int = (rk & 0x01) != 0;
let mut value = if is_int {
let int_val = (rk >> 2) as i32;
if d100 {
if int_val % 100 != 0 {
int_val as f64 / 100.0
} else {
(int_val / 100) as f64
}
} else {
int_val as f64
}
} else {
let mut float_bits = [0u8; 8];
float_bits[0..4].copy_from_slice(&(rk & 0xFFFFFFFC).to_le_bytes());
float_bits[7] = 0x3C; f64::from_le_bytes(float_bits)
};
if d100 && !is_int {
value /= 100.0;
}
value
}
pub fn parse_formula_value(data: &[u8]) -> XlsResult<FormulaValue> {
if data.len() < 8 {
return Err(XlsError::InvalidLength {
expected: 8,
found: data.len(),
});
}
match data[6] {
0x00 => {
Ok(FormulaValue::Empty)
}
0x01 => {
Ok(FormulaValue::Bool(data[2] != 0))
}
0x02 => {
Ok(FormulaValue::Error(data[2]))
}
0x03 => {
Ok(FormulaValue::String(String::new()))
}
_ => {
Ok(FormulaValue::Number(binary::read_f64_le_at(data, 0)?))
}
}
}
pub fn column_index_to_name(mut col: u32) -> String {
let mut name = String::new();
while col > 0 {
col -= 1; let ch = (b'A' + (col % 26) as u8) as char;
name.insert(0, ch);
col /= 26;
}
name
}
pub fn column_name_to_index(name: &str) -> Option<u32> {
let mut result: u32 = 0;
for ch in name.chars() {
let ch = ch.to_ascii_uppercase();
if !ch.is_ascii_uppercase() {
return None;
}
result = result * 26 + (ch as u32 - 'A' as u32) + 1;
}
Some(result - 1) }
pub fn cell_reference(row: u32, col: u32) -> String {
format!("{}{}", column_index_to_name(col + 1), row + 1)
}
pub fn parse_cell_reference(ref_str: &str) -> Option<(u32, u32)> {
let ref_str = ref_str.to_ascii_uppercase();
let mut col_str = String::new();
let mut row_str = String::new();
for ch in ref_str.chars() {
if ch.is_ascii_uppercase() {
col_str.push(ch);
} else if ch.is_ascii_digit() {
row_str.push(ch);
} else {
return None;
}
}
if col_str.is_empty() || row_str.is_empty() {
return None;
}
let col = column_name_to_index(&col_str)?;
let row: u32 = row_str.parse().ok()?;
Some((row - 1, col)) }
#[allow(dead_code)]
pub fn excel_date_to_datetime(serial: f64, is_1904: bool) -> Option<chrono::NaiveDateTime> {
use chrono::{NaiveDate, Duration};
let base_date = if is_1904 {
NaiveDate::from_ymd_opt(1904, 1, 1)?
} else {
NaiveDate::from_ymd_opt(1899, 12, 30)?
};
let days = serial.trunc() as i64;
let seconds = ((serial.fract() * 86400.0).round() as i64) * 1_000_000;
let date = base_date + Duration::days(days);
let time = Duration::microseconds(seconds);
Some(date.and_time(chrono::NaiveTime::from_hms_opt(0, 0, 0)?) + time)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_column_index_to_name() {
assert_eq!(column_index_to_name(0), "A");
assert_eq!(column_index_to_name(1), "B");
assert_eq!(column_index_to_name(25), "Z");
assert_eq!(column_index_to_name(26), "AA");
assert_eq!(column_index_to_name(702), "AAA");
}
#[test]
fn test_column_name_to_index() {
assert_eq!(column_name_to_index("A"), Some(0));
assert_eq!(column_name_to_index("B"), Some(1));
assert_eq!(column_name_to_index("Z"), Some(25));
assert_eq!(column_name_to_index("AA"), Some(26));
assert_eq!(column_name_to_index("AAA"), Some(702));
assert_eq!(column_name_to_index("a"), Some(0)); assert_eq!(column_name_to_index("1A"), None); }
#[test]
fn test_cell_reference() {
assert_eq!(cell_reference(0, 0), "A1");
assert_eq!(cell_reference(1, 1), "B2");
assert_eq!(cell_reference(0, 26), "AA1");
}
#[test]
fn test_parse_cell_reference() {
assert_eq!(parse_cell_reference("A1"), Some((0, 0)));
assert_eq!(parse_cell_reference("B2"), Some((1, 1)));
assert_eq!(parse_cell_reference("AA1"), Some((0, 26)));
assert_eq!(parse_cell_reference("a1"), Some((0, 0))); assert_eq!(parse_cell_reference("1A"), None); assert_eq!(parse_cell_reference("A"), None); assert_eq!(parse_cell_reference("1"), None); }
}