coding_tools/tree.rs
1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2026 Jonathan Shook
3
4//! Pure helpers behind `ct-tree`'s reporting: per-file line/word/character
5//! counts, the metric-bound predicate, and the immediate-parent grouping used by
6//! the per-folder predicate and the directory summary.
7
8/// Count lines, words, and characters of `content`.
9///
10/// Lines are counted `wc`-style (a final line without a trailing newline still
11/// counts); words are whitespace-separated; characters are Unicode scalar values
12/// and include newlines (like `wc -m`).
13///
14/// # Examples
15///
16/// ```
17/// use coding_tools::tree::metrics;
18///
19/// // 2 lines; words a, b, cd; 7 chars including both newlines.
20/// assert_eq!(metrics("a b\ncd\n"), (2, 3, 7));
21/// // a final line without a newline still counts as a line.
22/// assert_eq!(metrics("one two"), (1, 2, 7));
23/// assert_eq!(metrics(""), (0, 0, 0));
24/// ```
25pub fn metrics(content: &str) -> (u64, u64, u64) {
26 let lines = content.lines().count() as u64;
27 let words = content.split_whitespace().count() as u64;
28 let chars = content.chars().count() as u64;
29 (lines, words, chars)
30}
31
32/// Whether a value satisfies an optional `>= min` / `<= max` pair (an absent
33/// bound never constrains).
34///
35/// # Examples
36///
37/// ```
38/// use coding_tools::tree::within;
39///
40/// assert!(within(10, Some(5), Some(20)));
41/// assert!(!within(3, Some(5), None)); // below min
42/// assert!(!within(30, None, Some(20))); // above max
43/// assert!(within(10, None, None)); // unbounded
44/// ```
45pub fn within(value: u64, min: Option<u64>, max: Option<u64>) -> bool {
46 min.is_none_or(|m| value >= m) && max.is_none_or(|m| value <= m)
47}
48
49/// The immediate parent directory of a relative path (`"."` for a root-level
50/// file). Used to group files for the per-folder predicate and `--summary dir`.
51///
52/// # Examples
53///
54/// ```
55/// use coding_tools::tree::parent_dir;
56///
57/// assert_eq!(parent_dir("a/b/c.rs"), "a/b");
58/// assert_eq!(parent_dir("top.rs"), ".");
59/// ```
60pub fn parent_dir(rel: &str) -> String {
61 match rel.rsplit_once('/') {
62 Some((dir, _)) => dir.to_string(),
63 None => ".".to_string(),
64 }
65}
66
67#[cfg(test)]
68mod tests {
69 use super::*;
70
71 #[test]
72 fn metrics_count_lines_words_chars() {
73 // chars counts newlines too (like `wc -m`): a, space, b, \n, c, d, \n = 7.
74 assert_eq!(metrics("a b\ncd\n"), (2, 3, 7));
75 assert_eq!(metrics("one two"), (1, 2, 7));
76 assert_eq!(metrics(""), (0, 0, 0));
77 }
78
79 #[test]
80 fn within_bounds() {
81 assert!(within(10, Some(5), Some(20)));
82 assert!(!within(3, Some(5), None));
83 assert!(!within(30, None, Some(20)));
84 assert!(within(10, None, None));
85 }
86
87 #[test]
88 fn parent_dir_of_paths() {
89 assert_eq!(parent_dir("a/b/c.rs"), "a/b");
90 assert_eq!(parent_dir("top.rs"), ".");
91 }
92}