Yabutan 技術ブログ > Rust エクセルファイルの読み書きテスト

Rust エクセルファイルの読み書きテスト

#Rust#Excel

2023-03-12

エクセルを読み取るクレートの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をどう扱うかは、利用側で決める必要がある。