以前、JSON ファイルから読み出した二次元配列を扱う記事を書きました。
今回は CSV ファイルから読み出した二次元配列の扱いと、前回は対応できなかったジェネリクス型への対応について調査しましたので、メモに残します。
イメージ
コード的にはこんなイメージになります。
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 の内容が以下のとき、
品物/寸法,width,hight,depth
商品A,100,200,300
商品B,400,500,600
商品C,700,800,900
process1 の実行結果は以下のようになります。
// 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
また、行単位や列単位でまとめてデータを扱えるようにします。
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 は、各列の名前、各行の名前、行数、列数、配列のデータをメンバ変数として保持する構造体です。
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>, // 配列のデータ
}
各要素・列単位・行単位でデータを取り出すメソッドや、列数や行数を取り出すメソッドを持ちます。
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 メンバ変数にリストとして保管されます。
// 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 トレイトを以下のように実装することでデータを二次元配列っぽく表示してみました。
// 二次元配列の表示
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 で実装してみました。
ここまで書いておいてなんですが。
こんな記事、どうせ誰も読まねーよなー。