excelreader 6.0.0

Read and write Excel/CSV workbooks via ExcelReader's native ABI (schema-driven typed parse and write).
use excelreader::workbook::{ExcelMapper, Sheet, SheetVisibility, Workbook};
use excelreader::writer_handle::WriterHandle;
use excelreader::{RowCursor, XL_ERROR, XL_FORMAT_AUTO, XL_FORMAT_XLSX, XL_INVALID_ARGUMENT};

#[derive(Default, ExcelMapper)]
struct R {
    #[excel(name = "a")]
    a: String,
}

fn two_sheets() -> Workbook {
    let mut writer = WriterHandle::open_memory(XL_FORMAT_XLSX, None).expect("open writer");
    for (name, rows) in [("First", vec!["a1", "a2"]), ("Second", vec!["b1", "b2", "b3"])] {
        writer.start_sheet(name).expect("start sheet");
        for value in rows {
            writer.start_row().expect("start row");
            writer.write_str(Some(value)).expect("write");
            writer.end_row().expect("end row");
        }
        writer.end_sheet().expect("end sheet");
    }
    let bytes = writer.bytes().expect("bytes");
    Workbook::open_memory(&bytes, XL_FORMAT_AUTO, None).expect("open")
}

fn first_column(cursor: &mut excelreader::RowCursor<'_>) -> Vec<String> {
    let mut values = Vec::new();
    while let Some(row) = cursor.next_row() {
        let row = row.expect("no error mid-sheet");
        values.push(row.get(0).expect("a cell").as_str().expect("utf-8").to_string());
    }
    values
}

#[test]
fn a_sheet_reads_its_own_rows() {
    let workbook = two_sheets();
    let second = workbook.sheet(1).expect("sheet 1");
    let first = workbook.sheet(0).expect("sheet 0");

    assert_eq!(first_column(&mut second.rows().expect("cursor")), ["b1", "b2", "b3"]);
    assert_eq!(first_column(&mut first.rows().expect("cursor")), ["a1", "a2"]);
}

#[test]
fn two_cursors_on_one_sheet_each_return_every_row() {
    let workbook = two_sheets();
    let sheet = workbook.sheet(1).expect("sheet 1");
    let mut one = sheet.rows().expect("cursor one");
    let mut two = sheet.rows().expect("cursor two");

    assert!(one.next_row().expect("a row").is_ok());
    assert!(two.next_row().expect("a row").is_ok());

    assert_eq!(first_column(&mut one), ["b2", "b3"]);
    assert_eq!(first_column(&mut two), ["b2", "b3"]);
}

#[test]
fn a_cursor_dropped_midway_leaves_the_workbook_usable() {
    let workbook = two_sheets();
    let sheet = workbook.sheet(1).expect("sheet 1");
    for _ in 0..1000 {
        let mut cursor = sheet.rows().expect("cursor");
        assert!(cursor.next_row().expect("a row").is_ok());
    }

    assert_eq!(first_column(&mut sheet.rows().expect("cursor")), ["b1", "b2", "b3"]);
}

#[test]
fn sheet_rejects_an_index_out_of_range_at_the_call() {
    let workbook = two_sheets();

    let past_end = workbook.sheet(2).expect_err("past the end");
    assert_eq!(past_end.code(), XL_ERROR);
    assert!(!past_end.message().is_empty());
    assert_eq!(workbook.sheet(-1).expect_err("negative").code(), XL_INVALID_ARGUMENT);
}

#[test]
fn sheets_lists_every_sheet_with_its_name_index_and_visibility() {
    let workbook = two_sheets();
    let sheets = workbook.sheets().expect("sheets");

    assert_eq!(sheets.len(), 2);
    assert_eq!(sheets[1].index(), 1);
    assert_eq!(sheets[1].name().expect("name"), "Second");
    assert_eq!(sheets[0].visibility().expect("visibility"), SheetVisibility::Visible);
}

#[test]
fn sheet_by_name_ignores_case_and_reports_a_miss_as_none() {
    let workbook = two_sheets();

    assert_eq!(workbook.sheet_by_name("Second").expect("ok").expect("found").index(), 1);
    assert_eq!(workbook.sheet_by_name("sEcOnD").expect("ok").expect("found").index(), 1);
    assert!(workbook.sheet_by_name("Missing").expect("ok").is_none());
}

