textwrap/wrap_algorithms.rs
1//! Word wrapping algorithms.
2//!
3//! After a text has been broken into words (or [`Fragment`]s), one
4//! now has to decide how to break the fragments into lines. The
5//! simplest algorithm for this is implemented by
6//! [`wrap_first_fit()`]: it uses no look-ahead and simply adds
7//! fragments to the line as long as they fit. However, this can lead
8//! to poor line breaks if a large fragment almost-but-not-quite fits
9//! on a line. When that happens, the fragment is moved to the next
10//! line and it will leave behind a large gap.
11//!
12//! A more advanced algorithm, implemented by [`wrap_optimal_fit()`],
13//! will take this into account. The optimal-fit algorithm considers
14//! all possible line breaks and will attempt to minimize the gaps
15//! left behind by overly short lines.
16//!
17//! While both algorithms run in linear time, the first-fit algorithm
18//! is about 4 times faster than the optimal-fit algorithm.
19
20#[cfg(feature = "smawk")]
21mod optimal_fit;
22#[cfg(feature = "smawk")]
23pub use optimal_fit::{OverflowError, Penalties, wrap_optimal_fit};
24
25use crate::core::{Fragment, Word};
26use alloc::vec::Vec;
27
28/// Describes how to wrap words into lines.
29///
30/// The simplest approach is to wrap words one word at a time and
31/// accept the first way of wrapping that fits
32/// ([`WrapAlgorithm::FirstFit`]). If the `smawk` Cargo feature is
33/// enabled, a more complex algorithm is available that will look at
34/// an entire paragraph at a time in order to find optimal line breaks
35/// ([`WrapAlgorithm::OptimalFit`]).
36#[derive(Clone, Copy, Debug)]
37pub enum WrapAlgorithm {
38 /// Wrap words using a fast and simple algorithm.
39 ///
40 /// This algorithm uses no look-ahead when finding line breaks.
41 /// Implemented by [`wrap_first_fit()`], please see that function
42 /// for details and examples.
43 FirstFit,
44
45 /// Wrap words using an advanced algorithm with look-ahead.
46 ///
47 /// This wrapping algorithm considers the entire paragraph to find
48 /// optimal line breaks. When wrapping text, "penalties" are
49 /// assigned to line breaks based on the gaps left at the end of
50 /// lines. See [`Penalties`] for details.
51 ///
52 /// The underlying wrapping algorithm is implemented by
53 /// [`wrap_optimal_fit()`], please see that function for examples.
54 ///
55 /// **Note:** Only available when the `smawk` Cargo feature is
56 /// enabled.
57 #[cfg(feature = "smawk")]
58 OptimalFit(Penalties),
59
60 /// Custom wrapping function.
61 ///
62 /// Use this if you want to implement your own wrapping algorithm.
63 /// The function can freely decide how to turn a slice of
64 /// [`Word`]s into lines.
65 ///
66 /// # Example
67 ///
68 /// ```
69 /// use textwrap::core::Word;
70 /// use textwrap::{wrap, Options, WrapAlgorithm};
71 ///
72 /// fn stair<'a, 'b>(words: &'b [Word<'a>], _: &'b [usize]) -> Vec<&'b [Word<'a>]> {
73 /// let mut lines = Vec::new();
74 /// let mut step = 1;
75 /// let mut start_idx = 0;
76 /// while start_idx + step <= words.len() {
77 /// lines.push(&words[start_idx .. start_idx+step]);
78 /// start_idx += step;
79 /// step += 1;
80 /// }
81 /// lines
82 /// }
83 ///
84 /// let options = Options::new(10).wrap_algorithm(WrapAlgorithm::Custom(stair));
85 /// assert_eq!(wrap("First, second, third, fourth, fifth, sixth", options),
86 /// vec!["First,",
87 /// "second, third,",
88 /// "fourth, fifth, sixth"]);
89 /// ```
90 Custom(for<'a, 'b> fn(words: &'b [Word<'a>], line_widths: &'b [usize]) -> Vec<&'b [Word<'a>]>),
91}
92
93impl PartialEq for WrapAlgorithm {
94 /// Compare two wrap algorithms.
95 ///
96 /// ```
97 /// use textwrap::WrapAlgorithm;
98 ///
99 /// assert_eq!(WrapAlgorithm::FirstFit, WrapAlgorithm::FirstFit);
100 /// #[cfg(feature = "smawk")] {
101 /// assert_eq!(WrapAlgorithm::new_optimal_fit(), WrapAlgorithm::new_optimal_fit());
102 /// }
103 /// ```
104 ///
105 /// Note that `WrapAlgorithm::Custom` values never compare equal:
106 ///
107 /// ```
108 /// use textwrap::WrapAlgorithm;
109 ///
110 /// assert_ne!(WrapAlgorithm::Custom(|words, line_widths| vec![words]),
111 /// WrapAlgorithm::Custom(|words, line_widths| vec![words]));
112 /// ```
113 fn eq(&self, other: &Self) -> bool {
114 match (self, other) {
115 (WrapAlgorithm::FirstFit, WrapAlgorithm::FirstFit) => true,
116 #[cfg(feature = "smawk")]
117 (WrapAlgorithm::OptimalFit(a), WrapAlgorithm::OptimalFit(b)) => a == b,
118 (_, _) => false,
119 }
120 }
121}
122
123impl WrapAlgorithm {
124 /// Create new wrap algorithm.
125 ///
126 /// The best wrapping algorithm is used by default, i.e.,
127 /// [`WrapAlgorithm::OptimalFit`] if available, otherwise
128 /// [`WrapAlgorithm::FirstFit`].
129 pub const fn new() -> Self {
130 #[cfg(not(feature = "smawk"))]
131 {
132 WrapAlgorithm::FirstFit
133 }
134
135 #[cfg(feature = "smawk")]
136 {
137 WrapAlgorithm::new_optimal_fit()
138 }
139 }
140
141 /// New [`WrapAlgorithm::OptimalFit`] with default penalties. This
142 /// works well for monospace text.
143 ///
144 /// **Note:** Only available when the `smawk` Cargo feature is
145 /// enabled.
146 #[cfg(feature = "smawk")]
147 pub const fn new_optimal_fit() -> Self {
148 WrapAlgorithm::OptimalFit(Penalties::new())
149 }
150
151 /// Wrap words according to line widths.
152 ///
153 /// The `line_widths` slice gives the target line width for each
154 /// line (the last slice element is repeated as necessary). This
155 /// can be used to implement hanging indentation.
156 #[inline]
157 pub fn wrap<'a, 'b>(
158 &self,
159 words: &'b [Word<'a>],
160 line_widths: &'b [usize],
161 ) -> Vec<&'b [Word<'a>]> {
162 // Every integer up to 2u64.pow(f64::MANTISSA_DIGITS) = 2**53
163 // = 9_007_199_254_740_992 can be represented without loss by
164 // a f64. Larger line widths will be rounded to the nearest
165 // representable number.
166 let f64_line_widths = line_widths.iter().map(|w| *w as f64).collect::<Vec<_>>();
167
168 match self {
169 WrapAlgorithm::FirstFit => wrap_first_fit(words, &f64_line_widths),
170
171 #[cfg(feature = "smawk")]
172 WrapAlgorithm::OptimalFit(penalties) => {
173 // The computation cannot overflow when the line
174 // widths are restricted to usize.
175 wrap_optimal_fit(words, &f64_line_widths, penalties).unwrap()
176 }
177
178 WrapAlgorithm::Custom(func) => func(words, line_widths),
179 }
180 }
181}
182
183impl Default for WrapAlgorithm {
184 fn default() -> Self {
185 WrapAlgorithm::new()
186 }
187}
188
189/// Wrap abstract fragments into lines with a first-fit algorithm.
190///
191/// The `line_widths` slice gives the target line width for each line
192/// (the last slice element is repeated as necessary). This can be
193/// used to implement hanging indentation.
194///
195/// The fragments must already have been split into the desired
196/// widths. This function will not (and cannot) attempt to split them
197/// further when arranging them into lines.
198///
199/// # First-Fit Algorithm
200///
201/// This implements a simple “greedy” algorithm: accumulate fragments
202/// one by one and when a fragment no longer fits, start a new line.
203/// There is no look-ahead, we simply take the first fit of the
204/// fragments we find.
205///
206/// While fast and predictable, this algorithm can produce poor line
207/// breaks when a long fragment is moved to a new line, leaving behind
208/// a large gap:
209///
210/// ```
211/// use textwrap::core::Word;
212/// use textwrap::wrap_algorithms::wrap_first_fit;
213/// use textwrap::WordSeparator;
214///
215/// // Helper to convert wrapped lines to a Vec<String>.
216/// fn lines_to_strings(lines: Vec<&[Word<'_>]>) -> Vec<String> {
217/// lines.iter().map(|line| {
218/// line.iter().map(|word| &**word).collect::<Vec<_>>().join(" ")
219/// }).collect::<Vec<_>>()
220/// }
221///
222/// let text = "These few words will unfortunately not wrap nicely.";
223/// let words = WordSeparator::AsciiSpace.find_words(text).collect::<Vec<_>>();
224/// assert_eq!(lines_to_strings(wrap_first_fit(&words, &[15.0])),
225/// vec!["These few words",
226/// "will", // <-- short line
227/// "unfortunately",
228/// "not wrap",
229/// "nicely."]);
230///
231/// // We can avoid the short line if we look ahead:
232/// #[cfg(feature = "smawk")]
233/// use textwrap::wrap_algorithms::{wrap_optimal_fit, Penalties};
234/// #[cfg(feature = "smawk")]
235/// assert_eq!(lines_to_strings(wrap_optimal_fit(&words, &[15.0], &Penalties::new()).unwrap()),
236/// vec!["These few",
237/// "words will",
238/// "unfortunately",
239/// "not wrap",
240/// "nicely."]);
241/// ```
242///
243/// The [`wrap_optimal_fit()`] function was used above to get better
244/// line breaks. It uses an advanced algorithm which tries to avoid
245/// short lines. This function is about 4 times faster than
246/// [`wrap_optimal_fit()`].
247///
248/// # Examples
249///
250/// Imagine you're building a house site and you have a number of
251/// tasks you need to execute. Things like pouring the foundation,
252/// framing, plumbing, electrical cabling, and installing insulation.
253///
254/// The construction workers can only work during daytime, so they
255/// need to pack up everything at night. Because they need to secure
256/// their tools and move machines back to the garage, this process
257/// takes much more time than the time it would take them to simply
258/// switch to another task.
259///
260/// You would like to make a list of tasks to execute every day based
261/// on your estimates. You can model this with a program like this:
262///
263/// ```
264/// use textwrap::core::{Fragment, Word};
265/// use textwrap::wrap_algorithms::wrap_first_fit;
266///
267/// #[derive(Debug)]
268/// struct Task<'a> {
269/// name: &'a str,
270/// hours: f64, // Time needed to complete task.
271/// sweep: f64, // Time needed for a quick sweep after task during the day.
272/// cleanup: f64, // Time needed for full cleanup if day ends with this task.
273/// }
274///
275/// impl Fragment for Task<'_> {
276/// fn width(&self) -> f64 { self.hours }
277/// fn whitespace_width(&self) -> f64 { self.sweep }
278/// fn penalty_width(&self) -> f64 { self.cleanup }
279/// }
280///
281/// // The morning tasks
282/// let tasks = vec![
283/// Task { name: "Foundation", hours: 4.0, sweep: 2.0, cleanup: 3.0 },
284/// Task { name: "Framing", hours: 3.0, sweep: 1.0, cleanup: 2.0 },
285/// Task { name: "Plumbing", hours: 2.0, sweep: 2.0, cleanup: 2.0 },
286/// Task { name: "Electrical", hours: 2.0, sweep: 1.0, cleanup: 2.0 },
287/// Task { name: "Insulation", hours: 2.0, sweep: 1.0, cleanup: 2.0 },
288/// Task { name: "Drywall", hours: 3.0, sweep: 1.0, cleanup: 2.0 },
289/// Task { name: "Floors", hours: 3.0, sweep: 1.0, cleanup: 2.0 },
290/// Task { name: "Countertops", hours: 1.0, sweep: 1.0, cleanup: 2.0 },
291/// Task { name: "Bathrooms", hours: 2.0, sweep: 1.0, cleanup: 2.0 },
292/// ];
293///
294/// // Fill tasks into days, taking `day_length` into account. The
295/// // output shows the hours worked per day along with the names of
296/// // the tasks for that day.
297/// fn assign_days<'a>(tasks: &[Task<'a>], day_length: f64) -> Vec<(f64, Vec<&'a str>)> {
298/// let mut days = Vec::new();
299/// // Assign tasks to days. The assignment is a vector of slices,
300/// // with a slice per day.
301/// let assigned_days: Vec<&[Task<'a>]> = wrap_first_fit(&tasks, &[day_length]);
302/// for day in assigned_days.iter() {
303/// let last = day.last().unwrap();
304/// let work_hours: f64 = day.iter().map(|t| t.hours + t.sweep).sum();
305/// let names = day.iter().map(|t| t.name).collect::<Vec<_>>();
306/// days.push((work_hours - last.sweep + last.cleanup, names));
307/// }
308/// days
309/// }
310///
311/// // With a single crew working 8 hours a day:
312/// assert_eq!(
313/// assign_days(&tasks, 8.0),
314/// [
315/// (7.0, vec!["Foundation"]),
316/// (8.0, vec!["Framing", "Plumbing"]),
317/// (7.0, vec!["Electrical", "Insulation"]),
318/// (5.0, vec!["Drywall"]),
319/// (7.0, vec!["Floors", "Countertops"]),
320/// (4.0, vec!["Bathrooms"]),
321/// ]
322/// );
323///
324/// // With two crews working in shifts, 16 hours a day:
325/// assert_eq!(
326/// assign_days(&tasks, 16.0),
327/// [
328/// (14.0, vec!["Foundation", "Framing", "Plumbing"]),
329/// (15.0, vec!["Electrical", "Insulation", "Drywall", "Floors"]),
330/// (6.0, vec!["Countertops", "Bathrooms"]),
331/// ]
332/// );
333/// ```
334///
335/// Apologies to anyone who actually knows how to build a house and
336/// knows how long each step takes :-)
337pub fn wrap_first_fit<'a, T: Fragment>(fragments: &'a [T], line_widths: &[f64]) -> Vec<&'a [T]> {
338 // The final line width is used for all remaining lines.
339 let default_line_width = line_widths.last().copied().unwrap_or(0.0);
340 let mut lines = Vec::new();
341 let mut start = 0;
342 let mut width = 0.0;
343
344 for (idx, fragment) in fragments.iter().enumerate() {
345 let line_width = line_widths
346 .get(lines.len())
347 .copied()
348 .unwrap_or(default_line_width);
349 if width + fragment.width() + fragment.penalty_width() > line_width && idx > start {
350 lines.push(&fragments[start..idx]);
351 start = idx;
352 width = 0.0;
353 }
354 width += fragment.width() + fragment.whitespace_width();
355 }
356 lines.push(&fragments[start..]);
357 lines
358}
359
360#[cfg(test)]
361mod tests {
362 use super::*;
363 use alloc::vec;
364
365 #[derive(Debug, PartialEq)]
366 struct Word(f64);
367
368 #[rustfmt::skip]
369 impl Fragment for Word {
370 fn width(&self) -> f64 { self.0 }
371 fn whitespace_width(&self) -> f64 { 1.0 }
372 fn penalty_width(&self) -> f64 { 0.0 }
373 }
374
375 #[test]
376 fn wrap_string_longer_than_f64() {
377 let words = vec![
378 Word(1e307),
379 Word(2e307),
380 Word(3e307),
381 Word(4e307),
382 Word(5e307),
383 Word(6e307),
384 ];
385 // Wrap at just under f64::MAX (~19e307). The tiny
386 // whitespace_widths disappear because of loss of precision.
387 assert_eq!(
388 wrap_first_fit(&words, &[15e307]),
389 &[
390 vec![
391 Word(1e307),
392 Word(2e307),
393 Word(3e307),
394 Word(4e307),
395 Word(5e307)
396 ],
397 vec![Word(6e307)]
398 ]
399 );
400 }
401}