Skip to main content

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}