use crate::shared::char_offset_to_byte_index;
use std::error::Error;
use std::fmt;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct AbbreviateError {
message: String,
}
impl AbbreviateError {
fn new(message: impl Into<String>) -> Self {
Self {
message: message.into(),
}
}
}
impl fmt::Display for AbbreviateError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.message)
}
}
impl Error for AbbreviateError {}
pub trait StringAbbreviate {
fn abbreviate(&self, max_width: usize) -> Result<String, AbbreviateError>;
fn abbreviate_with_offset(
&self,
offset: usize,
max_width: usize,
) -> Result<String, AbbreviateError>;
fn abbreviate_with_marker(
&self,
abbrev_marker: &str,
max_width: usize,
) -> Result<String, AbbreviateError>;
fn abbreviate_with_marker_and_offset(
&self,
abbrev_marker: &str,
offset: usize,
max_width: usize,
) -> Result<String, AbbreviateError>;
#[must_use]
fn abbreviate_middle(&self, middle: &str, length: usize) -> String;
#[must_use]
fn overlay(&self, overlay: &str, start: usize, end: usize) -> String;
}
impl StringAbbreviate for str {
fn abbreviate(&self, max_width: usize) -> Result<String, AbbreviateError> {
self.abbreviate_with_marker_and_offset("...", 0, max_width)
}
fn abbreviate_with_offset(
&self,
offset: usize,
max_width: usize,
) -> Result<String, AbbreviateError> {
self.abbreviate_with_marker_and_offset("...", offset, max_width)
}
fn abbreviate_with_marker(
&self,
abbrev_marker: &str,
max_width: usize,
) -> Result<String, AbbreviateError> {
self.abbreviate_with_marker_and_offset(abbrev_marker, 0, max_width)
}
fn abbreviate_with_marker_and_offset(
&self,
abbrev_marker: &str,
offset: usize,
max_width: usize,
) -> Result<String, AbbreviateError> {
if self.is_empty() {
return Ok(String::new());
}
let abbrev_marker_len = abbrev_marker.chars().count();
let min_abbrev_width = abbrev_marker_len + 1;
let min_abbrev_width_offset = abbrev_marker_len + abbrev_marker_len + 1;
if max_width < min_abbrev_width {
return Err(AbbreviateError::new(format!(
"Minimum abbreviation width is {}",
min_abbrev_width
)));
}
let str_len = self.chars().count();
if str_len <= max_width {
return Ok(self.to_string());
}
let offset = adjust_offset(offset, max_width, abbrev_marker_len, str_len);
if offset <= abbrev_marker_len + 1 {
let byte_end = char_offset_to_byte_index(self, max_width - abbrev_marker_len);
return Ok(format!("{}{}", &self[..byte_end], abbrev_marker));
}
if max_width < min_abbrev_width_offset {
return Err(AbbreviateError::new(format!(
"Minimum abbreviation width with offset is {}",
min_abbrev_width_offset
)));
}
if offset + max_width - abbrev_marker_len < str_len {
let byte_offset = char_offset_to_byte_index(self, offset);
let abbreviated = self[byte_offset..].abbreviate_with_marker_and_offset(
abbrev_marker,
0,
max_width - abbrev_marker_len,
)?;
return Ok(format!("{}{}", abbrev_marker, abbreviated));
}
let start_pos = str_len.saturating_sub(max_width.saturating_sub(abbrev_marker_len));
let byte_offset = char_offset_to_byte_index(self, start_pos);
Ok(format!("{}{}", abbrev_marker, &self[byte_offset..]))
}
fn abbreviate_middle(&self, middle: &str, length: usize) -> String {
let str_len = self.chars().count();
let middle_len = middle.chars().count();
let missing_requirements = self.is_empty()
|| middle.is_empty()
|| length >= str_len
|| length == 0
|| length < middle_len + 2;
if missing_requirements {
return self.to_string();
}
let target_string = length - middle_len;
let start_offset = target_string / 2 + target_string % 2;
let end_offset = str_len - target_string / 2;
let start_byte = char_offset_to_byte_index(self, start_offset);
let end_byte = char_offset_to_byte_index(self, end_offset);
format!("{}{}{}", &self[..start_byte], middle, &self[end_byte..])
}
fn overlay(&self, overlay: &str, start: usize, end: usize) -> String {
let len = self.chars().count();
let start = start.min(len);
let end = end.min(len);
let (start, end) = if start > end {
(end, start)
} else {
(start, end)
};
let start_byte = char_offset_to_byte_index(self, start);
let end_byte = char_offset_to_byte_index(self, end);
format!("{}{}{}", &self[..start_byte], overlay, &self[end_byte..])
}
}
fn adjust_offset(
offset: usize,
max_width: usize,
abbrev_marker_len: usize,
str_len: usize,
) -> usize {
let offset = offset.min(str_len);
if str_len.saturating_sub(offset) < max_width.saturating_sub(abbrev_marker_len) {
str_len.saturating_sub(max_width.saturating_sub(abbrev_marker_len))
} else {
offset
}
}
#[cfg(test)]
mod tests {
use super::*;
const ABBREVIATE_TEST_STRING: &str = "abcdefghijklmno";
fn assert_abbreviate_with_offset(expected: &str, offset: usize, max_width: usize) {
assert_eq!(
ABBREVIATE_TEST_STRING
.abbreviate_with_offset(offset, max_width)
.unwrap(),
expected
);
}
fn assert_abbreviate_with_marker_and_offset(
expected: &str,
marker: &str,
offset: usize,
max_width: usize,
) {
assert_eq!(
ABBREVIATE_TEST_STRING
.abbreviate_with_marker_and_offset(marker, offset, max_width)
.unwrap(),
expected
);
}
mod abbreviate {
use super::*;
#[test]
fn empty_string() {
assert_eq!("".abbreviate(10).unwrap(), "");
}
#[test]
fn short_string() {
assert_eq!("short".abbreviate(10).unwrap(), "short");
}
#[test]
fn long_sentence() {
assert_eq!(
"Now is the time for all good men to come to the aid of their party."
.abbreviate(10)
.unwrap(),
"Now is ..."
);
}
#[test]
fn raspberry_peach_14() {
assert_eq!("raspberry peach".abbreviate(14).unwrap(), "raspberry p...");
}
#[test]
fn raspberry_peach_15() {
assert_eq!("raspberry peach".abbreviate(15).unwrap(), "raspberry peach");
}
#[test]
fn raspberry_peach_16() {
assert_eq!("raspberry peach".abbreviate(16).unwrap(), "raspberry peach");
}
#[test]
fn abcdefg_6() {
assert_eq!("abcdefg".abbreviate(6).unwrap(), "abc...");
}
#[test]
fn abcdefg_7() {
assert_eq!("abcdefg".abbreviate(7).unwrap(), "abcdefg");
}
#[test]
fn abcdefg_8() {
assert_eq!("abcdefg".abbreviate(8).unwrap(), "abcdefg");
}
#[test]
fn abcdefg_4() {
assert_eq!("abcdefg".abbreviate(4).unwrap(), "a...");
}
#[test]
fn empty_string_width_4() {
assert_eq!("".abbreviate(4).unwrap(), "");
}
#[test]
fn width_too_small() {
assert!("abc".abbreviate(3).is_err());
}
}
mod abbreviate_with_offset {
use super::*;
#[test]
fn empty_string() {
assert_eq!("".abbreviate_with_offset(0, 10).unwrap(), "");
}
#[test]
fn empty_string_offset_2() {
assert_eq!("".abbreviate_with_offset(2, 10).unwrap(), "");
}
#[test]
fn width_too_small() {
assert!("abcdefghij".abbreviate_with_offset(0, 3).is_err());
}
#[test]
fn offset_width_too_small() {
assert!("abcdefghij".abbreviate_with_offset(5, 6).is_err());
}
#[test]
fn raspberry_peach() {
assert_eq!(
"raspberry peach".abbreviate_with_offset(11, 15).unwrap(),
"raspberry peach"
);
}
#[test]
fn offset_0() {
assert_abbreviate_with_offset("abcdefg...", 0, 10);
}
#[test]
fn offset_1() {
assert_abbreviate_with_offset("abcdefg...", 1, 10);
}
#[test]
fn offset_2() {
assert_abbreviate_with_offset("abcdefg...", 2, 10);
}
#[test]
fn offset_3() {
assert_abbreviate_with_offset("abcdefg...", 3, 10);
}
#[test]
fn offset_4() {
assert_abbreviate_with_offset("abcdefg...", 4, 10);
}
#[test]
fn offset_5() {
assert_abbreviate_with_offset("...fghi...", 5, 10);
}
#[test]
fn offset_6() {
assert_abbreviate_with_offset("...ghij...", 6, 10);
}
#[test]
fn offset_7() {
assert_abbreviate_with_offset("...hijk...", 7, 10);
}
#[test]
fn offset_8() {
assert_abbreviate_with_offset("...ijklmno", 8, 10);
}
#[test]
fn offset_9() {
assert_abbreviate_with_offset("...ijklmno", 9, 10);
}
#[test]
fn offset_10() {
assert_abbreviate_with_offset("...ijklmno", 10, 10);
}
#[test]
fn offset_11() {
assert_abbreviate_with_offset("...ijklmno", 11, 10);
}
#[test]
fn offset_12() {
assert_abbreviate_with_offset("...ijklmno", 12, 10);
}
#[test]
fn offset_13() {
assert_abbreviate_with_offset("...ijklmno", 13, 10);
}
#[test]
fn offset_14() {
assert_abbreviate_with_offset("...ijklmno", 14, 10);
}
#[test]
fn offset_15() {
assert_abbreviate_with_offset("...ijklmno", 15, 10);
}
#[test]
fn offset_16() {
assert_abbreviate_with_offset("...ijklmno", 16, 10);
}
#[test]
fn offset_max() {
assert_abbreviate_with_offset("...ijklmno", usize::MAX, 10);
}
}
mod abbreviate_with_marker {
use super::*;
#[test]
fn empty_string() {
assert_eq!("".abbreviate_with_marker("...", 2).unwrap(), "");
}
#[test]
fn custom_marker() {
assert_eq!("waiheke".abbreviate_with_marker("**", 5).unwrap(), "wai**");
}
#[test]
fn long_marker() {
assert_eq!(
"And after a long time, he finally met his son."
.abbreviate_with_marker(",,,,", 10)
.unwrap(),
"And af,,,,"
);
}
#[test]
fn raspberry_peach_double_dot() {
assert_eq!(
"raspberry peach".abbreviate_with_marker("..", 14).unwrap(),
"raspberry pe.."
);
}
#[test]
fn raspberry_peach_long_marker() {
assert_eq!(
"raspberry peach"
.abbreviate_with_marker("---*---", 15)
.unwrap(),
"raspberry peach"
);
}
#[test]
fn raspberry_peach_single_dot() {
assert_eq!(
"raspberry peach".abbreviate_with_marker(".", 16).unwrap(),
"raspberry peach"
);
}
#[test]
fn triple_paren_marker() {
assert_eq!(
"abcdefg".abbreviate_with_marker("()(", 6).unwrap(),
"abc()("
);
}
#[test]
fn semicolon_marker() {
assert_eq!("abcdefg".abbreviate_with_marker(";", 7).unwrap(), "abcdefg");
}
#[test]
fn underscore_hyphen_marker() {
assert_eq!(
"abcdefg".abbreviate_with_marker("_-", 8).unwrap(),
"abcdefg"
);
}
#[test]
fn single_dot_marker() {
assert_eq!("abcdefg".abbreviate_with_marker(".", 4).unwrap(), "abc.");
}
#[test]
fn empty_string_width_4() {
assert_eq!("".abbreviate_with_marker("...", 4).unwrap(), "");
}
#[test]
fn width_too_small() {
assert!("abcdefghij".abbreviate_with_marker("...", 3).is_err());
}
}
mod abbreviate_with_marker_and_offset {
use super::*;
#[test]
fn empty_string() {
assert_eq!(
"".abbreviate_with_marker_and_offset("...", 0, 10).unwrap(),
""
);
}
#[test]
fn empty_string_offset_2() {
assert_eq!(
"".abbreviate_with_marker_and_offset("...", 2, 10).unwrap(),
""
);
}
#[test]
fn empty_marker() {
assert_eq!(
"abcdefg"
.abbreviate_with_marker_and_offset("", 2, 10)
.unwrap(),
"abcdefg"
);
}
#[test]
fn empty_marker_truncate() {
assert_eq!(
"abcdefg"
.abbreviate_with_marker_and_offset("", 0, 3)
.unwrap(),
"abc"
);
}
#[test]
fn empty_marker_offset() {
assert_eq!(
"abcdefg"
.abbreviate_with_marker_and_offset("", 2, 3)
.unwrap(),
"cde"
);
}
#[test]
fn width_too_small() {
assert!(
"abcdefghij"
.abbreviate_with_marker_and_offset("::", 0, 2)
.is_err()
);
}
#[test]
fn offset_width_too_small() {
assert!(
"abcdefghij"
.abbreviate_with_marker_and_offset("!!!", 5, 6)
.is_err()
);
}
#[test]
fn raspberry_peach() {
assert_eq!(
"raspberry peach"
.abbreviate_with_marker_and_offset("--", 12, 15)
.unwrap(),
"raspberry peach"
);
}
#[test]
fn double_semicolon_offset_neg1() {
assert_abbreviate_with_marker_and_offset("abcdefgh;;", ";;", 0, 10);
}
#[test]
fn single_dot_offset_0() {
assert_abbreviate_with_marker_and_offset("abcdefghi.", ".", 0, 10);
}
#[test]
fn double_plus_offset_1() {
assert_abbreviate_with_marker_and_offset("abcdefgh++", "++", 1, 10);
}
#[test]
fn star_offset_2() {
assert_abbreviate_with_marker_and_offset("abcdefghi*", "*", 2, 10);
}
#[test]
fn quad_brace_offset_4() {
assert_abbreviate_with_marker_and_offset("abcdef{{{{", "{{{{", 4, 10);
}
#[test]
fn quad_underscore_offset_5() {
assert_abbreviate_with_marker_and_offset("abcdef____", "____", 5, 10);
}
#[test]
fn double_equals_offset_5() {
assert_abbreviate_with_marker_and_offset("==fghijk==", "==", 5, 10);
}
#[test]
fn triple_underscore_offset_6() {
assert_abbreviate_with_marker_and_offset("___ghij___", "___", 6, 10);
}
#[test]
fn slash_offset_7() {
assert_abbreviate_with_marker_and_offset("/ghijklmno", "/", 7, 10);
}
#[test]
fn slash_offset_8() {
assert_abbreviate_with_marker_and_offset("/ghijklmno", "/", 8, 10);
}
#[test]
fn slash_offset_9() {
assert_abbreviate_with_marker_and_offset("/ghijklmno", "/", 9, 10);
}
#[test]
fn triple_slash_offset_10() {
assert_abbreviate_with_marker_and_offset("///ijklmno", "///", 10, 10);
}
#[test]
fn double_slash_offset_10() {
assert_abbreviate_with_marker_and_offset("//hijklmno", "//", 10, 10);
}
#[test]
fn double_slash_offset_11() {
assert_abbreviate_with_marker_and_offset("//hijklmno", "//", 11, 10);
}
#[test]
fn ellipsis_offset_12() {
assert_abbreviate_with_marker_and_offset("...ijklmno", "...", 12, 10);
}
#[test]
fn slash_offset_13() {
assert_abbreviate_with_marker_and_offset("/ghijklmno", "/", 13, 10);
}
#[test]
fn slash_offset_14() {
assert_abbreviate_with_marker_and_offset("/ghijklmno", "/", 14, 10);
}
#[test]
fn triple_nine_offset_15() {
assert_abbreviate_with_marker_and_offset("999ijklmno", "999", 15, 10);
}
#[test]
fn underscore_offset_16() {
assert_abbreviate_with_marker_and_offset("_ghijklmno", "_", 16, 10);
}
#[test]
fn plus_offset_max() {
assert_abbreviate_with_marker_and_offset("+ghijklmno", "+", usize::MAX, 10);
}
#[test]
fn triple_dash_offset_0() {
assert_abbreviate_with_marker_and_offset("abcdefg---", "---", 0, 10);
}
#[test]
fn comma_offset_0() {
assert_abbreviate_with_marker_and_offset("abcdefghi,", ",", 0, 10);
}
#[test]
fn comma_offset_1() {
assert_abbreviate_with_marker_and_offset("abcdefghi,", ",", 1, 10);
}
#[test]
fn comma_offset_2() {
assert_abbreviate_with_marker_and_offset("abcdefghi,", ",", 2, 10);
}
#[test]
fn double_colon_offset_4() {
assert_abbreviate_with_marker_and_offset("::efghij::", "::", 4, 10);
}
#[test]
fn ellipsis_offset_6() {
assert_abbreviate_with_marker_and_offset("...ghij...", "...", 6, 10);
}
#[test]
fn star_offset_9() {
assert_abbreviate_with_marker_and_offset("*ghijklmno", "*", 9, 10);
}
#[test]
fn quote_offset_10() {
assert_abbreviate_with_marker_and_offset("'ghijklmno", "'", 10, 10);
}
#[test]
fn exclamation_offset_12() {
assert_abbreviate_with_marker_and_offset("!ghijklmno", "!", 12, 10);
}
#[test]
fn abra_marker_error() {
assert!(
"abcdefghij"
.abbreviate_with_marker_and_offset("abra", 0, 4)
.is_err()
);
}
}
mod abbreviate_marker_empty_string {
use super::*;
#[test]
fn greater_than_max() {
assert_eq!(
"much too long text".abbreviate_with_marker("", 13).unwrap(),
"much too long"
);
}
}
mod abbreviate_middle {
use super::*;
#[test]
fn empty_middle_marker() {
assert_eq!("abc".abbreviate_middle("", 0), "abc");
}
#[test]
fn length_zero() {
assert_eq!("abc".abbreviate_middle(".", 0), "abc");
}
#[test]
fn length_equals_string_length() {
assert_eq!("abc".abbreviate_middle(".", 3), "abc");
}
#[test]
fn basic() {
assert_eq!("abcdef".abbreviate_middle(".", 4), "ab.f");
}
#[test]
fn long_text() {
assert_eq!(
"A very long text with unimportant stuff in the middle but interesting start and end to see if the text is complete."
.abbreviate_middle("...", 50),
"A very long text with un...f the text is complete."
);
}
#[test]
fn long_text_with_arrow() {
let long_text = format!("Start text{}Close text", "x".repeat(10000));
assert_eq!(
long_text.abbreviate_middle("->", 22),
"Start text->Close text"
);
}
#[test]
fn length_1() {
assert_eq!("abc".abbreviate_middle(".", 1), "abc");
}
#[test]
fn length_2() {
assert_eq!("abc".abbreviate_middle(".", 2), "abc");
}
#[test]
fn single_char() {
assert_eq!("a".abbreviate_middle(".", 1), "a");
}
#[test]
fn abcd_length_3() {
assert_eq!("abcd".abbreviate_middle(".", 3), "a.d");
}
#[test]
fn double_dot_marker() {
assert_eq!("abcdef".abbreviate_middle("..", 4), "a..f");
}
#[test]
fn length_5() {
assert_eq!("abcdef".abbreviate_middle(".", 5), "ab.ef");
}
}
mod emoji {
use super::*;
#[test]
fn fox_emoji_abbreviate() {
for i in 4..=10 {
let result = "🦊🦊🦊🦊🦊🦊🦊🦊🦊🦊🦊🦊🦊🦊".abbreviate(i);
assert!(result.is_ok());
}
}
#[test]
fn family_emoji_abbreviate() {
let family_string = "👩🏻👨🏻👦🏻👦🏻👩🏼👨🏼👦🏼👦🏼👩🏽👨🏽👦🏽👦🏽👩🏾👨🏾👦🏾👦🏾👩🏿👨🏿👦🏿👦🏿👩🏻👨🏻👦🏻👦🏻👩🏼👨🏼👦🏼👦🏼👩🏽👨🏽👦🏽👦🏽👩🏾👨🏾👦🏾👦🏾👩🏿👨🏿👦🏿👦🏿";
for i in 4..=10 {
let result = family_string.abbreviate(i);
assert!(result.is_ok());
}
}
}
mod string_types {
use super::*;
#[test]
fn string_type() {
assert_eq!(
String::from("hello world").abbreviate(8).unwrap(),
"hello..."
);
}
#[test]
fn string_ref() {
let s = String::from("hello world");
assert_eq!(s.abbreviate(8).unwrap(), "hello...");
}
#[test]
fn boxed_str() {
let s: Box<str> = "hello world".into();
assert_eq!(s.abbreviate(8).unwrap(), "hello...");
}
}
mod overlay {
use super::*;
#[test]
fn empty_string_empty_overlay() {
assert_eq!("".overlay("", 0, 0), "");
}
#[test]
fn empty_string_with_overlay() {
assert_eq!("".overlay("zzzz", 0, 0), "zzzz");
}
#[test]
fn empty_string_out_of_range() {
assert_eq!("".overlay("zzzz", 2, 4), "zzzz");
}
#[test]
fn empty_overlay_removes_chars() {
assert_eq!("abcdef".overlay("", 2, 4), "abef");
}
#[test]
fn empty_overlay_swapped_indices() {
assert_eq!("abcdef".overlay("", 4, 2), "abef");
}
#[test]
fn normal_overlay() {
assert_eq!("abcdef".overlay("zzzz", 2, 4), "abzzzzef");
}
#[test]
fn normal_overlay_swapped_indices() {
assert_eq!("abcdef".overlay("zzzz", 4, 2), "abzzzzef");
}
#[test]
fn end_past_length() {
assert_eq!("abcdef".overlay("zzzz", 4, 10), "abcdzzzz");
}
#[test]
fn end_past_length_swapped() {
assert_eq!("abcdef".overlay("zzzz", 10, 4), "abcdzzzz");
}
#[test]
fn both_past_length() {
assert_eq!("abcdef".overlay("zzzz", 8, 10), "abcdefzzzz");
}
#[test]
fn both_past_length_swapped() {
assert_eq!("abcdef".overlay("zzzz", 10, 8), "abcdefzzzz");
}
#[test]
fn start_at_end_of_string() {
assert_eq!("abcdef".overlay("zzzz", 2, 8), "abzzzz");
}
#[test]
fn empty_string_abc_overlay() {
assert_eq!("".overlay("abc", 0, 0), "abc");
}
}
}