wordcrab/
lib.rs

1use serde_derive::Serialize;
2use std::fmt;
3
4use std::fs::File;
5use std::io::prelude::*;
6use std::io::BufReader;
7
8use rayon::prelude::*;
9
10/// Structure representing the results of a file analysis.
11#[derive(Serialize)]
12pub struct FileStats {
13    /// Number of lines in the file. Based on \n and \r\n
14    pub lines: Option<usize>,
15    /// Number of words in the file. Based on the Unicode Derived Core Property White_Space
16    pub words: Option<usize>,
17    /// Number of characters in the file. Based on the Unicode Scalar Value
18    pub chars: Option<usize>,
19}
20
21impl fmt::Display for FileStats {
22    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
23        // NOTE: intentional spacing here.
24        // Always at least one is reported
25        // Last one includes a space too, so removing one from filename print
26        write!(
27            f,
28            "{}{}{}",
29            match self.lines {
30                Some(lines) => format!("{} ", lines),
31                None => String::new(),
32            },
33            match self.words {
34                Some(words) => format!("{} ", words),
35                None => String::new(),
36            },
37            match self.chars {
38                Some(chars) => format!("{} ", chars),
39                None => String::new(),
40            },
41        )
42    }
43}
44
45#[derive(Copy, Clone, Debug)]
46pub struct AnalysisOptions {
47    pub lines: bool,
48    pub words: bool,
49    pub chars: bool,
50}
51
52#[derive(Serialize)]
53#[serde(untagged)]
54pub enum NamedOutput {
55    Success { filename: String, stats: FileStats },
56    Error { filename: String, error: String },
57}
58
59/// Display as String for CLI use
60impl fmt::Display for NamedOutput {
61    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
62        match self {
63            // NOTE: intentional spacing here, as stats leave a trailing space
64            NamedOutput::Success { filename, stats } => write!(f, "{}{}", stats, filename),
65            NamedOutput::Error { filename, error } => write!(f, "{} {}", error, filename),
66        }
67    }
68}
69
70/// Analyse a single string, returning FileStats
71pub fn analyse_string(contents: &str, options: AnalysisOptions) -> FileStats {
72    FileStats {
73        lines: if options.lines {
74            Some(contents.lines().count())
75        } else {
76            None
77        },
78        words: if options.words {
79            Some(contents.split_whitespace().count())
80        } else {
81            None
82        },
83        chars: if options.chars {
84            Some(contents.chars().count())
85        } else {
86            None
87        },
88    }
89}
90
91/// Runs a file analysis on the given filename path.
92/// Returns a NamedOutput structure, with the filename and
93/// either results or error
94pub fn analyse_file(filename: &str, options: AnalysisOptions) -> NamedOutput {
95    let file = match File::open(filename) {
96        Err(e) => {
97            return NamedOutput::Error {
98                filename: filename.to_string(),
99                error: e.to_string(),
100            }
101        }
102        Ok(f) => f,
103    };
104
105    let mut buf_reader = BufReader::new(file);
106    let mut contents = String::new();
107    match buf_reader.read_to_string(&mut contents) {
108        Ok(_bytes) => NamedOutput::Success {
109            filename: filename.to_string(),
110            stats: analyse_string(&contents, options),
111        },
112        Err(e) => NamedOutput::Error {
113            filename: filename.to_string(),
114            error: e.to_string(),
115        },
116    }
117}
118
119/// Runs a file analysis on the given list of path buffers.
120/// Returns a NamedOutput structure, with the filename and
121/// either results or error
122pub fn analyse_files(filenames: &[&str], options: AnalysisOptions) -> Vec<NamedOutput> {
123    filenames
124        .par_iter()
125        .map(|filename| analyse_file(filename, options))
126        .collect()
127}