aboutsummaryrefslogtreecommitdiff
path: root/src/wrap_algorithms.rs
diff options
context:
space:
mode:
Diffstat (limited to 'src/wrap_algorithms.rs')
-rw-r--r--src/wrap_algorithms.rs290
1 files changed, 207 insertions, 83 deletions
diff --git a/src/wrap_algorithms.rs b/src/wrap_algorithms.rs
index 368ef2a..5ca49c3 100644
--- a/src/wrap_algorithms.rs
+++ b/src/wrap_algorithms.rs
@@ -18,69 +18,149 @@
#[cfg(feature = "smawk")]
mod optimal_fit;
#[cfg(feature = "smawk")]
-pub use optimal_fit::{wrap_optimal_fit, OptimalFit};
+pub use optimal_fit::{wrap_optimal_fit, OverflowError, Penalties};
use crate::core::{Fragment, Word};
/// Describes how to wrap words into lines.
///
-/// The simplest approach is to wrap words one word at a time. This is
-/// implemented by [`FirstFit`]. If the `smawk` Cargo feature is
-/// enabled, a more complex algorithm is available, implemented by
-/// [`OptimalFit`], which will look at an entire paragraph at a time
-/// in order to find optimal line breaks.
-pub trait WrapAlgorithm: WrapAlgorithmClone + std::fmt::Debug {
- /// Wrap words according to line widths.
+/// The simplest approach is to wrap words one word at a time and
+/// accept the first way of wrapping which fit
+/// ([`WrapAlgorithm::FirstFit`]). If the `smawk` Cargo feature is
+/// enabled, a more complex algorithm is available which will look at
+/// an entire paragraph at a time in order to find optimal line breaks
+/// ([`WrapAlgorithm::OptimalFit`]).
+#[derive(Clone, Copy)]
+pub enum WrapAlgorithm {
+ /// Wrap words using a fast and simple algorithm.
///
- /// The `line_widths` slice gives the target line width for each
- /// line (the last slice element is repeated as necessary). This
- /// can be used to implement hanging indentation.
+ /// This algorithm uses no look-ahead when finding line breaks.
+ /// Implemented by [`wrap_first_fit`], please see that function for
+ /// details and examples.
+ FirstFit,
+
+ /// Wrap words using an advanced algorithm with look-ahead.
///
- /// Please see the implementors of the trait for examples.
- fn wrap<'a, 'b>(&self, words: &'b [Word<'a>], line_widths: &'b [usize]) -> Vec<&'b [Word<'a>]>;
-}
+ /// This wrapping algorithm considers the entire paragraph to find
+ /// optimal line breaks. When wrapping text, "penalties" are
+ /// assigned to line breaks based on the gaps left at the end of
+ /// lines. See [`Penalties`] for details.
+ ///
+ /// The underlying wrapping algorithm is implemented by
+ /// [`wrap_optimal_fit`], please see that function for examples.
+ ///
+ /// **Note:** Only available when the `smawk` Cargo feature is
+ /// enabled.
+ #[cfg(feature = "smawk")]
+ OptimalFit(Penalties),
-// The internal `WrapAlgorithmClone` trait is allows us to implement
-// `Clone` for `Box<dyn WrapAlgorithm>`. This in used in the
-// `From<&Options<'_, WrapAlgo, WordSep, WordSplit>> for Options<'a,
-// WrapAlgo, WordSep, WordSplit>` implementation.
-#[doc(hidden)]
-pub trait WrapAlgorithmClone {
- fn clone_box(&self) -> Box<dyn WrapAlgorithm>;
+ /// Custom wrapping function.
+ ///
+ /// Use this if you want to implement your own wrapping algorithm.
+ /// The function can freely decide how to turn a slice of
+ /// [`Word`]s into lines.
+ ///
+ /// # Example
+ ///
+ /// ```
+ /// use textwrap::core::Word;
+ /// use textwrap::{wrap, Options, WrapAlgorithm};
+ ///
+ /// fn stair<'a, 'b>(words: &'b [Word<'a>], _: &'b [usize]) -> Vec<&'b [Word<'a>]> {
+ /// let mut lines = Vec::new();
+ /// let mut step = 1;
+ /// let mut start_idx = 0;
+ /// while start_idx + step <= words.len() {
+ /// lines.push(&words[start_idx .. start_idx+step]);
+ /// start_idx += step;
+ /// step += 1;
+ /// }
+ /// lines
+ /// }
+ ///
+ /// let options = Options::new(10).wrap_algorithm(WrapAlgorithm::Custom(stair));
+ /// assert_eq!(wrap("First, second, third, fourth, fifth, sixth", options),
+ /// vec!["First,",
+ /// "second, third,",
+ /// "fourth, fifth, sixth"]);
+ /// ```
+ Custom(for<'a, 'b> fn(words: &'b [Word<'a>], line_widths: &'b [usize]) -> Vec<&'b [Word<'a>]>),
}
-impl<T: WrapAlgorithm + Clone + 'static> WrapAlgorithmClone for T {
- fn clone_box(&self) -> Box<dyn WrapAlgorithm> {
- Box::new(self.clone())
+impl std::fmt::Debug for WrapAlgorithm {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ match self {
+ WrapAlgorithm::FirstFit => f.write_str("FirstFit"),
+ #[cfg(feature = "smawk")]
+ WrapAlgorithm::OptimalFit(penalties) => write!(f, "OptimalFit({:?})", penalties),
+ WrapAlgorithm::Custom(_) => f.write_str("Custom(...)"),
+ }
}
}
-impl Clone for Box<dyn WrapAlgorithm> {
- fn clone(&self) -> Box<dyn WrapAlgorithm> {
- use std::ops::Deref;
- self.deref().clone_box()
- }
-}
+impl WrapAlgorithm {
+ /// Create new wrap algorithm.
+ ///
+ /// The best wrapping algorithm is used by default, i.e.,
+ /// [`WrapAlgorithm::OptimalFit`] if available, otherwise
+ /// [`WrapAlgorithm::FirstFit`].
+ pub const fn new() -> Self {
+ #[cfg(not(feature = "smawk"))]
+ {
+ WrapAlgorithm::FirstFit
+ }
-impl WrapAlgorithm for Box<dyn WrapAlgorithm> {
- fn wrap<'a, 'b>(&self, words: &'b [Word<'a>], line_widths: &'b [usize]) -> Vec<&'b [Word<'a>]> {
- use std::ops::Deref;
- self.deref().wrap(words, line_widths)
+ #[cfg(feature = "smawk")]
+ {
+ WrapAlgorithm::new_optimal_fit()
+ }
}
-}
-/// Wrap words using a fast and simple algorithm.
-///
-/// This algorithm uses no look-ahead when finding line breaks.
-/// Implemented by [`wrap_first_fit`], please see that function for
-/// details and examples.
-#[derive(Clone, Copy, Debug, Default)]
-pub struct FirstFit;
+ /// New [`WrapAlgorithm::OptimalFit`] with default penalties. This
+ /// works well for monospace text.
+ ///
+ /// **Note:** Only available when the `smawk` Cargo feature is
+ /// enabled.
+ #[cfg(feature = "smawk")]
+ pub const fn new_optimal_fit() -> Self {
+ WrapAlgorithm::OptimalFit(Penalties::new())
+ }
-impl WrapAlgorithm for FirstFit {
+ /// Wrap words according to line widths.
+ ///
+ /// The `line_widths` slice gives the target line width for each
+ /// line (the last slice element is repeated as necessary). This
+ /// can be used to implement hanging indentation.
#[inline]
- fn wrap<'a, 'b>(&self, words: &'b [Word<'a>], line_widths: &'b [usize]) -> Vec<&'b [Word<'a>]> {
- wrap_first_fit(words, line_widths)
+ pub fn wrap<'a, 'b>(
+ &self,
+ words: &'b [Word<'a>],
+ line_widths: &'b [usize],
+ ) -> Vec<&'b [Word<'a>]> {
+ // Every integer up to 2u64.pow(f64::MANTISSA_DIGITS) = 2**53
+ // = 9_007_199_254_740_992 can be represented without loss by
+ // a f64. Larger line widths will be rounded to the nearest
+ // representable number.
+ let f64_line_widths = line_widths.iter().map(|w| *w as f64).collect::<Vec<_>>();
+
+ match self {
+ WrapAlgorithm::FirstFit => wrap_first_fit(words, &f64_line_widths),
+
+ #[cfg(feature = "smawk")]
+ WrapAlgorithm::OptimalFit(penalties) => {
+ // The computation cannnot overflow when the line
+ // widths are restricted to usize.
+ wrap_optimal_fit(words, &f64_line_widths, penalties).unwrap()
+ }
+
+ WrapAlgorithm::Custom(func) => func(words, line_widths),
+ }
+ }
+}
+
+impl Default for WrapAlgorithm {
+ fn default() -> Self {
+ WrapAlgorithm::new()
}
}
@@ -107,8 +187,8 @@ impl WrapAlgorithm for FirstFit {
///
/// ```
/// use textwrap::core::Word;
-/// use textwrap::wrap_algorithms;
-/// use textwrap::word_separators::{AsciiSpace, WordSeparator};
+/// use textwrap::wrap_algorithms::wrap_first_fit;
+/// use textwrap::WordSeparator;
///
/// // Helper to convert wrapped lines to a Vec<String>.
/// fn lines_to_strings(lines: Vec<&[Word<'_>]>) -> Vec<String> {
@@ -118,8 +198,8 @@ impl WrapAlgorithm for FirstFit {
/// }
///
/// let text = "These few words will unfortunately not wrap nicely.";
-/// let words = AsciiSpace.find_words(text).collect::<Vec<_>>();
-/// assert_eq!(lines_to_strings(wrap_algorithms::wrap_first_fit(&words, &[15])),
+/// let words = WordSeparator::AsciiSpace.find_words(text).collect::<Vec<_>>();
+/// assert_eq!(lines_to_strings(wrap_first_fit(&words, &[15.0])),
/// vec!["These few words",
/// "will", // <-- short line
/// "unfortunately",
@@ -128,7 +208,9 @@ impl WrapAlgorithm for FirstFit {
///
/// // We can avoid the short line if we look ahead:
/// #[cfg(feature = "smawk")]
-/// assert_eq!(lines_to_strings(wrap_algorithms::wrap_optimal_fit(&words, &[15])),
+/// use textwrap::wrap_algorithms::{wrap_optimal_fit, Penalties};
+/// #[cfg(feature = "smawk")]
+/// assert_eq!(lines_to_strings(wrap_optimal_fit(&words, &[15.0], &Penalties::new()).unwrap()),
/// vec!["These few",
/// "words will",
/// "unfortunately",
@@ -157,47 +239,47 @@ impl WrapAlgorithm for FirstFit {
/// on your estimates. You can model this with a program like this:
///
/// ```
-/// use textwrap::wrap_algorithms::wrap_first_fit;
/// use textwrap::core::{Fragment, Word};
+/// use textwrap::wrap_algorithms::wrap_first_fit;
///
/// #[derive(Debug)]
/// struct Task<'a> {
/// name: &'a str,
-/// hours: usize, // Time needed to complete task.
-/// sweep: usize, // Time needed for a quick sweep after task during the day.
-/// cleanup: usize, // Time needed for full cleanup if day ends with this task.
+/// hours: f64, // Time needed to complete task.
+/// sweep: f64, // Time needed for a quick sweep after task during the day.
+/// cleanup: f64, // Time needed for full cleanup if day ends with this task.
/// }
///
/// impl Fragment for Task<'_> {
-/// fn width(&self) -> usize { self.hours }
-/// fn whitespace_width(&self) -> usize { self.sweep }
-/// fn penalty_width(&self) -> usize { self.cleanup }
+/// fn width(&self) -> f64 { self.hours }
+/// fn whitespace_width(&self) -> f64 { self.sweep }
+/// fn penalty_width(&self) -> f64 { self.cleanup }
/// }
///
/// // The morning tasks
/// let tasks = vec![
-/// Task { name: "Foundation", hours: 4, sweep: 2, cleanup: 3 },
-/// Task { name: "Framing", hours: 3, sweep: 1, cleanup: 2 },
-/// Task { name: "Plumbing", hours: 2, sweep: 2, cleanup: 2 },
-/// Task { name: "Electrical", hours: 2, sweep: 1, cleanup: 2 },
-/// Task { name: "Insulation", hours: 2, sweep: 1, cleanup: 2 },
-/// Task { name: "Drywall", hours: 3, sweep: 1, cleanup: 2 },
-/// Task { name: "Floors", hours: 3, sweep: 1, cleanup: 2 },
-/// Task { name: "Countertops", hours: 1, sweep: 1, cleanup: 2 },
-/// Task { name: "Bathrooms", hours: 2, sweep: 1, cleanup: 2 },
+/// Task { name: "Foundation", hours: 4.0, sweep: 2.0, cleanup: 3.0 },
+/// Task { name: "Framing", hours: 3.0, sweep: 1.0, cleanup: 2.0 },
+/// Task { name: "Plumbing", hours: 2.0, sweep: 2.0, cleanup: 2.0 },
+/// Task { name: "Electrical", hours: 2.0, sweep: 1.0, cleanup: 2.0 },
+/// Task { name: "Insulation", hours: 2.0, sweep: 1.0, cleanup: 2.0 },
+/// Task { name: "Drywall", hours: 3.0, sweep: 1.0, cleanup: 2.0 },
+/// Task { name: "Floors", hours: 3.0, sweep: 1.0, cleanup: 2.0 },
+/// Task { name: "Countertops", hours: 1.0, sweep: 1.0, cleanup: 2.0 },
+/// Task { name: "Bathrooms", hours: 2.0, sweep: 1.0, cleanup: 2.0 },
/// ];
///
/// // Fill tasks into days, taking `day_length` into account. The
/// // output shows the hours worked per day along with the names of
/// // the tasks for that day.
-/// fn assign_days<'a>(tasks: &[Task<'a>], day_length: usize) -> Vec<(usize, Vec<&'a str>)> {
+/// fn assign_days<'a>(tasks: &[Task<'a>], day_length: f64) -> Vec<(f64, Vec<&'a str>)> {
/// let mut days = Vec::new();
/// // Assign tasks to days. The assignment is a vector of slices,
/// // with a slice per day.
/// let assigned_days: Vec<&[Task<'a>]> = wrap_first_fit(&tasks, &[day_length]);
/// for day in assigned_days.iter() {
/// let last = day.last().unwrap();
-/// let work_hours: usize = day.iter().map(|t| t.hours + t.sweep).sum();
+/// let work_hours: f64 = day.iter().map(|t| t.hours + t.sweep).sum();
/// let names = day.iter().map(|t| t.name).collect::<Vec<_>>();
/// days.push((work_hours - last.sweep + last.cleanup, names));
/// }
@@ -206,24 +288,24 @@ impl WrapAlgorithm for FirstFit {
///
/// // With a single crew working 8 hours a day:
/// assert_eq!(
-/// assign_days(&tasks, 8),
+/// assign_days(&tasks, 8.0),
/// [
-/// (7, vec!["Foundation"]),
-/// (8, vec!["Framing", "Plumbing"]),
-/// (7, vec!["Electrical", "Insulation"]),
-/// (5, vec!["Drywall"]),
-/// (7, vec!["Floors", "Countertops"]),
-/// (4, vec!["Bathrooms"]),
+/// (7.0, vec!["Foundation"]),
+/// (8.0, vec!["Framing", "Plumbing"]),
+/// (7.0, vec!["Electrical", "Insulation"]),
+/// (5.0, vec!["Drywall"]),
+/// (7.0, vec!["Floors", "Countertops"]),
+/// (4.0, vec!["Bathrooms"]),
/// ]
/// );
///
/// // With two crews working in shifts, 16 hours a day:
/// assert_eq!(
-/// assign_days(&tasks, 16),
+/// assign_days(&tasks, 16.0),
/// [
-/// (14, vec!["Foundation", "Framing", "Plumbing"]),
-/// (15, vec!["Electrical", "Insulation", "Drywall", "Floors"]),
-/// (6, vec!["Countertops", "Bathrooms"]),
+/// (14.0, vec!["Foundation", "Framing", "Plumbing"]),
+/// (15.0, vec!["Electrical", "Insulation", "Drywall", "Floors"]),
+/// (6.0, vec!["Countertops", "Bathrooms"]),
/// ]
/// );
/// ```
@@ -232,13 +314,13 @@ impl WrapAlgorithm for FirstFit {
/// knows how long each step takes :-)
pub fn wrap_first_fit<'a, 'b, T: Fragment>(
fragments: &'a [T],
- line_widths: &'b [usize],
+ line_widths: &'b [f64],
) -> Vec<&'a [T]> {
// The final line width is used for all remaining lines.
- let default_line_width = line_widths.last().copied().unwrap_or(0);
+ let default_line_width = line_widths.last().copied().unwrap_or(0.0);
let mut lines = Vec::new();
let mut start = 0;
- let mut width = 0;
+ let mut width = 0.0;
for (idx, fragment) in fragments.iter().enumerate() {
let line_width = line_widths
@@ -248,10 +330,52 @@ pub fn wrap_first_fit<'a, 'b, T: Fragment>(
if width + fragment.width() + fragment.penalty_width() > line_width && idx > start {
lines.push(&fragments[start..idx]);
start = idx;
- width = 0;
+ width = 0.0;
}
width += fragment.width() + fragment.whitespace_width();
}
lines.push(&fragments[start..]);
lines
}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[derive(Debug, PartialEq)]
+ struct Word(f64);
+
+ #[rustfmt::skip]
+ impl Fragment for Word {
+ fn width(&self) -> f64 { self.0 }
+ fn whitespace_width(&self) -> f64 { 1.0 }
+ fn penalty_width(&self) -> f64 { 0.0 }
+ }
+
+ #[test]
+ fn wrap_string_longer_than_f64() {
+ let words = vec![
+ Word(1e307),
+ Word(2e307),
+ Word(3e307),
+ Word(4e307),
+ Word(5e307),
+ Word(6e307),
+ ];
+ // Wrap at just under f64::MAX (~19e307). The tiny
+ // whitespace_widths disappear because of loss of precision.
+ assert_eq!(
+ wrap_first_fit(&words, &[15e307]),
+ &[
+ vec![
+ Word(1e307),
+ Word(2e307),
+ Word(3e307),
+ Word(4e307),
+ Word(5e307)
+ ],
+ vec![Word(6e307)]
+ ]
+ );
+ }
+}