cli_justify/
lib.rs

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      // Calculate the length if we add this word
34      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 adding this word would exceed the line width and we have words on
39      // the line
40      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    // Add the last line of the paragraph
51    if !line.is_empty() {
52      lines.push(line.join(" "));
53    }
54
55    // Add a blank line after each paragraph to preserve paragraph breaks
56    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 the words are already longer than or equal to line width,
66  // or if there's only one word, just join them with single spaces
67  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    // Should not panic
112  }
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    // Word should be split into multiple lines
120    assert!(result.len() > 1);
121  }
122
123  #[test]
124  fn test_normal_justification() {
125    // Test with multiple lines to see justification
126    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    // Find a line that was justified (not the last line)
131    let mut found_justified = false;
132    for (i, line) in result.iter().enumerate() {
133      if !line.is_empty() && i < result.len() - 2 {
134        // Not the last line or blank line
135        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}