org_gdocs/google/utf16.rs
1//! UTF-16 code-unit measurement.
2//!
3//! The Google Docs API expresses every `Location.index` and range bound in
4//! UTF-16 code units (the JavaScript string convention), not bytes or Unicode
5//! scalar values. This module is the single authority for that arithmetic
6//! (domain invariant DI-7); other modules MUST route length calculations through
7//! it rather than using `str::len()` or `chars().count()`.
8
9/// Count the UTF-16 code units in `text`.
10///
11/// Astral-plane scalar values (e.g. most emoji) count as two code units because
12/// they encode as a surrogate pair; values in the Basic Multilingual Plane count
13/// as one.
14#[must_use]
15pub fn len_utf16(text: &str) -> usize {
16 text.chars().map(char::len_utf16).sum()
17}
18
19#[cfg(test)]
20mod tests {
21 use super::len_utf16;
22
23 #[test]
24 fn ascii_is_one_unit_each() {
25 assert_eq!(len_utf16("hello"), 5);
26 assert_eq!(len_utf16(""), 0);
27 }
28
29 #[test]
30 fn bmp_scalar_is_one_unit() {
31 // U+00E9 LATIN SMALL LETTER E WITH ACUTE: 2 UTF-8 bytes, 1 UTF-16 unit.
32 assert_eq!("Ć©".len(), 2);
33 assert_eq!(len_utf16("Ć©"), 1);
34 }
35
36 #[test]
37 fn astral_scalar_is_two_units() {
38 // U+1F600 GRINNING FACE: 4 UTF-8 bytes, surrogate pair = 2 UTF-16 units.
39 assert_eq!("š".len(), 4);
40 assert_eq!(len_utf16("š"), 2);
41 }
42
43 #[test]
44 fn mixed_string_sums_per_scalar() {
45 // 'a' (1) + 'Ć©' (1) + 'š' (2) + 'b' (1) = 5.
46 assert_eq!(len_utf16("aĆ©šb"), 5);
47 }
48}