第2章の12から19まで(言語処理100本ノック2020)

Posted by johtani on Tuesday, May 12, 2020

目次

Rustで言語処理100本ノックの第2章の残りです。

前回はこちら

ちなみに、標準入力から受け取る処理は書いてないです。 出力に関してはファイル分割、保存と支持があるもの以外は文字列として取り出すところで終わっています。

12. 1列目をcol1.txtに,2列目をcol2.txtに保存

pub fn extract_column(input_file_name: &str, num: usize, output_file_name: &str) {
    let input_f = File::open(input_file_name).expect("file not found");
    let read_buf = BufReader::new(input_f);
    let mut output_f = OpenOptions::new()
        .write(true)
        .create(true)
        .open(output_file_name)
        .expect(format!("can't open file[{}] with write option", output_file_name).as_str());
    read_buf.lines().for_each(|line| match line {
        Ok(line) => {
            let columns: Vec<_> = line.split('\t').collect();
            writeln!(output_f, "{}", columns[num]);
            output_f.flush().expect("Error during flush");
        }
        Err(_) => panic!("parse error "),
    });
}

13で出力結果を利用するので入力として出力ファイル名も受け取るようにしました。 問題としては、1列目と2列目を別々に出力すればいいので、1回の処理で書いても良かったのですが、1回1ファイルの出力という形で実装しました(効率は悪い)。

改行コードあたりを考えるのがめんどくさかったのでwriteln!マクロでファイルに書き出しています。が、普通にwriteメソッドで改行コードを追加しても良かったのかなと。

あとは、出力先ファイルが存在しない場合だけopenするようにOpenOptionsを利用してみています。

flushを呼び出すべきなのかどうか?を調べないとな。。。

13. col1.txtとcol2.txtをマージ

pub fn merge_files(col1_file: &str, col2_file: &str, output_file_name: &str) {
    let col1_buf = BufReader::new(File::open(col1_file).expect("file not found"));
    let col2_buf = BufReader::new(File::open(col2_file).expect("file not found"));
    let mut output_f = OpenOptions::new()
        .write(true)
        .create(true)
        .open(output_file_name)
        .expect(format!("can't open file[{}] with write option", output_file_name).as_str());
    col1_buf
        .lines()
        .zip(col2_buf.lines())
        .for_each(|(col1, col2)| {
            let col1 = col1.expect("parse error col1");
            let col2 = col2.expect("parse error col2");
            writeln!(output_f, "{}\t{}", col1, col2);
            output_f.flush().expect("Error during flush");
        });
}

2つのファイル名を入力として受け取り、タブでくっつけて出力します。 zipを利用することで、2つのイテレーターを同時に回しています。

14. 先頭からN行を出力

pub fn head(input_file_name: &str, lines: usize) -> String {
    let buf = BufReader::new(File::open(input_file_name).expect("file not found"));
    let mut head = String::new();
    buf.lines().take(lines).for_each(|line| {
        head.push_str(format!("{}\n", line.expect("parse error")).as_str());
    });
    return head;
}

イテレーターのメソッドにtakeがあります。 これを利用することで、引数に指定した数のエレメントが取得できるので、これでheadが実現できます。

15. 末尾のN行を出力

pub fn tail(input_file_name: &str, lines: usize) -> String {
    let buf = BufReader::new(File::open(input_file_name).expect("file not found"));
    let mut tail = String::new();
    let line_count = word_count(input_file_name);
    buf.lines().skip(line_count - lines).for_each(|line| {
        tail.push_str(format!("{}\n", line.expect("parse error")).as_str());
    });
    return tail;
}

tailの場合は少し複雑で、11で作成した行数をカウントするメソッドで総行数を取り出し、そこから引数で指定された行数を引き算した数(=出力しない行数)を、イテレーターのskipメソッドの引数に渡しています。これにより、指定された数のエレメントをスキップしたあとの処理がかけます。

16. ファイルをN分割する

pub fn split_files(
    input_file_name: &str,
    num: usize,
    output_file_prefix: &str,
    output_file_suffix: &str,
) {
    let total = word_count(input_file_name) as f64;
    let lines_in_file = total / num as f64;
    let lines_in_file = lines_in_file.ceil() as usize; //
    let buf = BufReader::new(File::open(input_file_name).expect("file not found"));

    let output_files: Vec<File> = create_file_vec(output_file_prefix, num, output_file_suffix);

    println!("split file each {} lines.", lines_in_file);
    let mut lines = buf.lines();

    for mut output_f in output_files {
        let mut current = 1;
        while current < lines_in_file + 1 {
            let line = lines.next();
            if let Some(line_rs) = line {
                if let Ok(line_str) = line_rs {
                    writeln!(output_f, "{}", line_str);
                }
            }
            current = current + 1;
        }
        output_f.flush().expect("error during flush");
    }
}

