1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
//! # truncatable
//!
//! Truncatable is a simple crate for Strings that should append
//! a follower when truncated (eg, ellipsis)
//!
//! ```
//! use truncatable::Truncatable;
//! let to_truncate = Truncatable::from("Hello World!").truncator("~~".into());
//! assert_eq!(to_truncate.truncate(5), String::from("Hello~~"));
//!```

/// Main struct, serves as a String wrapper
///
/// At its core, this struct is a wrapper for a string
/// but contains other crucial values such as the direction
/// of the text (right-to-left vs left-to-right). As well
/// as the token that should be appended to the truncated
/// text.
#[derive(Debug, Clone)]
pub struct Truncatable {
    value: String,
    truncator: String,
    rtl: bool,
}

impl From<&str> for Truncatable {
    fn from(value: &str) -> Self {
        Truncatable::new(String::from(value))
    }
}

impl From<String> for Truncatable {
    fn from(value: String) -> Self {
        Truncatable::new(value)
    }
}

impl Truncatable {
    /// Create a new truncator from a String
    pub fn new(value: String) -> Self {
        Self {
            value,
            rtl: false,
            truncator: String::from("..."),
        }
    }
    /// Change the truncator token (default = "...")
    pub fn truncator(mut self, truncator: String) -> Self {
        self.truncator = truncator;
        self
    }

    /// Set the truncation direction to be Left-To-Right (default)
    pub fn ltr(mut self) -> Self {
        self.rtl = false;
        self
    }

    /// Set the truncation direction to be Right-To-Left
    pub fn rtl(mut self) -> Self {
        self.rtl = true;
        self
    }

    /// Return a truncated string, syntactic sugar for ```format!("{value:.length$}", value=Truncator::from("test"), length=2)```
    pub fn truncate(&self, length: usize) -> String {
        return format!("{1:.*}", length, self);
    }
}
impl std::fmt::Display for Truncatable {
    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
        if let Some(precision) = f.precision() {
            if precision < self.value.len() {
                if !self.rtl {
                    f.pad(self.value.as_str())?;
                    write!(f, "{truncator}", truncator = self.truncator)?;
                    return Ok(());
                } else {
                    write!(f, "{truncator}", truncator = self.truncator)?;
                    f.pad(self.value.as_str())?;
                    return Ok(());
                }
            }
        }
        return f.pad(self.value.as_str());
    }
}

#[cfg(test)]
mod tests {
    use crate::Truncatable;

    #[test]
    fn build_from_str() {
        let trunc = Truncatable::from("Test_Truncatable");
        assert_eq!(format!("{}", trunc), String::from("Test_Truncatable"));
    }

    #[test]
    fn default_truncator() {
        let trunc = Truncatable::from("Test_Truncatable");
        assert_eq!(trunc.truncate(4), String::from("Test..."));
    }

    #[test]
    fn custom_truncator() {
        let trunc = Truncatable::from("Test_Truncatable").truncator("-".into());
        assert_eq!(trunc.truncate(4), String::from("Test-"));
    }

    #[test]
    fn right_to_left() {
        let trunc = Truncatable::from("Test_Truncatable").rtl();
        assert_eq!(trunc.truncate(4), "...Test");
        let trunc = trunc.ltr();
        assert_eq!(trunc.truncate(4), "Test...");
    }

    #[test]
    fn too_long_to_truncate() {
        let trunc = Truncatable::from("Test_Truncatable");
        assert_eq!(trunc.truncate(20), trunc.value);
        assert_eq!(format!("{:.20}", trunc), "Test_Truncatable");
    }

    #[test]
    fn works_with_other_formatting_flags() {
        let trunc = Truncatable::from("Test_Truncatable");
        let control = String::from("Test_Truncatable");

        let f_trunc = format!("{:]<50}", trunc);
        let f_control = format!("{:]<50}", control);
        assert_eq!(f_trunc, f_control);
    }
}