typst_count/output/
mod.rs

1//! Output formatting for word and character count results.
2//!
3//! This module provides formatters for displaying count results in various formats
4//! including human-readable tables, JSON, and CSV. It handles different display modes
5//! and counting modes to present the data appropriately.
6
7mod csv;
8mod human;
9mod json;
10
11use crate::cli::{CountMode, DisplayMode, OutputFormat};
12use crate::counter::Count;
13
14/// Formatter for outputting count results in various formats.
15///
16/// Combines an output format (human/JSON/CSV) with a counting mode (words/characters/both)
17/// to produce formatted output strings from count results.
18///
19/// # Examples
20///
21/// ```no_run
22/// use typst_count::output::OutputFormatter;
23/// use typst_count::cli::{OutputFormat, CountMode, DisplayMode};
24/// use typst_count::counter::Count;
25///
26/// let formatter = OutputFormatter::new(OutputFormat::Human, CountMode::Both);
27/// let results = vec![("document.typ".to_string(), Count { words: 100, characters: 500 })];
28/// let output = formatter.format_output(&results, DisplayMode::Auto);
29/// println!("{}", output);
30/// ```
31pub struct OutputFormatter {
32    /// The output format to use (human/JSON/CSV)
33    format: OutputFormat,
34    /// What to count and display (words/characters/both)
35    mode: CountMode,
36}
37
38impl OutputFormatter {
39    /// Creates a new output formatter with the specified format and counting mode.
40    ///
41    /// # Arguments
42    ///
43    /// * `format` - The output format (human-readable, JSON, or CSV)
44    /// * `mode` - The counting mode (words, characters, or both)
45    ///
46    /// # Examples
47    ///
48    /// ```no_run
49    /// use typst_count::output::OutputFormatter;
50    /// use typst_count::cli::{OutputFormat, CountMode};
51    ///
52    /// let formatter = OutputFormatter::new(OutputFormat::Human, CountMode::Both);
53    /// ```
54    #[must_use]
55    pub const fn new(format: OutputFormat, mode: CountMode) -> Self {
56        Self { format, mode }
57    }
58
59    /// Formats count results according to the configured format and mode.
60    ///
61    /// Takes a slice of file paths and their counts, and produces a formatted string
62    /// according to the output format (human/JSON/CSV) and display mode.
63    ///
64    /// # Arguments
65    ///
66    /// * `results` - Slice of tuples containing file paths and their counts
67    /// * `display` - Display mode controlling output verbosity and style
68    ///
69    /// # Returns
70    ///
71    /// A formatted string ready for output to stdout or a file.
72    ///
73    /// # Examples
74    ///
75    /// ```no_run
76    /// use typst_count::output::OutputFormatter;
77    /// use typst_count::cli::{OutputFormat, CountMode, DisplayMode};
78    /// use typst_count::counter::Count;
79    ///
80    /// let formatter = OutputFormatter::new(OutputFormat::Json, CountMode::Words);
81    /// let results = vec![
82    ///     ("doc1.typ".to_string(), Count { words: 100, characters: 500 }),
83    ///     ("doc2.typ".to_string(), Count { words: 200, characters: 1000 }),
84    /// ];
85    /// let output = formatter.format_output(&results, DisplayMode::Detailed);
86    /// ```
87    #[must_use]
88    pub fn format_output(&self, results: &[(String, Count)], display: DisplayMode) -> String {
89        match self.format {
90            OutputFormat::Human => human::format(results, display, self.mode),
91            OutputFormat::Json => json::format(results, display, self.mode),
92            OutputFormat::Csv => csv::format(results, display, self.mode),
93        }
94    }
95}
96
97/// Calculates the total word and character count across multiple files.
98///
99/// Sums up all word counts and character counts from the provided results
100/// to produce aggregate totals.
101///
102/// # Arguments
103///
104/// * `results` - Slice of tuples containing file paths and their counts
105///
106/// # Returns
107///
108/// A `Count` struct containing the summed totals of all files.
109///
110/// # Examples
111///
112/// ```no_run
113/// use typst_count::output::calculate_total;
114/// use typst_count::counter::Count;
115///
116/// let results = vec![
117///     ("doc1.typ".to_string(), Count { words: 100, characters: 500 }),
118///     ("doc2.typ".to_string(), Count { words: 200, characters: 1000 }),
119/// ];
120/// let total = calculate_total(&results);
121/// assert_eq!(total.words, 300);
122/// assert_eq!(total.characters, 1500);
123/// ```
124#[must_use]
125pub fn calculate_total(results: &[(String, Count)]) -> Count {
126    Count {
127        words: results.iter().map(|(_, c)| c.words).sum(),
128        characters: results.iter().map(|(_, c)| c.characters).sum(),
129    }
130}
131
132#[cfg(test)]
133mod tests {
134    use super::*;
135
136    #[test]
137    fn test_calculate_total_single_file() {
138        let results = vec![(
139            "file1.typ".to_string(),
140            Count {
141                words: 100,
142                characters: 500,
143            },
144        )];
145
146        let total = calculate_total(&results);
147        assert_eq!(total.words, 100);
148        assert_eq!(total.characters, 500);
149    }
150
151    #[test]
152    fn test_calculate_total_multiple_files() {
153        let results = vec![
154            (
155                "file1.typ".to_string(),
156                Count {
157                    words: 100,
158                    characters: 500,
159                },
160            ),
161            (
162                "file2.typ".to_string(),
163                Count {
164                    words: 200,
165                    characters: 1000,
166                },
167            ),
168            (
169                "file3.typ".to_string(),
170                Count {
171                    words: 50,
172                    characters: 250,
173                },
174            ),
175        ];
176
177        let total = calculate_total(&results);
178        assert_eq!(total.words, 350);
179        assert_eq!(total.characters, 1750);
180    }
181
182    #[test]
183    fn test_calculate_total_empty() {
184        let results: Vec<(String, Count)> = vec![];
185
186        let total = calculate_total(&results);
187        assert_eq!(total.words, 0);
188        assert_eq!(total.characters, 0);
189    }
190
191    #[test]
192    fn test_calculate_total_zero_counts() {
193        let results = vec![
194            (
195                "file1.typ".to_string(),
196                Count {
197                    words: 0,
198                    characters: 0,
199                },
200            ),
201            (
202                "file2.typ".to_string(),
203                Count {
204                    words: 0,
205                    characters: 0,
206                },
207            ),
208        ];
209
210        let total = calculate_total(&results);
211        assert_eq!(total.words, 0);
212        assert_eq!(total.characters, 0);
213    }
214
215    #[test]
216    fn test_output_formatter_creation() {
217        let formatter = OutputFormatter::new(OutputFormat::Human, CountMode::Both);
218        // Just verify it can be created without panicking
219        assert_eq!(formatter.mode, CountMode::Both);
220    }
221
222    #[test]
223    fn test_output_formatter_format_output_single_file() {
224        let formatter = OutputFormatter::new(OutputFormat::Human, CountMode::Both);
225        let results = vec![(
226            "test.typ".to_string(),
227            Count {
228                words: 42,
229                characters: 200,
230            },
231        )];
232
233        let output = formatter.format_output(&results, DisplayMode::Auto);
234        assert!(output.contains("42"));
235        assert!(output.contains("200"));
236    }
237}