Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 7 additions & 5 deletions src/extension/writer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,16 @@
use crate::database::column::Column;
use crate::database::column::ColumnType;
use crate::error::RustySheetError;
use crate::spreadsheet::SpreadsheetError;
use crate::spreadsheet::cell::Cell;
use crate::spreadsheet::cell::CellType;
use crate::spreadsheet::resolve_loaded_shared_string;
use crate::spreadsheet::sheet::Sheet;
use duckdb::core::FlatVector;
use duckdb::core::Inserter;
use libduckdb_sys::duckdb_date;
use libduckdb_sys::duckdb_time;
use libduckdb_sys::duckdb_timestamp;
use crate::spreadsheet::sheet::Sheet;
use crate::spreadsheet::SpreadsheetError;

/// Writes a cell value to a DuckDB vector based on column type.
/// Handles type conversion and error mapping for different data types.
Expand All @@ -25,8 +26,9 @@ pub(super) fn write_to_vector(sheet: &Sheet, column: &Column, cell: &Cell, vecto
)
};
let cell = if cell.kind == CellType::SharedString {
let index = cell.value.parse::<usize>()?;
if let Some(shared_string) = &shared_strings[index] {
if let Some(shared_string) =
resolve_loaded_shared_string(shared_strings, &sheet.file_name, &sheet.name, cell)?
{
&Cell {
row: cell.row,
col: cell.col,
Expand All @@ -41,7 +43,7 @@ pub(super) fn write_to_vector(sheet: &Sheet, column: &Column, cell: &Cell, vecto
cell
};
match column.kind {
ColumnType::Varchar => vector.insert(row, &cell.to_string()),
ColumnType::Varchar => vector.insert(row, &cell.to_display_string().map_err(mapper)?),
ColumnType::Boolean => write_primitive(vector, row, cell.to_boolean()),
ColumnType::BigInt => write_primitive(vector, row, cell.to_bigint().map_err(mapper)?),
ColumnType::Double => write_primitive(vector, row, cell.to_double().map_err(mapper)?),
Expand Down
90 changes: 25 additions & 65 deletions src/spreadsheet/cell.rs
Original file line number Diff line number Diff line change
Expand Up @@ -243,76 +243,36 @@ impl Cell {
_ => Err(format!("parse '{}' to datetime failed", self.value))?,
}
}
}

impl Display for Cell {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let value = match self.kind {
CellType::Boolean => if self.value == "1" { "true" } else { "false" }.to_owned(),
CellType::NumberDateTime1900 => {
if let Ok(value) = to_datetime_string(&self.value, false) {
value
} else {
panic!(
"Parse cell value '{}' at {} to DateTime(1900) failed",
self.value,
self.reference()
);
}
}
CellType::NumberDate1900 => {
if let Ok(value) = to_date_string(&self.value, false) {
value
} else {
panic!(
"Parse cell value '{}' at {} to Date(1900) failed",
self.value,
self.reference()
);
}
}
CellType::NumberDateTime1904 => {
if let Ok(value) = to_datetime_string(&self.value, true) {
value
} else {
panic!(
"Parse cell value '{}' at {} to DateTime(1904) failed",
self.value,
self.reference()
);
}
}
CellType::NumberDate1904 => {
if let Ok(value) = to_date_string(&self.value, true) {
value
} else {
panic!(
"Parse cell value '{}' at {} to Date(1904) failed",
self.value,
self.reference()
);
}
}
CellType::NumberTime1900 | CellType::NumberTime1904 => {
if let Ok(value) = to_time_string(&self.value) {
value
} else {
panic!(
"Parse cell value '{}' at {} to Time failed",
self.value,
self.reference()
);
}
}
CellType::IsoDateTime => self.value.replace("T", " "),
CellType::IsoDuration => self
/// Converts the cell to the user-visible string representation.
pub(crate) fn to_display_string(&self) -> Result<String, String> {
match self.kind {
CellType::Boolean => Ok(if self.value == "1" { "true" } else { "false" }.to_owned()),
CellType::NumberDateTime1900 => to_datetime_string(&self.value, false)
.map_err(|_| format!("parse '{}' to DateTime(1900) failed", self.value)),
CellType::NumberDate1900 => to_date_string(&self.value, false)
.map_err(|_| format!("parse '{}' to Date(1900) failed", self.value)),
CellType::NumberDateTime1904 => to_datetime_string(&self.value, true)
.map_err(|_| format!("parse '{}' to DateTime(1904) failed", self.value)),
CellType::NumberDate1904 => to_date_string(&self.value, true)
.map_err(|_| format!("parse '{}' to Date(1904) failed", self.value)),
CellType::NumberTime1900 | CellType::NumberTime1904 => to_time_string(&self.value)
.map_err(|_| format!("parse '{}' to Time failed", self.value)),
CellType::IsoDateTime => Ok(self.value.replace("T", " ")),
CellType::IsoDuration => Ok(self
.value
.replace("PT", "")
.replace("H", ":")
.replace("M", ":")
.replace("S", ""),
_ => self.value.to_owned(),
};
.replace("S", "")),
_ => Ok(self.value.to_owned()),
}
}
}

impl Display for Cell {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let value = self.to_display_string().unwrap_or_else(|_| self.value.to_owned());
write!(f, "{}", value)
}
}
Expand Down
202 changes: 165 additions & 37 deletions src/spreadsheet/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,100 @@ pub(crate) mod xlsx;
pub(crate) mod criteria;
pub(crate) mod sheet;

pub(crate) fn resolve_number_format(
number_formats: &[CellType],
file_name: &str,
sheet_name: &str,
row: usize,
col: usize,
style_index: usize,
) -> Result<CellType, SpreadsheetError> {
number_formats.get(style_index).copied().ok_or_else(|| {
SpreadsheetError::CellStyleIndexError(
file_name.to_owned(),
sheet_name.to_owned(),
crate::spreadsheet::reference::index_to_reference(row, col),
style_index,
number_formats.len(),
)
})
}

#[cfg(test)]
mod number_format_tests {
use super::*;

#[test]
fn invalid_style_with_out_of_range_column_returns_error() {
let error = resolve_number_format(
&[CellType::Number],
"workbook.xlsb",
"Sheet1",
10,
16_384,
999,
)
.unwrap_err();

assert_eq!(
error.to_string(),
"Cell '[workbook.xlsb]Sheet1!R11C16385': invalid style index 999; workbook defines 1 styles"
);
}
}

pub(crate) fn resolve_shared_string<'a>(
shared_strings: &'a [String],
mappings: &HashMap<usize, usize>,
file_name: &str,
sheet_name: &str,
cell: &Cell,
) -> Result<&'a str, RustySheetError> {
let shared_string_index = cell.value.parse::<usize>()?;
let mapped_index = mappings.get(&shared_string_index).copied().ok_or_else(|| {
SpreadsheetError::CellSharedStringIndexError(
file_name.to_owned(),
sheet_name.to_owned(),
cell.reference(),
shared_string_index,
)
})?;

shared_strings
.get(mapped_index)
.map(String::as_str)
.ok_or_else(|| {
SpreadsheetError::CellSharedStringIndexError(
file_name.to_owned(),
sheet_name.to_owned(),
cell.reference(),
shared_string_index,
)
.into()
})
}