#[test]
fn read_all_reads_the_named_sheet() {
    let workbook = two_sheets();
    let sheet = workbook.sheet(1).expect("sheet 1");

    assert_eq!(sheet.read_all_decoded().expect("decoded").len(), 3);
    assert_eq!(sheet.read_all_blob().expect("blob").len(), 3);
    assert_eq!(workbook.sheet(0).expect("sheet 0").read_all_blob().expect("blob").len(), 2);
}

#[test]
fn infer_schema_samples_the_named_sheet() {
    let workbook = two_sheets();

    let columns = workbook.sheet(1).expect("sheet 1").infer_schema(0, 10).expect("schema");

    assert_eq!(columns.len(), 1);
}

const PARALLEL_SHEETS: usize = 6;
const PARALLEL_ROWS: usize = 500;

fn many_sheets() -> Workbook {
    let mut writer = WriterHandle::open_memory(XL_FORMAT_XLSX, None).expect("open writer");
    for sheet in 0..PARALLEL_SHEETS {
        writer.start_sheet(&format!("sheet{sheet}")).expect("start sheet");
        for row in 0..PARALLEL_ROWS {
            writer.start_row().expect("start row");
            writer.write_str(Some(&format!("s{sheet}-r{row}"))).expect("write");
            writer.end_row().expect("end row");
        }
        writer.end_sheet().expect("end sheet");
    }
    let bytes = writer.bytes().expect("bytes");
    Workbook::open_memory(&bytes, XL_FORMAT_AUTO, None).expect("open")
}

fn read_sheet(sheet: Sheet<'_>) -> Vec<String> {
    first_column(&mut sheet.rows().expect("cursor"))
}

#[test]
fn workbook_and_sheet_cross_threads_and_cursors_move_between_them() {
    fn assert_send_sync<T: Send + Sync>() {}
    fn assert_send<T: Send>() {}

    assert_send_sync::<Workbook>();
    assert_send_sync::<Sheet<'static>>();
    assert_send::<RowCursor<'static>>();
    assert_send_sync::<excelreader::DecodedRows>();
    assert_send_sync::<excelreader::workbook::TableView<R>>();
    assert_send::<excelreader::workbook::TypedChunks<'static, R>>();
    #[cfg(feature = "arrow")]
    assert_send::<excelreader::arrow::ArrowChunks<'static>>();
}

#[test]
fn decoded_rows_come_back_from_scoped_threads() {
    let workbook = many_sheets();
    let sheets = workbook.sheets().expect("sheets");
    let counts: Vec<usize> = std::thread::scope(|scope| {
        let handles: Vec<_> = sheets
            .iter()
            .map(|sheet| {
                let sheet = *sheet;
                scope.spawn(move || sheet.read_all_decoded())
            })
            .collect();
        handles
            .into_iter()
            .map(|handle| handle.join().expect("no panic").expect("read").len())
            .collect()
    });
    assert_eq!(counts, vec![PARALLEL_ROWS; PARALLEL_SHEETS]);
}

#[test]
fn every_sheet_read_on_its_own_thread_matches_a_sequential_read() {
    let workbook = many_sheets();
    let sheets = workbook.sheets().expect("sheets");
    let sequential: Vec<Vec<String>> = sheets.iter().map(|sheet| read_sheet(*sheet)).collect();
    assert_eq!(sequential[3][0], "s3-r0");
    assert_eq!(sequential[3].len(), PARALLEL_ROWS);

    let parallel: Vec<Vec<String>> = std::thread::scope(|scope| {
        let handles: Vec<_> = sheets
            .iter()
            .chain(sheets.iter())
            .map(|sheet| {
                let sheet = *sheet;
                scope.spawn(move || read_sheet(sheet))
            })
            .collect();
        handles.into_iter().map(|handle| handle.join().expect("no panic")).collect()
    });

    for (index, rows) in parallel.iter().enumerate() {
        assert_eq!(rows, &sequential[index % PARALLEL_SHEETS]);
    }
}