1fn split_at_char(s: &str, n: usize) -> (&str, Option<&str>) {
2 for (char_index, (i, _)) in s.char_indices().enumerate() {
3 if char_index == n {
4 let (w1, w2) = s.split_at(i);
5 return (w1, Some(w2));
6 }
7 }
8
9 (s, None)
10}
11
12pub fn justify(text: &str, line_width: usize) -> Vec<String> {
13 let paragraphs: Vec<&str> = text.split("\n\n").collect();
14 let mut lines: Vec<String> = Vec::new();
15
16 for paragraph in paragraphs {
17 let raw_words: Vec<&str> = paragraph.split_whitespace().collect();
18 let mut words = vec![];
19
20 for mut word in raw_words {
21 while let (w1, Some(w2)) = split_at_char(word, line_width) {
22 words.push(w1);
23 word = w2;
24 }
25
26 words.push(word);
27 }
28
29 let mut line: Vec<&str> = Vec::new();
30 let mut len = 0;
31
32 for word in words {
33 let word_len = word.len();
35 let space_len = if line.is_empty() { 0 } else { 1 };
36 let new_len = len + space_len + word_len;
37
38 if new_len > line_width && !line.is_empty() {
41 lines.push(justify_line(&line, line_width));
42 line.clear();
43 len = 0;
44 }
45
46 line.push(word);
47 len = if line.len() == 1 { word_len } else { len + space_len + word_len };
48 }
49
50 if !line.is_empty() {
52 lines.push(line.join(" "));
53 }
54
55 lines.push(String::new());
57 }
58
59 lines
60}
61
62fn justify_line(line: &[&str], line_width: usize) -> String {
63 let word_len: usize = line.iter().map(|s| s.len()).sum();
64
65 if word_len >= line_width || line.len() <= 1 {
68 return line.join(" ");
69 }
70
71 let spaces = line_width - word_len;
72
73 let line_len_div = if (line.len() > 1) { (line.len() - 1) } else { 1 };
74
75 let each_space = spaces / line_len_div;
76 let extra_space = spaces % line_len_div;
77
78 let mut justified = String::new();
79 for (i, word) in line.iter().enumerate() {
80 justified.push_str(word);
81 if i < line.len() - 1 {
82 let mut space = " ".repeat(each_space);
83 if i < extra_space {
84 space.push(' ');
85 }
86 justified.push_str(&space);
87 }
88 }
89
90 justified
91}
92
93#[cfg(test)]
94mod tests {
95 use super::*;
96
97 #[test]
98 fn test_handles_long_words() {
99 let input_text = r#"some text and a very loooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooong word but no cause to panic"#;
100 let pretty_short_line_width = 10;
101 let result = justify(input_text, pretty_short_line_width);
102 assert!(!result.is_empty());
103 }
104
105 #[test]
106 fn test_handles_line_longer_than_width() {
107 let input_text =
108 "This is a line that is definitely longer than the requested width";
109 let result = justify(input_text, 20);
110 assert!(!result.is_empty());
111 }
113
114 #[test]
115 fn test_single_word_longer_than_width() {
116 let input_text = "supercalifragilisticexpialidocious";
117 let result = justify(input_text, 10);
118 assert!(!result.is_empty());
119 assert!(result.len() > 1);
121 }
122
123 #[test]
124 fn test_normal_justification() {
125 let input_text = "This is a test of the justification system. It should properly justify lines that need to be wrapped.";
127 let result = justify(input_text, 20);
128 assert!(!result.is_empty());
129
130 let mut found_justified = false;
132 for (i, line) in result.iter().enumerate() {
133 if !line.is_empty() && i < result.len() - 2 {
134 if line.len() == 20 {
136 found_justified = true;
137 break;
138 }
139 }
140 }
141 assert!(found_justified, "Should have at least one justified line");
142 }
143}