use std::path::{Path, PathBuf};
use laterite_ags4_core::ags4_codec::{DuplicateHeadings, ExcessFields, ReadOptions};
use laterite_ags4_excel::{ExcelStats, ags4_bytes_to_xlsx_with, xlsx_bytes_to_ags4};
use super::bad_encoding;
use crate::{Error, ErrorKind};
enum ExcelInput {
Path(PathBuf),
Bytes(Vec<u8>),
}
impl ExcelInput {
fn describe(&self) -> String {
match self {
ExcelInput::Path(p) => format!("path {}", p.display()),
ExcelInput::Bytes(b) => format!("{} bytes", b.len()),
}
}
fn load(&self) -> Result<Vec<u8>, Error> {
match self {
ExcelInput::Path(p) => std::fs::read(p).map_err(|e| {
Error::with_source(ErrorKind::Io, format!("cannot read {}", p.display()), e)
}),
ExcelInput::Bytes(b) => Ok(b.clone()),
}
}
}
pub struct ToExcel {
input: ExcelInput,
encoding: Option<String>,
recover_duplicate_headings: bool,
truncate_excess_fields: bool,
}
fn pending_to_excel(input: ExcelInput) -> ToExcel {
ToExcel {
input,
encoding: None,
recover_duplicate_headings: false,
truncate_excess_fields: false,
}
}
#[must_use]
pub fn to_excel(path: impl AsRef<Path>) -> ToExcel {
pending_to_excel(ExcelInput::Path(path.as_ref().to_path_buf()))
}
#[must_use]
pub fn to_excel_bytes(bytes: impl Into<Vec<u8>>) -> ToExcel {
pending_to_excel(ExcelInput::Bytes(bytes.into()))
}
impl ToExcel {
#[must_use]
pub fn encoding(mut self, label: impl Into<String>) -> ToExcel {
self.encoding = Some(label.into());
self
}
#[must_use]
pub fn recover_duplicate_headings(mut self, yes: bool) -> ToExcel {
self.recover_duplicate_headings = yes;
self
}
#[must_use]
pub fn truncate_excess_fields(mut self, yes: bool) -> ToExcel {
self.truncate_excess_fields = yes;
self
}
pub fn run(self) -> Result<Workbook, Error> {
let raw = self.input.load()?;
let bytes = match &self.encoding {
None => raw,
Some(label) => {
let enc = laterite_ags4_parse::resolve_encoding(Some(label))
.ok_or_else(|| bad_encoding(label))?;
enc.decode(&raw).0.into_owned().into_bytes()
}
};
let opts = ReadOptions {
duplicate_headings: if self.recover_duplicate_headings {
DuplicateHeadings::Recover
} else {
DuplicateHeadings::Error
},
excess_fields: if self.truncate_excess_fields {
ExcessFields::Truncate
} else {
ExcessFields::Error
},
};
let (xlsx, stats) = ags4_bytes_to_xlsx_with(&bytes, None, opts).map_err(|e| {
Error::with_source(
ErrorKind::NotAgs4,
format!("cannot convert {} to XLSX", self.input.describe()),
e,
)
})?;
Ok(Workbook {
bytes: xlsx,
stats: Stats::from_engine(stats),
})
}
pub fn to_path(self, path: impl AsRef<Path>) -> Result<Workbook, Error> {
let workbook = self.run()?;
workbook.save(path)?;
Ok(workbook)
}
}
pub struct Workbook {
bytes: Vec<u8>,
stats: Stats,
}
impl Workbook {
#[must_use]
pub fn bytes(&self) -> &[u8] {
&self.bytes
}
#[must_use]
pub fn into_bytes(self) -> Vec<u8> {
self.bytes
}
#[must_use]
pub fn sheets_written(&self) -> usize {
self.stats.sheets
}
#[must_use]
pub fn rows_written(&self) -> usize {
self.stats.rows
}
#[must_use]
pub fn warnings(&self) -> &[String] {
&self.stats.warnings
}
pub fn save(&self, path: impl AsRef<Path>) -> Result<(), Error> {
let path = path.as_ref();
std::fs::write(path, &self.bytes).map_err(|e| {
Error::with_source(ErrorKind::Io, format!("cannot write {}", path.display()), e)
})
}
}
pub struct FromExcel {
input: ExcelInput,
format_numeric_columns: bool,
}
#[must_use]
pub fn from_excel(path: impl AsRef<Path>) -> FromExcel {
FromExcel {
input: ExcelInput::Path(path.as_ref().to_path_buf()),
format_numeric_columns: true,
}
}
#[must_use]
pub fn from_excel_bytes(bytes: impl Into<Vec<u8>>) -> FromExcel {
FromExcel {
input: ExcelInput::Bytes(bytes.into()),
format_numeric_columns: true,
}
}
impl FromExcel {
#[must_use]
pub fn format_numeric_columns(mut self, yes: bool) -> FromExcel {
self.format_numeric_columns = yes;
self
}
pub fn run(self) -> Result<Converted, Error> {
let raw = self.input.load()?;
let (ags4, stats) = xlsx_bytes_to_ags4(&raw, self.format_numeric_columns).map_err(|e| {
Error::with_source(
ErrorKind::Other,
format!("cannot convert {} to AGS4", self.input.describe()),
e,
)
})?;
Ok(Converted {
text: String::from_utf8(ags4).map_err(|e| {
Error::with_source(
ErrorKind::Other,
"the conversion produced bytes that are not UTF-8",
e,
)
})?,
stats: Stats::from_engine(stats),
})
}
pub fn to_path(self, path: impl AsRef<Path>) -> Result<Converted, Error> {
let converted = self.run()?;
converted.save(path)?;
Ok(converted)
}
}
pub struct Converted {
text: String,
stats: Stats,
}
impl Converted {
#[must_use]
pub fn bytes(&self) -> &[u8] {
self.text.as_bytes()
}
#[must_use]
pub fn into_bytes(self) -> Vec<u8> {
self.text.into_bytes()
}
#[must_use]
pub fn text(&self) -> &str {
&self.text
}
#[must_use]
pub fn into_text(self) -> String {
self.text
}
#[must_use]
pub fn sheets_written(&self) -> usize {
self.stats.sheets
}
#[must_use]
pub fn rows_written(&self) -> usize {
self.stats.rows
}
#[must_use]
pub fn warnings(&self) -> &[String] {
&self.stats.warnings
}
pub fn save(&self, path: impl AsRef<Path>) -> Result<(), Error> {
let path = path.as_ref();
std::fs::write(path, self.bytes()).map_err(|e| {
Error::with_source(ErrorKind::Io, format!("cannot write {}", path.display()), e)
})
}
}
struct Stats {
sheets: usize,
rows: usize,
warnings: Vec<String>,
}
impl Stats {
fn from_engine(stats: ExcelStats) -> Stats {
Stats {
sheets: stats.sheets_written,
rows: stats.rows_written,
warnings: stats.warnings,
}
}
}
impl std::fmt::Debug for ToExcel {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("ToExcel")
.field("input", &self.input.describe())
.field("encoding", &self.encoding)
.field(
"recover_duplicate_headings",
&self.recover_duplicate_headings,
)
.field("truncate_excess_fields", &self.truncate_excess_fields)
.finish()
}
}
impl std::fmt::Debug for Workbook {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Workbook")
.field("bytes", &self.bytes.len())
.field("sheets", &self.stats.sheets)
.field("rows", &self.stats.rows)
.field("warnings", &self.stats.warnings.len())
.finish()
}
}
impl std::fmt::Debug for FromExcel {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("FromExcel")
.field("input", &self.input.describe())
.field("format_numeric_columns", &self.format_numeric_columns)
.finish()
}
}
impl std::fmt::Debug for Converted {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Converted")
.field("bytes", &self.text.len())
.field("sheets", &self.stats.sheets)
.field("rows", &self.stats.rows)
.field("warnings", &self.stats.warnings.len())
.finish()
}
}