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}