File size: 4,241 Bytes
e5034c3
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
use std::fmt::Display;

pub struct NumberedContent<'a> {
    start: usize,
    raw_content: &'a str,
}

impl<'a> Display for NumberedContent<'a> {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        let lines: Vec<&str> = self.raw_content.lines().collect();

        if lines.is_empty() {
            return Ok(());
        }

        // Calculate the width needed for the largest line number
        let max_line_number = self.start + lines.len() - 1;
        let width = max_line_number.to_string().len();
        let start = self.start;
        let last = lines.len() - 1;
        for (i, line) in lines.into_iter().enumerate() {
            if i < last {
                writeln!(f, "{:>width$}:{}", start + i, line, width = width)?;
            } else {
                write!(f, "{:>width$}:{}", start + i, line, width = width)?;
            }
        }

        Ok(())
    }
}

pub trait LineNumbers {
    /// Returns the text with each line numbered, starting at 1.
    fn to_numbered(&self) -> NumberedContent<'_> {
        self.to_numbered_from(1)
    }

    /// Returns the text with each line numbered, starting at the given offset.
    fn to_numbered_from(&self, start: usize) -> NumberedContent<'_>;
}

impl<T: AsRef<str>> LineNumbers for T {
    fn to_numbered_from(&self, start: usize) -> NumberedContent<'_> {
        NumberedContent { start, raw_content: self.as_ref() }
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_numbered_default_start() {
        let text = "first line\nsecond line\nthird line";
        let expected = "1:first line\n2:second line\n3:third line";
        assert_eq!(text.to_numbered().to_string(), expected);
    }

    #[test]
    fn test_numbered_from_custom_start() {
        let text = "alpha\nbeta\ngamma";
        let expected = "5:alpha\n6:beta\n7:gamma";
        assert_eq!(text.to_numbered_from(5).to_string(), expected);
    }

    #[test]
    fn test_numbered_single_line() {
        let text = "single line";
        let expected = "1:single line";
        assert_eq!(text.to_numbered().to_string(), expected);
    }

    #[test]
    fn test_numbered_empty_string() {
        let text = "";
        let expected = "";
        assert_eq!(text.to_numbered().to_string(), expected);
    }

    #[test]
    fn test_numbered_with_empty_lines() {
        let text = "line1\n\nline3";
        let expected = "1:line1\n2:\n3:line3";
        assert_eq!(text.to_numbered().to_string(), expected);
    }

    #[test]
    fn test_numbered_right_aligned_single_digit() {
        let text = "line1\nline2\nline3";
        let expected = "1:line1\n2:line2\n3:line3";
        assert_eq!(text.to_numbered().to_string(), expected);
    }

    #[test]
    fn test_numbered_right_aligned_two_digits() {
        let text = "a\nb\nc\nd\ne\nf\ng\nh\ni\nj\nk";
        let expected = " 1:a\n 2:b\n 3:c\n 4:d\n 5:e\n 6:f\n 7:g\n 8:h\n 9:i\n10:j\n11:k";
        assert_eq!(text.to_numbered().to_string(), expected);
    }

    #[test]
    fn test_numbered_right_aligned_three_digits() {
        let mut lines = Vec::new();
        for i in 1..=100 {
            lines.push(format!("line{}", i));
        }
        let text = lines.join("\n");
        let actual = text.to_numbered().to_string();

        // Check first line has 2 leading spaces (001 -> "  1")
        assert!(actual.starts_with("  1:line1"));
        // Check line 10 has 1 leading space (010 -> " 10")
        assert!(actual.contains("\n 10:line10\n"));
        // Check line 100 has no leading spaces (100 -> "100")
        assert!(actual.contains("\n100:line100"));
    }

    #[test]
    fn test_numbered_from_right_aligned() {
        let text = "alpha\nbeta\ngamma\ndelta";
        // Starting from 98, so max is 101 (3 digits)
        let expected = " 98:alpha\n 99:beta\n100:gamma\n101:delta";
        assert_eq!(text.to_numbered_from(98).to_string(), expected);
    }

    #[test]
    fn test_numbered_from_crosses_digit_boundary() {
        let text = "line8\nline9\nline10\nline11";
        // Starting from 8, max is 11 (2 digits)
        let expected = " 8:line8\n 9:line9\n10:line10\n11:line11";
        assert_eq!(text.to_numbered_from(8).to_string(), expected);
    }
}