Rust研究：二次元配列、CSV読み込み、ジェネリクス

以前、JSON ファイルから読み出した二次元配列を扱う記事を書きました。

https://qiita.com/mekamaru/items/d2f9dcd33cfc20041796

今回は CSV ファイルから読み出した二次元配列の扱いと、前回は対応できなかったジェネリクス型への対応について調査しましたので、メモに残します。

# イメージ

コード的にはこんなイメージになります。

```rust
fn process1() -> std::io::Result<()> {

    // data.csv を「整数データ」の二次元配列として読み出す。
    let data: TwoDimentionalArray<i32> =  from_csv_file("data.csv")?;

    println!("{}", data);

    println!("  データの列数 = {}", data.col_size());
    println!("  データの行数 = {}", data.row_size());

    // data.csv を「浮動小数点数データ」の二次元配列として読み出す。
    let data: TwoDimentionalArray<f32> = from_csv_file("data.csv")?;

    println!("{}", data);

    println!("  データの列数 = {}", data.col_size());
    println!("  データの行数 = {}", data.row_size());

    // data.csv を「文字列データ」の二次元配列として読み出す。
    let data: TwoDimentionalArray<String> = from_csv_file("data.csv")?;

    println!("{}", data);

    println!("  データの列数 = {}", data.col_size());
    println!("  データの行数 = {}", data.row_size());

    Ok(())
}
```

CSV ファイルを読み出す際に指定する「型」によって扱う二次元配列の要素のデータ型が変化します。

data.csv の内容が以下のとき、

```rust
品物/寸法,width,hight,depth
商品A,100,200,300
商品B,400,500,600
商品C,700,800,900
```

process1 の実行結果は以下のようになります。

```rust
// process1 の実行結果
[ 100 200 300
  400 500 600
  700 800 900 ]
  データの列数 = 3
  データの行数 = 3

[ 100.0 200.0 300.0
  400.0 500.0 600.0
  700.0 800.0 900.0 ]
  データの列数 = 3
  データの行数 = 3

[ "100" "200" "300"
  "400" "500" "600"
  "700" "800" "900" ]
  データの列数 = 3
  データの行数 = 3
```

また、行単位や列単位でまとめてデータを扱えるようにします。

```rust
fn process2() -> std::io::Result<()> {

    // data.csv を整数データの二次元配列として読み出す。
    let data: TwoDimentionalArray<i32> =  from_csv_file("data.csv")?;

    // 合計
    let sum:i32 = data.iter().sum();

    // 0列の合計
    let sum_c0:i32 = data.col(0).iter().sum();

    // 各行の合計のベクタ
    let sum_rows:Vec<i32> = (0..data.row_size()).map(|i|data.row(i).iter().sum()).collect();

    // 平均
    let ave: f32 = (sum as f32) / (data.size() as f32);

    println!("合計 = {}", sum);
    println!("0列の合計 = {}", sum_c0);
    println!("各行の合計 = {:?}", sum_rows);
    println!("平均 = {}", ave);

    Ok(())
}

// process2 の実行結果
合計 = 4500
0列の合計 = 1200
各行の合計 = [600, 1500, 2400]
平均 = 500
```

# 実装例

TwoDimentionalArray は、各列の名前、各行の名前、行数、列数、配列のデータをメンバ変数として保持する構造体です。

```rust
use std::io::Error;
use std::io::Result;
use std::io::ErrorKind;

// 二次元配列（ジェネリクス版）
#[derive(Debug)]
pub struct TwoDimentionalArray<T> {
    headers:Vec<String>, // 各列の名前（先頭行の各セル）
    index:Vec<String>,   // 各行の名前（各行の先頭セル）
    row_size:usize,      // 行数
    col_size:usize,      // 列数
    dat:Vec<T>,          // 配列のデータ
}
```

各要素・列単位・行単位でデータを取り出すメソッドや、列数や行数を取り出すメソッドを持ちます。

