315 lines
9.3 KiB
Rust
315 lines
9.3 KiB
Rust
use std::cmp::Ordering;
|
|
use std::iter::Skip;
|
|
use std::str::Chars;
|
|
|
|
use unicode_width::UnicodeWidthChar;
|
|
|
|
/// The action performed by [`StrShortener`].
|
|
#[derive(Clone, Copy, PartialEq, Eq)]
|
|
pub enum TextAction {
|
|
/// Yield a spacer.
|
|
Spacer,
|
|
/// Terminate state reached.
|
|
Terminate,
|
|
/// Yield a shortener.
|
|
Shortener,
|
|
/// Yield a character.
|
|
Char,
|
|
}
|
|
|
|
/// The direction which we should shorten.
|
|
#[derive(Clone, Copy, PartialEq, Eq)]
|
|
pub enum ShortenDirection {
|
|
/// Shorten to the start of the string.
|
|
Left,
|
|
|
|
/// Shorten to the end of the string.
|
|
Right,
|
|
}
|
|
|
|
/// Iterator that yield shortened version of the text.
|
|
pub struct StrShortener<'a> {
|
|
chars: Skip<Chars<'a>>,
|
|
accumulted_len: usize,
|
|
max_width: usize,
|
|
direction: ShortenDirection,
|
|
shortener: Option<char>,
|
|
text_action: TextAction,
|
|
}
|
|
|
|
impl<'a> StrShortener<'a> {
|
|
pub fn new(
|
|
text: &'a str,
|
|
max_width: usize,
|
|
direction: ShortenDirection,
|
|
mut shortener: Option<char>,
|
|
) -> Self {
|
|
if text.is_empty() {
|
|
// If we don't have any text don't produce a shortener for it.
|
|
let _ = shortener.take();
|
|
}
|
|
|
|
if direction == ShortenDirection::Right {
|
|
return Self {
|
|
chars: text.chars().skip(0),
|
|
accumulted_len: 0,
|
|
text_action: TextAction::Char,
|
|
max_width,
|
|
direction,
|
|
shortener,
|
|
};
|
|
}
|
|
|
|
let mut offset = 0;
|
|
let mut current_len = 0;
|
|
|
|
let mut iter = text.chars().rev().enumerate();
|
|
|
|
while let Some((idx, ch)) = iter.next() {
|
|
let ch_width = ch.width().unwrap_or(1);
|
|
current_len += ch_width;
|
|
|
|
match current_len.cmp(&max_width) {
|
|
// We can only be here if we've faced wide character or we've already
|
|
// handled equality situation. Anyway, break.
|
|
Ordering::Greater => break,
|
|
Ordering::Equal => {
|
|
if shortener.is_some() && iter.clone().next().is_some() {
|
|
// We have one more character after, shortener will accumulate for
|
|
// the `current_len`.
|
|
break;
|
|
} else {
|
|
// The match is exact, consume shortener.
|
|
let _ = shortener.take();
|
|
}
|
|
},
|
|
Ordering::Less => (),
|
|
}
|
|
|
|
offset = idx + 1;
|
|
}
|
|
|
|
// Consume the iterator to count the number of characters in it.
|
|
let num_chars = iter.last().map(|(idx, _)| idx + 1).unwrap_or(offset);
|
|
let skip_chars = num_chars - offset;
|
|
|
|
let text_action = if num_chars <= max_width || shortener.is_none() {
|
|
TextAction::Char
|
|
} else {
|
|
TextAction::Shortener
|
|
};
|
|
|
|
let chars = text.chars().skip(skip_chars);
|
|
|
|
Self { chars, accumulted_len: 0, text_action, max_width, direction, shortener }
|
|
}
|
|
}
|
|
|
|
impl<'a> Iterator for StrShortener<'a> {
|
|
type Item = char;
|
|
|
|
fn next(&mut self) -> Option<Self::Item> {
|
|
match self.text_action {
|
|
TextAction::Spacer => {
|
|
self.text_action = TextAction::Char;
|
|
Some(' ')
|
|
},
|
|
TextAction::Terminate => {
|
|
// We've reached the termination state.
|
|
None
|
|
},
|
|
TextAction::Shortener => {
|
|
// When we shorten from the left we yield the shortener first and process the rest.
|
|
self.text_action = if self.direction == ShortenDirection::Left {
|
|
TextAction::Char
|
|
} else {
|
|
TextAction::Terminate
|
|
};
|
|
|
|
// Consume the shortener to avoid yielding it later when shortening left.
|
|
self.shortener.take()
|
|
},
|
|
TextAction::Char => {
|
|
let ch = self.chars.next()?;
|
|
let ch_width = ch.width().unwrap_or(1);
|
|
|
|
// Advance width.
|
|
self.accumulted_len += ch_width;
|
|
|
|
if self.accumulted_len > self.max_width {
|
|
self.text_action = TextAction::Terminate;
|
|
return self.shortener;
|
|
} else if self.accumulted_len == self.max_width && self.shortener.is_some() {
|
|
// Check if we have a next char.
|
|
let has_next = self.chars.clone().next().is_some();
|
|
|
|
// We should terminate after that.
|
|
self.text_action = TextAction::Terminate;
|
|
|
|
return has_next.then(|| self.shortener.unwrap()).or(Some(ch));
|
|
}
|
|
|
|
// Add a spacer for wide character.
|
|
if ch_width == 2 {
|
|
self.text_action = TextAction::Spacer;
|
|
}
|
|
|
|
Some(ch)
|
|
},
|
|
}
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn into_shortened_with_shortener() {
|
|
let s = "Hello";
|
|
let len = s.chars().count();
|
|
assert_eq!(
|
|
"",
|
|
StrShortener::new("", 1, ShortenDirection::Left, Some('.')).collect::<String>()
|
|
);
|
|
|
|
assert_eq!(
|
|
".",
|
|
StrShortener::new(s, 1, ShortenDirection::Right, Some('.')).collect::<String>()
|
|
);
|
|
|
|
assert_eq!(
|
|
".",
|
|
StrShortener::new(s, 1, ShortenDirection::Left, Some('.')).collect::<String>()
|
|
);
|
|
|
|
assert_eq!(
|
|
"H.",
|
|
StrShortener::new(s, 2, ShortenDirection::Right, Some('.')).collect::<String>()
|
|
);
|
|
|
|
assert_eq!(
|
|
".o",
|
|
StrShortener::new(s, 2, ShortenDirection::Left, Some('.')).collect::<String>()
|
|
);
|
|
|
|
assert_eq!(
|
|
s,
|
|
&StrShortener::new(s, len * 2, ShortenDirection::Right, Some('.')).collect::<String>()
|
|
);
|
|
|
|
assert_eq!(
|
|
s,
|
|
&StrShortener::new(s, len * 2, ShortenDirection::Left, Some('.')).collect::<String>()
|
|
);
|
|
|
|
let s = "こJんにちはP";
|
|
let len = 2 + 1 + 2 + 2 + 2 + 2 + 1;
|
|
assert_eq!(
|
|
".",
|
|
&StrShortener::new(s, 1, ShortenDirection::Right, Some('.')).collect::<String>()
|
|
);
|
|
|
|
assert_eq!(
|
|
&".",
|
|
&StrShortener::new(s, 1, ShortenDirection::Left, Some('.')).collect::<String>()
|
|
);
|
|
|
|
assert_eq!(
|
|
".",
|
|
&StrShortener::new(s, 2, ShortenDirection::Right, Some('.')).collect::<String>()
|
|
);
|
|
|
|
assert_eq!(
|
|
".P",
|
|
&StrShortener::new(s, 2, ShortenDirection::Left, Some('.')).collect::<String>()
|
|
);
|
|
|
|
assert_eq!(
|
|
"こ .",
|
|
&StrShortener::new(s, 3, ShortenDirection::Right, Some('.')).collect::<String>()
|
|
);
|
|
|
|
assert_eq!(
|
|
".P",
|
|
&StrShortener::new(s, 3, ShortenDirection::Left, Some('.')).collect::<String>()
|
|
);
|
|
|
|
assert_eq!(
|
|
"こ Jん に ち は P",
|
|
&StrShortener::new(s, len * 2, ShortenDirection::Left, Some('.')).collect::<String>()
|
|
);
|
|
|
|
assert_eq!(
|
|
"こ Jん に ち は P",
|
|
&StrShortener::new(s, len * 2, ShortenDirection::Right, Some('.')).collect::<String>()
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn into_shortened_without_shortener() {
|
|
let s = "Hello";
|
|
assert_eq!("", StrShortener::new("", 1, ShortenDirection::Left, None).collect::<String>());
|
|
|
|
assert_eq!(
|
|
"H",
|
|
&StrShortener::new(s, 1, ShortenDirection::Right, None).collect::<String>()
|
|
);
|
|
|
|
assert_eq!("o", &StrShortener::new(s, 1, ShortenDirection::Left, None).collect::<String>());
|
|
|
|
assert_eq!(
|
|
"He",
|
|
&StrShortener::new(s, 2, ShortenDirection::Right, None).collect::<String>()
|
|
);
|
|
|
|
assert_eq!(
|
|
"lo",
|
|
&StrShortener::new(s, 2, ShortenDirection::Left, None).collect::<String>()
|
|
);
|
|
|
|
assert_eq!(
|
|
&s,
|
|
&StrShortener::new(s, s.len(), ShortenDirection::Right, None).collect::<String>()
|
|
);
|
|
|
|
assert_eq!(
|
|
&s,
|
|
&StrShortener::new(s, s.len(), ShortenDirection::Left, None).collect::<String>()
|
|
);
|
|
|
|
let s = "こJんにちはP";
|
|
let len = 2 + 1 + 2 + 2 + 2 + 2 + 1;
|
|
assert_eq!("", &StrShortener::new(s, 1, ShortenDirection::Right, None).collect::<String>());
|
|
|
|
assert_eq!("P", &StrShortener::new(s, 1, ShortenDirection::Left, None).collect::<String>());
|
|
|
|
assert_eq!(
|
|
"こ ",
|
|
&StrShortener::new(s, 2, ShortenDirection::Right, None).collect::<String>()
|
|
);
|
|
|
|
assert_eq!("P", &StrShortener::new(s, 2, ShortenDirection::Left, None).collect::<String>());
|
|
|
|
assert_eq!(
|
|
"こ J",
|
|
&StrShortener::new(s, 3, ShortenDirection::Right, None).collect::<String>()
|
|
);
|
|
|
|
assert_eq!(
|
|
"は P",
|
|
&StrShortener::new(s, 3, ShortenDirection::Left, None).collect::<String>()
|
|
);
|
|
|
|
assert_eq!(
|
|
"こ Jん に ち は P",
|
|
&StrShortener::new(s, len, ShortenDirection::Left, None).collect::<String>()
|
|
);
|
|
|
|
assert_eq!(
|
|
"こ Jん に ち は P",
|
|
&StrShortener::new(s, len, ShortenDirection::Right, None).collect::<String>()
|
|
);
|
|
}
|
|
}
|