エクセルを読み取るクレートのcalamineですが、書き込み機能はないのでテストコードを書く際には、 エクセル生成用のクレート rust_xlsxwriter と組み合わせて書くのが良い。
この記事では、rust_xlsxwriterクレートを使って、メモリ上に作成したエクセルを、 calamineで読み取るテストコードをサンプルとして紹介します。
calamineとは
エクセルファイル読み取り用のクレートです。
エクセルファイルからの読み取りが一般的ですが、メモリバッファ上のバイト列からもエクセル読み取りができます。
rust_xlsxwriterとは
エクセルファイルを生成するためのクレート。
ファイルに書き出しは勿論ですが、メモリバッファ上にエクセルファイルを書きだす事もできます。
最近出てきたものらしいですが、Pythonの XlsxWriter の作者が作っているようです。
まだまだ開発段階のようですが、凝った事しない限りは充分使えると思います。
100% Rust実装なので、Cargo.tomlに追記するだけで使えます。
テストコードサンプル
早速テストコードのサンプルです。
追加しているクレートは、calamine とrust_xlsxwriter 。
[dependencies]
calamine = { version = "0.19", features = ["dates"] }
rust_xlsxwriter = "0.26"
anyhow = "1.0"
thiserror = "1.0"
rust_xlsxwriter で作成したエクセルをメモリ上に保存して calamineで読み取るコードです。
#[test]
fn test_provide_u32() -> anyhow::Result<()> {
// テスト用のエクセルデータをメモリ上に作成
let buf = {
// add_worksheetで、`Sheet1` が作成される。
let mut wb = rust_xlsxwriter::Workbook::new();
let sheet = wb.add_worksheet();
// write_xxx でセルに対して値を書き込む。
// row:行 col:列 ... ゼロベースのインデックス値で指定
// write_number(row, col, number)
// write_string(row, col, string)
sheet.write_number(0, 0, 0)?;
sheet.write_number(0, 1, 1)?;
sheet.write_number(0, 2, 1.01)?; // 小数点
sheet.write_string(0, 3, "100")?; // 文字列
// None扱いにする値
sheet.write_string(1, 0, "")?; // 空文字
// u32にパースできない値
sheet.write_number(2, 0, -1)?; // マイナス値
sheet.write_number(2, 1, 9999999999_f64)?; // 桁あふれ
sheet.write_string(2, 2, "abc")?; // 文字列
// メモリバッファ上に保存
// そのままテストコードで使用したいので、ファイルではなくメモリ上に保存しています。
wb.save_to_buffer()?
};
// メモリ上に作成したエクセルを、calamineで読み取る。
let mut wb: Xlsx<_> = open_workbook_from_rs(Cursor::new(buf))?;
let sheet = wb.worksheet_range("Sheet1").unwrap()?;
// アクセスしやすいように、Cellトレイトを作って利用しています。
assert_eq!(sheet.cell_value_u32(0, 0)?, Some(0));
assert_eq!(sheet.cell_value_u32(0, 1)?, Some(1));
assert_eq!(sheet.cell_value_u32(0, 2)?, Some(1));
assert_eq!(sheet.cell_value_u32(0, 3)?, Some(100));
// 空文字、空セルは、Noneになること。
assert_eq!(sheet.cell_value_u32(1, 0)?, None);
assert_eq!(sheet.cell_value_u32(1, 99)?, None);
// u32にパースできない値は、エラーになること。
assert_eq!(
sheet.cell_value_u32(2, 0),
Err(IllegalValueAsU32(DataType::Float(-1_f64)))
);
assert_eq!(
sheet.cell_value_u32(2, 1),
Err(IllegalValueAsU32(DataType::Float(9999999999_f64)))
);
assert_eq!(
sheet.cell_value_u32(2, 2),
Err(IllegalValueAsU32(DataType::String("abc".to_string())))
);
Ok(())
}
#[test]
fn test_provide_string() -> anyhow::Result<()> {
// テスト用のエクセルデータをメモリ上に作成
let buf = {
// add_worksheetで、`Sheet1` が作成される。
let mut wb = rust_xlsxwriter::Workbook::new();
let sheet = wb.add_worksheet();
// write_xxx でセルに対して値を書き込む。
// row:行 col:列 ... ゼロベースのインデックス値で指定
// write_number(row, col, number)
// write_string(row, col, string)
sheet.write_number(0, 0, 0)?;
sheet.write_number(0, 1, 1)?;
sheet.write_number(0, 2, -1)?;
sheet.write_number(0, 3, 1.01)?;
sheet.write_number(0, 4, 1.00001)?;
// 文字列
sheet.write_string(1, 0, "'hello'")?;
sheet.write_string(1, 1, "日本語")?;
sheet.write_string(1, 2, "")?;
// 日付
let format = Format::new().set_num_format("yyyy-mm-dd hh::mm:ss");
let datetime = NaiveDate::from_ymd_opt(2023, 1, 25)
.unwrap()
.and_hms_opt(12, 30, 0)
.unwrap();
sheet.write_datetime(2, 0, &datetime, &format)?;
// メモリバッファ上に保存
// そのままテストコードで使用したいので、ファイルではなくメモリ上に保存しています。
wb.save_to_buffer()?
};
// メモリ上に作成したエクセルを、calamineで読み取る。
let mut wb: Xlsx<_> = open_workbook_from_rs(Cursor::new(buf))?;
let sheet = wb.worksheet_range("Sheet1").unwrap()?;
// アクセスしやすいように、Cellトレイトを作って利用しています。
assert_eq!(sheet.cell_value_string(0, 0)?, Some("0".to_string()));
assert_eq!(sheet.cell_value_string(0, 1)?, Some("1".to_string()));
assert_eq!(sheet.cell_value_string(0, 2)?, Some("-1".to_string()));
assert_eq!(sheet.cell_value_string(0, 3)?, Some("1.01".to_string()));
assert_eq!(sheet.cell_value_string(0, 4)?, Some("1.00001".to_string()));
assert_eq!(sheet.cell_value_string(1, 0)?, Some("'hello'".to_string()));
assert_eq!(sheet.cell_value_string(1, 1)?, Some("日本語".to_string()));
assert_eq!(sheet.cell_value_string(1, 2)?, None); // 空文字
assert_eq!(sheet.cell_value_string(1, 99)?, None); // 空セル
assert_eq!(
sheet.cell_value_string(2, 0)?,
Some("2023-01-25 12:30:00".to_string())
);
Ok(())
}
calamineでセル値を取得する際には、
まずセルを取得して、DataTypeごとに処理を別ける必要があるので、
プロバイダーの役割を果たす、トレイト実装を挟むようにしています。
#[derive(Debug, Error, PartialEq)]
pub enum CellValueError {
#[error("{0} cannot convert to u32")]
IllegalValueAsU32(DataType),
#[error("unexpected cell type: {0}")]
UnexpectedType(DataType),
}
pub trait Cell {
fn cell(&self, r: usize, c: usize) -> Option<&DataType>;
fn cell_value_u32(&self, r: usize, c: usize) -> Result<Option<u32>, CellValueError>;
fn cell_value_string(&self, r: usize, c: usize) -> Result<Option<String>, CellValueError>;
}
impl Cell for Range<DataType> {
fn cell(&self, r: usize, c: usize) -> Option<&DataType> {
self.get_value((r as u32, c as u32))
}
fn cell_value_u32(&self, r: usize, c: usize) -> Result<Option<u32>, CellValueError> {
match self.cell(r, c) {
None => Ok(None),
Some(cell) => cell.provide_u32(),
}
}
fn cell_value_string(&self, r: usize, c: usize) -> Result<Option<String>, CellValueError> {
match self.cell(r, c) {
None => Ok(None),
Some(cell) => cell.provide_string(),
}
}
}
pub trait ValueProvide {
fn provide_u32(&self) -> Result<Option<u32>, CellValueError>;
fn provide_string(&self) -> Result<Option<String>, CellValueError>;
}
impl ValueProvide for DataType {
fn provide_u32(&self) -> Result<Option<u32>, CellValueError> {
match self {
&DataType::Float(f) => {
if f < u32::MIN as f64 || (u32::MAX as f64) < f {
return Err(IllegalValueAsU32(self.clone()));
}
Ok(Some(f as u32))
}
DataType::String(s) => {
let value: u32 = s.parse().map_err(|_| IllegalValueAsU32(self.clone()))?;
Ok(Some(value))
}
DataType::DateTime(_) => Err(UnexpectedType(self.clone())),
DataType::Error(_) => Err(UnexpectedType(self.clone())),
_ => Ok(None),
}
}
fn provide_string(&self) -> Result<Option<String>, CellValueError> {
match self {
DataType::String(s) => Ok(Some(s.to_string())),
DataType::Float(f) => Ok(Some(f.to_string())),
DataType::DateTime(_) => Ok(self.as_datetime().map(|dt| dt.to_string())),
DataType::Error(_) => Err(UnexpectedType(self.clone())),
_ => Ok(None),
}
}
}
DataTypeによって、どのような扱いでセル値を取得するのかは、
利用する側の都合でそれぞれだと思うので、状況に合わせる必要があります。
今回は、u32, String を取得するだけの処理を用意してます。
まとめ
- calamineは、エクセル読み取り専用のクレート
- rust_xlsxwriterは、エクセル書き込み専用のクレート (追記はできない)
- メモリバッファ上にエクセルは作成できるので、読み取り処理を比較的簡単にテストする事ができる。
- セル値のDataTypeをどう扱うかは、利用側で決める必要がある。