tftio-org-gdocs 0.1.3

Sync org-mode documents to Google Docs and pull reviewer comments back into org-mode
Documentation
//! 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);
    }
}