1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
//! wordcount はシンプルな文字、単語、行の出現頻度の計算機能を提供します。
//! 詳しくは[`count`](fn.count.html)関数のドキュメントを参照してください。
#![warn(missing_docs)]

use regex::Regex;
use std::collections::HashMap;
use std::io::BufRead;

/// [`count`](fn.count.html)で使うオプション
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum CountOption {
    /// Unicodeの1文字
    Char,
    /// 正規表現の \w+ にマッチする単語
    Word,
    /// \n で区切られた一行
    Line,
}

/// オプションのデフォルトは [`Word`](enum.CountOption.html#variant.Word)
impl Default for CountOption {
    fn default() -> Self {
        CountOption::Word
    }
}

/// input から一行ずつ文字列を読み込み。頻度を数える。
///
/// 頻度を数える対象はオプションによって制御される。
/// * [`CountOption::Char`](enum.CountOption.html#variant.Char)
/// * [`CountOption::Word`](enum.CountOption.html#variant.Word)
/// * [`CountOption::Line`](enum.CountOption.html#variant.Line)
///
/// # Examples
///
/// ```
/// use std::io::Cursor;
/// use fhiroki_bicycle_book_wordcount::{count, CountOption};
///
/// let mut input = Cursor::new("aa bb cc bb");
/// let freq = count(input, CountOption::Word);
/// assert_eq!(freq["aa"], 1);
/// assert_eq!(freq["bb"], 2);
/// assert_eq!(freq["cc"], 1);
/// ```
///
///
/// # Panics
/// 入力がUTF-8フォーマットされていない場合
pub fn count(input: impl BufRead, option: CountOption) -> HashMap<String, usize> {
    let re = Regex::new(r"\w+").unwrap();
    let mut freqs = HashMap::new();

    for line in input.lines() {
        let line = line.unwrap();
        use crate::CountOption::*;
        match option {
            Char => {
                for c in line.chars() {
                    *freqs.entry(c.to_string()).or_insert(0) += 1;
                }
            }
            Word => {
                for m in re.find_iter(&line) {
                    let word = m.as_str().to_string();
                    *freqs.entry(word).or_insert(0) += 1;
                }
            }
            Line => *freqs.entry(line.to_string()).or_insert(0) += 1,
        }
    }

    freqs
}

#[cfg(test)]
mod test {
    use super::*;
    use std::io::Cursor;

    macro_rules! assert_map {
        ($expr: expr, {$($key: expr => $value:expr),*}) => {
            $(assert_eq!($expr[$key], $value));*
        };
    }

    #[test]
    fn word_count_works() {
        let mut exp = HashMap::new();
        exp.insert("aa".to_string(), 1);
        exp.insert("bb".to_string(), 2);
        exp.insert("cc".to_string(), 1);
        assert_eq!(count(Cursor::new("aa bb cc bb"), CountOption::Word), exp);
    }

    #[test]
    fn word_count_works2() {
        use std::io::Cursor;

        let mut exp = HashMap::new();
        exp.insert("a".to_string(), 2);
        exp.insert("c".to_string(), 2);
        exp.insert("d".to_string(), 3);

        assert_eq!(count(Cursor::new("aaccddd"), CountOption::Char), exp);
    }

    #[test]
    fn word_count_works3() {
        let freqs = count(Cursor::new("aa bb cc"), CountOption::Word);
        assert_map!(freqs, {"aa" => 1, "bb" => 1, "cc" => 1});
    }

    #[test]
    #[should_panic]
    fn word_count_do_not_contain_unknown_words() {
        use std::io::Cursor;

        count(Cursor::new([b'a', 0xf0, 0x90, 0x80]), CountOption::Word);
    }
}