```rust
impl <T:Clone> TwoDimentionalArray <T> {

    // at(i,j) は i 行 j 列の要素を取り出す 
    pub fn at(&self, i:usize, j:usize) -> Result<T> {
        if i>=self.row_size || j>=self.col_size {
            return Err(Error::new(ErrorKind::Other, 
                "abnormal index!"))
        }
        Ok(self.dat[i*self.row_size + j].clone())
    }

    // col(j) は j 列の要素のベクタを取り出す。j が異常値のときは空のベクタを返す
    pub fn col(&self, j:usize) -> Vec<T> {
        let mut r:Vec<T> = vec![];
        if j < self.col_size {
            for (pos, v) in self.dat.iter().enumerate() {
                if pos % self.col_size == j {
                    r.push((*v).clone());
                }
            }
        }
        r
    }

    // row(i) は i 行の要素のベクタを取り出す。i が異常値のときは空のベクタを返す 
    pub fn row(&self, i:usize) -> Vec<T> {
        let mut r:Vec<T> = vec![];
        if i < self.row_size {
            for (pos, v) in self.dat.iter().enumerate() {
                if pos / self.col_size == i {
                    r.push((*v).clone());
                }
            }
        }
        r
    }

    // 各行の名前のベクタを返す
    pub fn index(&self) -> Vec<String> {
        self.index.clone()
    }

    // 各列の名前のベクタを返す
    pub fn headers(&self) -> Vec<String> {
        self.headers.clone()
    }

    // データのベクタを返す
    pub fn dat(&self) -> Vec<T> {
        self.dat.clone()
    }
    
    // データのイテレータを返す
    pub fn iter(&self) -> core::slice::Iter<T> {
        self.dat.iter()
    }

    pub fn row_size(&self) -> usize {
        self.row_size
    }

    pub fn col_size(&self) -> usize {
        self.col_size
    }

    pub fn size(&self) -> usize {
        self.row_size * self.col_size
    }

}
```

関数 from_csv_file は、CSV ファイルを読みだし、TwoDimentionalArray のインスタンスを作成する関数です。

先頭行の各セルは各列の名前、各行の先頭セルは各行の名前であり、headers と index メンバ変数にリストとして保管されます。

```rust
// CSV ファイルを読みだす関数
pub fn from_csv_file <S:std::str::FromStr> (csv_file: &str) 
    -> Result<TwoDimentionalArray<S>> {

    let mut dat: Vec<S> = vec![];

    let file = std::fs::File::open(csv_file)?;
    let mut rdr = csv::Reader::from_reader(file);

    // ヘッダの取得。CSVファイルの先頭行は常にヘッダとみなされることに注意。
    let line = rdr.headers()?;
    let headers:Vec<String> = line.iter().skip(1).map(|x|String::from(x)).collect();
    let col_size = headers.len();

    let mut index:Vec<String> = vec![];
    let mut row_size :usize = 0;

    for line in rdr.records() {
        let line = line?;

        // ヘッダの列数と合わないときはエラー
        if line.len()-1 != col_size {
            return Err(Error::new(ErrorKind::Other, 
                "column size is missmatched!"))
        }    

        let mut iter = line.iter();

        // 先頭セルは行の名前
        index.push(String::from(iter.next().unwrap()));

        for item in iter {
            if let Ok(value) = item.parse() {
                dat.push(value);
            } else {
                // [MEMO] parse のエラー std::str::FromStr::Err から
                // std::io::Error に変換する From トレイトが実装されていないことに注意
                return Err(Error::new(ErrorKind::Other, 
                    "failed in parsing!"));
            }
        }
        row_size += 1;
    }

    Ok(
        TwoDimentionalArray {
            headers,
            index,
            row_size,
            col_size,
            dat,
        }
    )
}
```

ジェネリクス型 S が FromStr トレイトを実装していることが大切で、これによって CSV ファイルの中の各セルの文字列から整数や浮動小数点数などの基本型への変換が行われます。

Display トレイトを以下のように実装することでデータを二次元配列っぽく表示してみました。
 
```rust
// 二次元配列の表示
impl <T:std::fmt::Debug> std::fmt::Display for TwoDimentionalArray<T> {
    fn fmt (&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "[ ")?;
        for (i, x) in self.dat.iter().enumerate() {
            write!(f, "{:?} ", x)?;
            if i == self.col_size * self.row_size - 1 {
                write!(f, "]")?;
            } else if (i+1)%self.col_size == 0 {
                write!(f, "\n  ")?;
            }
        }
        Ok(())
    }
}
```

以上、二次元配列の CSV ファイルからの読み出しと、ジェネリクス型対応を Rust で実装してみました。




ここまで書いておいてなんですが。
こんな記事、どうせ誰も読まねーよなー。