text-fx 0.4.0

A collection of text processing utilities for Rust.
Documentation
/// Returns the display width of a string, accounting for Unicode double-width and combining characters.
///
/// This function sums the display width of each character in the string, using [`char_display_width`].
///
/// # Examples
///
/// ```
/// use text_fx::presentation::display_width;
///
/// assert_eq!(display_width("hello"), 5); // ASCII
/// assert_eq!(display_width("你好"), 4);   // CJK
/// assert_eq!(display_width("á"), 1);    // 'a' + combining accent
/// ```
#[inline]
pub fn display_width(s: &str) -> usize {
    s.chars().map(char_display_width).sum()
}

/// Returns the display width of a single Unicode character.
///
/// - Control and combining characters have width 0.
/// - CJK and wide symbols have width 2.
/// - Most other characters have width 1.
///
/// # Examples
///
/// ```
/// use text_fx::presentation::char_display_width;
///
/// assert_eq!(char_display_width('a'), 1);
/// assert_eq!(char_display_width('你'), 2);
/// assert_eq!(char_display_width('\u{0301}'), 0); // combining acute accent
/// ```
pub fn char_display_width(c: char) -> usize {
    use std::cmp::Ordering::*;

    match c {
        '\u{0000}' => 0,                           // NULL
        '\u{0001}'..='\u{001F}' | '\u{007F}' => 0, // Control characters

        '\u{0300}'..='\u{036F}'
        | '\u{1AB0}'..='\u{1AFF}'
        | '\u{1DC0}'..='\u{1DFF}'
        | '\u{20D0}'..='\u{20FF}'
        | '\u{FE20}'..='\u{FE2F}' => 0, // Combining characters

        _ => match unicode_width_hint(c) {
            Less => 1,
            Equal | Greater => 2,
        },
    }
}

/// Heuristically determines if a character is double-width (CJK, emoji, etc).
///
/// Returns `Greater` for double-width, `Less` for single-width.
fn unicode_width_hint(c: char) -> std::cmp::Ordering {
    match c as u32 {
        0x1100..=0x115F
        | 0x2329
        | 0x232A
        | 0x2E80..=0xA4CF
        | 0xAC00..=0xD7A3
        | 0xF900..=0xFAFF
        | 0xFE10..=0xFE19
        | 0xFE30..=0xFE6F
        | 0xFF00..=0xFF60
        | 0xFFE0..=0xFFE6
        | 0x1F300..=0x1F64F
        | 0x1F900..=0x1F9FF => std::cmp::Ordering::Greater,
        _ => std::cmp::Ordering::Less,
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_display_width() {
        for (s, expected) in [
            ("hello", 5), // ASCII → width 5
            ("你好", 4),  // CJK → width 4
            ("", 1),     // 'a' + combining accent → width 1
        ] {
            assert_eq!(display_width(s), expected);
        }
    }
}