pub(crate) fn resolve_loaded_shared_string<'a>(
shared_strings: &'a [Option<String>],
file_name: &str,
sheet_name: &str,
cell: &Cell,
) -> Result<Option<&'a str>, RustySheetError> {
let shared_string_index = cell.value.parse::<usize>()?;
shared_strings
.get(shared_string_index)
.map(Option::as_deref)
.ok_or_else(|| {
SpreadsheetError::CellSharedStringIndexError(
file_name.to_owned(),
sheet_name.to_owned(),
cell.reference(),
shared_string_index,
)
.into()
})
}

#[derive(Error, Debug)]
pub(crate) enum SpreadsheetError {
/// Error indicating the spreadsheet format is not supported
Expand All @@ -48,6 +142,18 @@ pub(crate) enum SpreadsheetError {
/// Error indicating a specific cell value is invalid
#[error("Cell '[{0}]{1}!{2}': {3}")]
CellValueError(String, String, String, String),

/// Error indicating a cell style index is outside the workbook's style table.
#[error("Cell '[{0}]{1}!{2}': invalid style index {3}; workbook defines {4} styles")]
CellStyleIndexError(String, String, String, usize, usize),

/// Error indicating a shared string index is outside the workbook's shared string table.
#[error("Cell '[{0}]{1}!{2}': invalid shared string index {3}")]
CellSharedStringIndexError(String, String, String, usize),

/// Error indicating a worksheet cell reference is outside the supported spreadsheet bounds.
#[error("Sheet '[{0}]{1}': invalid cell reference '{2}'")]
CellReferenceError(String, String, String),
}

pub(crate) trait Spreadsheet {
Expand Down Expand Up @@ -109,56 +215,78 @@ pub(crate) trait Spreadsheet {
let (shared_strings, mappings) = self.load_shared_strings(Some(shared_indexes))?;

let mut tables = Vec::<Table>::new();
for (name, header, data, row_lower_bound, col_lower_bound, col_upper_bound) in sheets.into_iter() {
let names = (col_lower_bound..=col_upper_bound).map(|col| {
let index = col - col_lower_bound;
if let Some(cell) = &header[index] {
let value = if cell.kind == CellType::SharedString {
let id = cell.value.parse::<usize>().expect("Shared string index");
let index = mappings[&id];
shared_strings[index].to_owned()
} else {
cell.to_string()
};
if !criteria.nulls.contains(&value) {
value
let file_name = self.name();
for (sheet_name, header, data, row_lower_bound, col_lower_bound, col_upper_bound) in sheets.into_iter() {
let names = (col_lower_bound..=col_upper_bound)
.map(|col| -> Result<String, RustySheetError> {
let index = col - col_lower_bound;
if let Some(cell) = &header[index] {
let value = if cell.kind == CellType::SharedString {
resolve_shared_string(
&shared_strings,
&mappings,
&file_name,
&sheet_name,
cell,
)?.to_owned()
} else {
cell.to_display_string().map_err(|message| {
SpreadsheetError::CellValueError(
file_name.to_owned(),
sheet_name.to_owned(),
cell.reference(),
message,
)
})?
};
Ok(if !criteria.nulls.contains(&value) {
value
} else {
index_to_col(col).to_owned()
})
} else {
index_to_col(col).to_owned()
Ok(index_to_col(col).to_owned())
}
} else {
index_to_col(col).to_owned()
}
}).collect::<Vec<_>>();
})
.collect::<Result<Vec<_>, _>>()?;

let columns = names.iter().zip(data)
.map(|(name, cells)| {
.map(|(column_name, cells)| -> Result<Column, RustySheetError> {
let types = cells.iter()
.map(|cell| {
.map(|cell| -> Result<Option<ColumnType>, RustySheetError> {
if cell.kind == CellType::SharedString {
let id = cell.value.parse::<usize>().expect("Shared string index");
let index = mappings[&id];
let value = shared_strings[index].as_str();
ColumnType::from(if criteria.nulls.contains(value) {
&CellType::Empty
} else {
&cell.kind
}, value)
let value = resolve_shared_string(
&shared_strings,
&mappings,
&file_name,
&sheet_name,
cell,
)?;
Ok(ColumnType::from(
if criteria.nulls.contains(value) {
&CellType::Empty
} else {
&cell.kind
},
value,
))
} else {
ColumnType::from(&cell.kind, &cell.value)
Ok(ColumnType::from(&cell.kind, &cell.value))
}
})
.collect::<Vec<_>>();
Column {
name: name.to_owned(),
kind: presets.iter()
.find(|(pattern, _)| pattern.matches(name))
.collect::<Result<Vec<_>, _>>()?;
Ok(Column {
name: column_name.to_owned(),
kind: presets
.iter()
.find(|(pattern, _)| pattern.matches(column_name))
.map(|(_, kind)| kind.to_owned())
.unwrap_or(ColumnType::detect(types)),
}
})
})
.collect::<Vec<_>>();
.collect::<Result<Vec<_>, _>>()?;
tables.push(Table {
name,
name: sheet_name,
columns,
row_lower_bound,
col_lower_bound,
Expand Down
Loading