Skip to main content

uncurses/unicode/
segment.rs

1//! Grapheme-cluster segmentation, backed by `unicode-rs` or `icu`.
2//!
3//! See the [`unicode`](super) module docs for backend selection. Both
4//! implementations honour Unicode extended grapheme clusters.
5
6/// Iterate over the extended grapheme clusters in a string.
7///
8/// # Parameters
9///
10/// - `s`: UTF-8 string slice to segment.
11///
12/// # Returns
13///
14/// An iterator of `&str` slices, each covering one extended grapheme
15/// cluster from `s` in order.
16///
17/// # Panics
18///
19/// Never panics.
20///
21/// # Usage notes
22///
23/// The returned slices borrow from `s`; no cell-width classification is
24/// performed here.
25#[cfg(all(not(feature = "icu"), feature = "unicode-rs"))]
26pub fn graphemes(s: &str) -> impl Iterator<Item = &str> {
27    use unicode_segmentation::UnicodeSegmentation;
28    s.graphemes(true)
29}
30
31/// Iterate over the extended grapheme clusters in a string.
32///
33/// # Parameters
34///
35/// - `s`: UTF-8 string slice to segment.
36///
37/// # Returns
38///
39/// An iterator of `&str` slices, each covering one extended grapheme
40/// cluster from `s` in order.
41///
42/// # Panics
43///
44/// Never panics.
45///
46/// # Usage notes
47///
48/// The returned slices borrow from `s`; no cell-width classification is
49/// performed here.
50#[cfg(feature = "icu")]
51pub fn graphemes(s: &str) -> impl Iterator<Item = &str> {
52    use icu_segmenter::GraphemeClusterSegmenter;
53
54    // `new()` is effectively zero-cost with compiled-in data: the returned
55    // segmenter is a thin handle.
56    let segmenter = GraphemeClusterSegmenter::new();
57    let mut iter = segmenter.segment_str(s);
58    let mut prev = iter.next().unwrap_or(0);
59    core::iter::from_fn(move || {
60        let next = iter.next()?;
61        let slice = &s[prev..next];
62        prev = next;
63        Some(slice)
64    })
65}
66
67#[cfg(test)]
68mod tests {
69    use super::*;
70
71    #[test]
72    fn ascii() {
73        let v: Vec<_> = graphemes("abc").collect();
74        assert_eq!(v, vec!["a", "b", "c"]);
75    }
76
77    #[test]
78    fn combining() {
79        // "é" as e + combining acute should be a single grapheme.
80        let v: Vec<_> = graphemes("e\u{0301}f").collect();
81        assert_eq!(v, vec!["e\u{0301}", "f"]);
82    }
83
84    #[test]
85    fn zwj_emoji() {
86        // Family ZWJ sequence — one grapheme.
87        let s = "\u{1F468}\u{200D}\u{1F469}\u{200D}\u{1F466}X";
88        let v: Vec<_> = graphemes(s).collect();
89        assert_eq!(v.len(), 2);
90        assert_eq!(v[1], "X");
91    }
92
93    #[test]
94    fn empty() {
95        assert_eq!(graphemes("").count(), 0);
96    }
97}