//! UTF-16 code-unit measurement.
//!
//! The Google Docs API expresses every `Location.index` and range bound in
//! UTF-16 code units (the JavaScript string convention), not bytes or Unicode
//! scalar values. This module is the single authority for that arithmetic
//! (domain invariant DI-7); other modules MUST route length calculations through
//! it rather than using `str::len()` or `chars().count()`.

/// Count the UTF-16 code units in `text`.
///
/// Astral-plane scalar values (e.g. most emoji) count as two code units because
/// they encode as a surrogate pair; values in the Basic Multilingual Plane count
/// as one.
#[must_use]
pub fn len_utf16(text: &str) -> usize {
    text.chars().map(char::len_utf16).sum()
}

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

    #[test]
    fn ascii_is_one_unit_each() {
        assert_eq!(len_utf16("hello"), 5);
        assert_eq!(len_utf16(""), 0);
    }

    #[test]
    fn bmp_scalar_is_one_unit() {
        // U+00E9 LATIN SMALL LETTER E WITH ACUTE: 2 UTF-8 bytes, 1 UTF-16 unit.
        assert_eq!("é".len(), 2);
        assert_eq!(len_utf16("é"), 1);
    }

    #[test]
    fn astral_scalar_is_two_units() {
        // U+1F600 GRINNING FACE: 4 UTF-8 bytes, surrogate pair = 2 UTF-16 units.
        assert_eq!("😀".len(), 4);
        assert_eq!(len_utf16("😀"), 2);
    }

    #[test]
    fn mixed_string_sums_per_scalar() {
        // 'a' (1) + 'é' (1) + '😀' (2) + 'b' (1) = 5.
        assert_eq!(len_utf16("aé😀b"), 5);
    }
}