fn create_file_vec(output_file_prefix: &str, num: usize, output_file_suffix: &str) -> Vec<File> {
    let mut files = Vec::with_capacity(num);
    for i in 0..num {
        let output_file_name = format!("{}{}{}", output_file_prefix, i + 1, output_file_suffix);
        let output_f = OpenOptions::new()
            .write(true)
            .create(true)
            .open(output_file_name.as_str())
            .expect(format!("can't open file[{}] with write option", output_file_name).as_str());
        files.push(output_f);
    }
    return files;
}

ちょっと長いですね。

入力としては、分割するファイル数Nが指定されます。まずは、総行数/Nで各ファイルに保存されるべき行数を計算します。 次に、2つ目の関数をつかって、必要な数のファイルオブジェクトをベクトルとして生成します。

ファイルオブジェクトのベクトルの要素を元にしたfor文を回しつつ、それぞれのファイルに必要な行数を出力している処理になっています。

総行数がNで割り切れない場合にceilで切り上げした行数にするというちょっとした処理を入れてあります。

17. 1列目の文字列の異なり

pub fn count_uniq_words(input_file_name: &str, col: usize) -> usize {
    let mut words = HashSet::new();
    let buf = BufReader::new(File::open(input_file_name).expect("file not found"));
    buf.lines().for_each(|line| match line {
        Ok(line_str) => {
            let columns: Vec<_> = line_str.split('\t').collect();
            words.insert(columns[col].to_string());
        }
        Err(_) => panic!("parse error "),
    });
    return words.len();
}

HashSetを利用することでユニーク性を担保して、最後はHashSetの数を数え上げれば終了です。

18. 各行を3コラム目の数値の降順にソート

pub fn sort_on_col3(input_file_name: &str) -> String {
    let mut lines: BTreeSet<Line> = BTreeSet::new();
    let buf = BufReader::new(File::open(input_file_name).expect("file not found"));
    buf.lines().for_each(|line| match line {
        Ok(line_str) => {
            let columns: Vec<_> = line_str.split('\t').collect();
            let num: u32 = columns[2].parse().expect("parse error");
            let line = Line {
                line: line_str,
                num,
            };
            lines.insert(line);
        }
        Err(_) => panic!("parse error"),
    });
    let mut sorted = String::new();
    lines.iter().for_each(|line| {
        sorted.push_str(format!("{}\n", line.line).as_str());
    });

    return sorted;
}

#[derive(Eq)]
struct Line {
    line: String,
    num: u32,
}

impl Ord for Line {
    fn cmp(&self, other: &Self) -> Ordering {
        let ord = other.num.cmp(&self.num);
        if let Ordering::Equal = ord {
            other.line.cmp(&self.line)
        } else {
            ord
        }
    }
}

impl PartialOrd for Line {
    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
        Some(self.cmp(other))
    }
}

impl PartialEq for Line {
    fn eq(&self, other: &Self) -> bool {
        self.eq(other)
    }
}

Lineという、行の文章と第3カラム目の値をもった構造体を作成しました。そこにEq、Ord、PartialOrd、PartialEqを実装し、3カラム目での大小比較できるようにしました。 この構造体をBTeeSetに格納していき、イテレーターで回すことで、ソートされた状態にしてあります。 同一数値の場合は行の降順でソートできるようにOrdを実装してあります。

19. 各行の1コラム目の文字列の出現頻度を求め,出現頻度の高い順に並べる

pub fn sort_on_frequency(input_file_name: &str) -> String {
    let mut names: HashMap<String, u32> = HashMap::new();
    let buf = BufReader::new(File::open(input_file_name).expect("file not found"));
    buf.lines().for_each(|line| match line {
        Ok(line_str) => {
            let columns: Vec<_> = line_str.split('\t').collect();
            let name_str = columns[0].to_string();
            let count = names.entry(name_str).or_insert(0);
            *count += 1;
        }
        Err(_) => panic!("parse error"),
    });
    let mut sorted = String::new();
    let mut sorted_names: Vec<(&String, &u32)> = names.iter().collect();
    sorted_names.sort_by(|(aname, acount), (bname, bcount)| {
        let ord = bcount.cmp(acount);
        if let Ordering::Equal = ord {
            bname.cmp(aname)
        } else {
            ord
        }
    });
    sorted_names.iter().for_each(|(name, count)| {
        sorted.push_str(format!("{} {}\n", count, name).as_str());
    });
    return sorted;
}

もうすこしうまくできる気がしますが、いったんこれで。 数え上げのためにまずはHashMapに第1カラムの文字列, 個数という組み合わせでデータを入れていきます。 出来上がったHashMapをタプルのベクターに変換し、変換したベクターのsort_byメソッドに比較用の関数を渡すことで個数の降順に並べています。同一個数の場合は文字列の降順になっています。 で、最後に並びかわったベクターのイテレーターを使って出力しておしまいです。 内部的には最悪3回回る感じでしょうか? 最初からベクトルに入れつつソートできる仕組みにするようなのがいいのかなぁ?

まとめ

Unixコマンドの勉強になりましたw あとは、HashMapなどの勉強にもなりました。 最後の方は効率がいまいち良くない気もしてはいますが、とりあえず第3章に進もうかと思います。


comments powered by Disqus

See Also by Hugo


Related by prelims-cli