use crate::prelude::*; use calamine::*; use nu_data::TaggedListBuilder; use nu_engine::WholeStreamCommand; use nu_errors::ShellError; use nu_protocol::{Primitive, Signature, SyntaxShape, TaggedDictBuilder, UntaggedValue, Value}; use std::io::Cursor; pub struct FromXlsx; impl WholeStreamCommand for FromXlsx { fn name(&self) -> &str { "from xlsx" } fn signature(&self) -> Signature { Signature::build("from xlsx") .switch( "noheaders", "don't treat the first row as column names", Some('n'), ) .named( "sheets", SyntaxShape::Table, "Only convert specified sheets", Some('s'), ) } fn usage(&self) -> &str { "Parse binary Excel(.xlsx) data and create table." } fn run(&self, args: CommandArgs) -> Result { from_xlsx(args) } } // Adapted from crates/nu-command/src/commands/dataframe/utils.rs fn convert_columns(columns: &[Value]) -> Result, ShellError> { let res = columns .iter() .map(|value| match &value.value { UntaggedValue::Primitive(Primitive::String(s)) => Ok(s.clone()), _ => Err(ShellError::labeled_error( "Incorrect column format", "Only string as column name", &value.tag, )), }) .collect::, _>>()?; Ok(res) } fn from_xlsx(args: CommandArgs) -> Result { let tag = args.call_info.name_tag.clone(); let span = tag.span; let mut sel_sheets = vec![]; if let Some(columns) = args.get_flag::>("sheets")? { sel_sheets = convert_columns(columns.as_slice())?; } let value = args.input.collect_binary(tag.clone())?; let buf: Cursor> = Cursor::new(value.item); let mut xls = Xlsx::<_>::new(buf).map_err(|_| { ShellError::labeled_error("Could not load xlsx file", "could not load xlsx file", &tag) })?; let mut dict = TaggedDictBuilder::new(&tag); let mut sheet_names = xls.sheet_names().to_owned(); if !sel_sheets.is_empty() { sheet_names.retain(|e| sel_sheets.contains(e)); } for sheet_name in &sheet_names { let mut sheet_output = TaggedListBuilder::new(&tag); if let Some(Ok(current_sheet)) = xls.worksheet_range(sheet_name) { for row in current_sheet.rows() { let mut row_output = TaggedDictBuilder::new(&tag); for (i, cell) in row.iter().enumerate() { let value = match cell { DataType::Empty => UntaggedValue::nothing(), DataType::String(s) => UntaggedValue::string(s), DataType::Float(f) => UntaggedValue::decimal_from_float(*f, span), DataType::Int(i) => UntaggedValue::int(*i), DataType::Bool(b) => UntaggedValue::boolean(*b), _ => UntaggedValue::nothing(), }; row_output.insert_untagged(&format!("Column{}", i), value); } sheet_output.push_untagged(row_output.into_untagged_value()); } dict.insert_untagged(sheet_name, sheet_output.into_untagged_value()); } else { return Err(ShellError::labeled_error( "Could not load sheet", "could not load sheet", &tag, )); } } Ok(OutputStream::one(dict.into_value())) } #[cfg(test)] mod tests { use super::FromXlsx; use super::ShellError; #[test] fn examples_work_as_expected() -> Result<(), ShellError> { use crate::examples::test as test_examples; test_examples(FromXlsx {}) } }