unicode_columns/
lib.rs

1use unicode_width::UnicodeWidthChar;
2
3const ZWJ: u32 = 0x200d;
4
5/// Get the column width of a string
6pub fn width(string: &str) -> usize {
7    let mut cw = 0;
8    let mut iter = string.chars();
9    while let Some(c) = iter.next() {
10        if u32::from(c) == ZWJ {
11            iter.next();
12            continue;
13        }
14        cw += c.width().unwrap_or(0);
15    }
16    cw
17}
18
19/// Truncate a string to a specific column width
20pub fn truncate(string: &str, width: usize) -> &str {
21    let mut cw = 0;
22    let mut iter = string.char_indices();
23    while let Some((i, c)) = iter.next() {
24        if u32::from(c) == ZWJ {
25            iter.next();
26            continue;
27        }
28        cw += c.width().unwrap_or(0);
29        if cw > width {
30            return &string[..i];
31        }
32    }
33    string
34}
35
36#[cfg(test)]
37mod tests {
38    use super::*;
39
40    #[test]
41    fn test_width() {
42        // basic tests
43        assert_eq!(width("teststring"), 10);
44        // full-width (2 column) characters test
45        assert_eq!(width("잘라야"), 6);
46        // combining characters (zalgo text) test
47        assert_eq!(width("ę̵̡̛̮̹̼̝̲͓̳̣͉̞͔̳̥̝͍̩̣̹͙̘̼̥̗̼͈̯͎̮̥̤̪̻̮͕̩̮͓͔̟͈͇͎̣͉͇̦͔̝̣͎͎͔͇̭͈̌̂̈̄̈́̾͑̀̈̓̂͗̾̉͊͒̆̽͊̽͘̕͜͜͝͠ :width"), 8);
48        // zero-width-joiner (emoji) test
49        assert_eq!(width("👨‍👩‍👦:width"), 8);
50    }
51
52    #[test]
53    fn test_truncation() {
54        // basic tests
55        assert_eq!(truncate("teststring", 50), "teststring");
56        assert_eq!(truncate("teststring", 5), "tests");
57        assert_eq!(truncate("teststring", 0), "");
58        // full-width (2 column) characters test
59        assert_eq!(truncate("잘라야", 4), "잘라");
60        // combining characters (zalgo text) test
61        assert_eq!(truncate("ę̵̡̛̮̹̼̝̲͓̳̣͉̞͔̳̥̝͍̩̣̹͙̘̼̥̗̼͈̯͎̮̥̤̪̻̮͕̩̮͓͔̟͈͇͎̣͉͇̦͔̝̣͎͎͔͇̭͈̌̂̈̄̈́̾͑̀̈̓̂͗̾̉͊͒̆̽͊̽͘̕͜͜͝͠ :trunc", 3), "ę̵̡̛̮̹̼̝̲͓̳̣͉̞͔̳̥̝͍̩̣̹͙̘̼̥̗̼͈̯͎̮̥̤̪̻̮͕̩̮͓͔̟͈͇͎̣͉͇̦͔̝̣͎͎͔͇̭͈̌̂̈̄̈́̾͑̀̈̓̂͗̾̉͊͒̆̽͊̽͘̕͜͜͝͠ :");
62        // zero-width-joiner (emoji) test
63        assert_eq!(truncate("👨‍👩‍👦:trunc", 3), "👨‍👩‍👦:");
64    }
65}