char_ranges/
lib.rs

1//! Similar to the standard library's [`.char_indicies()`], but instead of only
2//! producing the start byte position. This library implements [`.char_ranges()`],
3//! that produce both the start and end byte positions.
4//!
5//! Note that simply using [`.char_indicies()`] and creating a range by mapping the
6//! returned index `i` to `i..(i + 1)` is not guaranteed to be valid. Given that
7//! some UTF-8 characters can be up to 4 bytes.
8//!
9//! | Char | Bytes | Range |
10//! |:----:|:-----:|:-----:|
11//! | `'O'` | 1 | `0..1` |
12//! | `'Ø'` | 2 | `0..2` |
13//! | `'∈'` | 3 | `0..3` |
14//! | `'🌏'` | 4 | `0..4` |
15//!
16//! _Assumes encoded in UTF-8._
17//!
18//! The implementation specializes [`last()`], [`nth()`], [`next_back()`],
19//! and [`nth_back()`]. Such that the length of intermediate characters is
20//! not wastefully calculated.
21//!
22//! # Example
23//!
24//! ```rust
25//! use char_ranges::CharRangesExt;
26//!
27//! let text = "Hello 🗻∈🌏";
28//!
29//! let mut chars = text.char_ranges();
30//! assert_eq!(chars.as_str(), "Hello 🗻∈🌏");
31//!
32//! assert_eq!(chars.next(), Some((0..1, 'H'))); // These chars are 1 byte
33//! assert_eq!(chars.next(), Some((1..2, 'e')));
34//! assert_eq!(chars.next(), Some((2..3, 'l')));
35//! assert_eq!(chars.next(), Some((3..4, 'l')));
36//! assert_eq!(chars.next(), Some((4..5, 'o')));
37//! assert_eq!(chars.next(), Some((5..6, ' ')));
38//!
39//! // Get the remaining substring
40//! assert_eq!(chars.as_str(), "🗻∈🌏");
41//!
42//! assert_eq!(chars.next(), Some((6..10, '🗻'))); // This char is 4 bytes
43//! assert_eq!(chars.next(), Some((10..13, '∈'))); // This char is 3 bytes
44//! assert_eq!(chars.next(), Some((13..17, '🌏'))); // This char is 4 bytes
45//! assert_eq!(chars.next(), None);
46//! ```
47//!
48//! # `DoubleEndedIterator`
49//!
50//! [`CharRanges`] also implements [`DoubleEndedIterator`] making it possible to iterate backwards.
51//!
52//! ```rust
53//! use char_ranges::CharRangesExt;
54//!
55//! let text = "ABCDE";
56//!
57//! let mut chars = text.char_ranges();
58//! assert_eq!(chars.as_str(), "ABCDE");
59//!
60//! assert_eq!(chars.next(), Some((0..1, 'A')));
61//! assert_eq!(chars.next_back(), Some((4..5, 'E')));
62//! assert_eq!(chars.as_str(), "BCD");
63//!
64//! assert_eq!(chars.next_back(), Some((3..4, 'D')));
65//! assert_eq!(chars.next(), Some((1..2, 'B')));
66//! assert_eq!(chars.as_str(), "C");
67//!
68//! assert_eq!(chars.next(), Some((2..3, 'C')));
69//! assert_eq!(chars.as_str(), "");
70//!
71//! assert_eq!(chars.next(), None);
72//! ```
73//!
74//! # Offset Ranges
75//!
76//! If the input `text` is a substring of some original text, and the produced
77//! ranges are desired to be offset in relation to the substring. Then instead
78//! of [`.char_ranges()`] use <code>[.char_ranges_offset]\(offset)</code>
79//! or <code>.[char_ranges]\().[offset]\(offset)</code>.
80//!
81//! ```rust
82//! use char_ranges::CharRangesExt;
83//!
84//! let text = "Hello 👋 World 🌏";
85//!
86//! let start = 11; // Start index of 'W'
87//! let text = &text[start..]; // "World 🌏"
88//!
89//! let mut chars = text.char_ranges_offset(start);
90//! // or
91//! // let mut chars = text.char_ranges().offset(start);
92//!
93//! assert_eq!(chars.next(), Some((11..12, 'W'))); // These chars are 1 byte
94//! assert_eq!(chars.next(), Some((12..13, 'o')));
95//! assert_eq!(chars.next(), Some((13..14, 'r')));
96//!
97//! assert_eq!(chars.next_back(), Some((17..21, '🌏'))); // This char is 4 bytes
98//! ```
99//!
100//! [`.char_ranges()`]: CharRangesExt::char_ranges
101//! [char_ranges]: CharRangesExt::char_ranges
102//! [.char_ranges_offset]: CharRangesExt::char_ranges_offset
103//! [offset]: CharRanges::offset
104//! [`CharRanges`]: CharRanges
105//!
106//! [`.char_indicies()`]: https://doc.rust-lang.org/std/primitive.str.html#method.char_indices
107//! [`DoubleEndedIterator`]: https://doc.rust-lang.org/std/iter/trait.DoubleEndedIterator.html
108//!
109//! [`last()`]: https://doc.rust-lang.org/std/iter/trait.Iterator.html#method.last
110//! [`nth()`]: https://doc.rust-lang.org/std/iter/trait.Iterator.html#method.nth
111//! [`next_back()`]: https://doc.rust-lang.org/std/iter/trait.DoubleEndedIterator.html#tymethod.next_back
112//! [`nth_back()`]: https://doc.rust-lang.org/std/iter/trait.DoubleEndedIterator.html#method.nth_back
113
114#![no_std]
115#![forbid(unsafe_code)]
116#![forbid(elided_lifetimes_in_paths)]
117
118use core::fmt;
119use core::iter::FusedIterator;
120use core::ops::Range;
121use core::str::CharIndices;
122
123pub trait CharRangesExt {
124    /// Returns an iterator over [`char`]s and their start and end byte positions.
125    ///
126    /// See examples in the [crate root](crate).
127    fn char_ranges(&self) -> CharRanges<'_>;
128
129    /// Returns an iterator over [`char`]s and their start and end byte positions,
130    /// with an offset applied to all positions.
131    ///
132    /// See examples in the [crate root](crate).
133    #[inline]
134    fn char_ranges_offset(&self, offset: usize) -> CharRangesOffset<'_> {
135        self.char_ranges().offset(offset)
136    }
137}
138
139impl CharRangesExt for str {
140    #[inline]
141    fn char_ranges(&self) -> CharRanges<'_> {
142        CharRanges::new(self)
143    }
144}
145
146/// An iterator over [`char`]s and their start and end byte positions.
147///
148/// Note: Cloning this iterator is essentially a copy.
149///
150/// See examples in the [crate root](crate).
151#[derive(Clone)]
152pub struct CharRanges<'a> {
153    iter: CharIndices<'a>,
154}
155
156impl<'a> CharRanges<'a> {
157    /// Creates an iterator over [`char`]s and their start and end byte positions.
158    ///
159    /// Consider using <code>text.[char_ranges()]</code>, instead of
160    /// explicitly using `CharRanges::new()`.
161    ///
162    /// See examples in the [crate root](crate).
163    ///
164    /// [char_ranges()]: CharRangesExt::char_ranges
165    #[inline]
166    pub fn new(text: &'a str) -> Self {
167        Self {
168            iter: text.char_indices(),
169        }
170    }
171
172    /// Returns the remaining substring.
173    ///
174    /// # Example
175    ///
176    /// ```rust
177    /// use char_ranges::CharRangesExt;
178    ///
179    /// let text = "ABCDE";
180    ///
181    /// let mut chars = text.char_ranges();
182    /// assert_eq!(chars.as_str(), "ABCDE");
183    ///
184    /// assert_eq!(chars.next(), Some((0..1, 'A')));
185    /// assert_eq!(chars.next_back(), Some((4..5, 'E')));
186    ///
187    /// assert_eq!(chars.as_str(), "BCD");
188    /// ```
189    #[inline]
190    pub fn as_str(&self) -> &'a str {
191        self.iter.as_str()
192    }
193
194    /// Returns an iterator over the remaining [`char`]s and their start and
195    /// end byte positions, with an offset applied to all positions.
196    ///
197    /// See examples in the [crate root](crate).
198    #[inline]
199    pub fn offset(self, offset: usize) -> CharRangesOffset<'a> {
200        CharRangesOffset { iter: self, offset }
201    }
202}
203
204impl Iterator for CharRanges<'_> {
205    type Item = (Range<usize>, char);
206
207    #[inline]
208    fn next(&mut self) -> Option<Self::Item> {
209        let (start, c) = self.iter.next()?;
210        let end = start + c.len_utf8();
211        Some((start..end, c))
212    }
213
214    #[inline]
215    fn count(self) -> usize {
216        self.iter.count()
217    }
218
219    #[inline]
220    fn size_hint(&self) -> (usize, Option<usize>) {
221        self.iter.size_hint()
222    }
223
224    #[inline]
225    fn last(mut self) -> Option<(Range<usize>, char)> {
226        self.next_back()
227    }
228
229    #[inline]
230    fn nth(&mut self, n: usize) -> Option<Self::Item> {
231        let (start, c) = self.iter.nth(n)?;
232        let end = start + c.len_utf8();
233        Some((start..end, c))
234    }
235}
236
237impl DoubleEndedIterator for CharRanges<'_> {
238    #[inline]
239    fn next_back(&mut self) -> Option<Self::Item> {
240        let (start, c) = self.iter.next_back()?;
241        let end = start + c.len_utf8();
242        Some((start..end, c))
243    }
244
245    #[inline]
246    fn nth_back(&mut self, n: usize) -> Option<Self::Item> {
247        let (start, c) = self.iter.nth_back(n)?;
248        let end = start + c.len_utf8();
249        Some((start..end, c))
250    }
251}
252
253impl FusedIterator for CharRanges<'_> {}
254
255impl fmt::Debug for CharRanges<'_> {
256    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
257        write!(f, "CharRanges(")?;
258        f.debug_list().entries(self.clone()).finish()?;
259        write!(f, ")")?;
260        Ok(())
261    }
262}
263
264/// An iterator over [`char`]s and their start and end byte positions,
265/// with an offset applied to all positions.
266///
267/// Note: Cloning this iterator is essentially a copy.
268///
269/// See examples in the [crate root](crate).
270#[derive(Clone)]
271pub struct CharRangesOffset<'a> {
272    iter: CharRanges<'a>,
273    offset: usize,
274}
275
276impl<'a> CharRangesOffset<'a> {
277    /// Creates an iterator over [`char`]s and their start and end byte positions,
278    /// with an offset applied to all positions.
279    ///
280    /// Consider using <code>text.[char_ranges_offset()]</code> or
281    /// <code>text.[char_ranges()].[offset()]</code>, instead of
282    /// explicitly using `CharRangesOffset::new()`.
283    ///
284    /// See examples in the [crate root](crate).
285    ///
286    /// [char_ranges()]: CharRangesExt::char_ranges
287    /// [char_ranges_offset()]: CharRangesExt::char_ranges_offset
288    /// [offset()]: CharRanges::offset
289    #[inline]
290    pub fn new(offset: usize, text: &'a str) -> Self {
291        Self {
292            iter: text.char_ranges(),
293            offset,
294        }
295    }
296
297    /// Returns the remaining substring.
298    ///
299    /// # Example
300    ///
301    /// ```rust
302    /// use char_ranges::CharRangesExt;
303    ///
304    /// let text = "Hello 👋 World 🌏";
305    ///
306    /// let start = 11; // Start index of 'W'
307    /// let text = &text[start..]; // "World 🌏"
308    ///
309    /// let mut chars = text.char_ranges_offset(start);
310    /// assert_eq!(chars.as_str(), "World 🌏");
311    ///
312    /// assert_eq!(chars.next(), Some((11..12, 'W'))); // These chars are 1 byte
313    /// assert_eq!(chars.next_back(), Some((17..21, '🌏'))); // This char is 4 bytes
314    ///
315    /// assert_eq!(chars.as_str(), "orld ");
316    /// ```
317    #[inline]
318    pub fn as_str(&self) -> &'a str {
319        self.iter.as_str()
320    }
321
322    /// Returns the `offset` this [`CharRangesOffset`] was created with.
323    ///
324    /// # Example
325    ///
326    /// ```rust
327    /// use char_ranges::CharRangesExt;
328    ///
329    /// let text = "Hello 👋 World 🌏";
330    ///
331    /// let start = 11; // Start index of 'W'
332    /// let text = &text[start..]; // "World 🌏"
333    ///
334    /// let mut chars = text.char_ranges_offset(start);
335    /// // Offset is `start`
336    /// assert_eq!(chars.offset(), start);
337    ///
338    /// assert_eq!(chars.next(), Some((11..12, 'W'))); // These chars are 1 byte
339    /// assert_eq!(chars.next_back(), Some((17..21, '🌏'))); // This char is 4 bytes
340    ///
341    /// // Offset remains as `start` always
342    /// assert_eq!(chars.offset(), start);
343    /// ```
344    #[inline]
345    pub fn offset(&self) -> usize {
346        self.offset
347    }
348}
349
350impl Iterator for CharRangesOffset<'_> {
351    type Item = (Range<usize>, char);
352
353    #[inline]
354    fn next(&mut self) -> Option<Self::Item> {
355        let (r, c) = self.iter.next()?;
356        let start = r.start + self.offset;
357        let end = r.end + self.offset;
358        Some((start..end, c))
359    }
360
361    #[inline]
362    fn count(self) -> usize {
363        self.iter.count()
364    }
365
366    #[inline]
367    fn size_hint(&self) -> (usize, Option<usize>) {
368        self.iter.size_hint()
369    }
370
371    #[inline]
372    fn last(mut self) -> Option<(Range<usize>, char)> {
373        self.next_back()
374    }
375
376    #[inline]
377    fn nth(&mut self, n: usize) -> Option<Self::Item> {
378        let (r, c) = self.iter.nth(n)?;
379        let start = r.start + self.offset;
380        let end = r.end + self.offset;
381        Some((start..end, c))
382    }
383}
384
385impl DoubleEndedIterator for CharRangesOffset<'_> {
386    #[inline]
387    fn next_back(&mut self) -> Option<Self::Item> {
388        let (r, c) = self.iter.next_back()?;
389        let start = r.start + self.offset;
390        let end = r.end + self.offset;
391        Some((start..end, c))
392    }
393
394    #[inline]
395    fn nth_back(&mut self, n: usize) -> Option<Self::Item> {
396        let (r, c) = self.iter.nth_back(n)?;
397        let start = r.start + self.offset;
398        let end = r.end + self.offset;
399        Some((start..end, c))
400    }
401}
402
403impl FusedIterator for CharRangesOffset<'_> {}
404
405impl fmt::Debug for CharRangesOffset<'_> {
406    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
407        write!(f, "CharRangesOffset(")?;
408        f.debug_list().entries(self.clone()).finish()?;
409        write!(f, ")")?;
410        Ok(())
411    }
412}
413
414#[cfg(test)]
415mod tests {
416    use core::iter;
417
418    use super::CharRangesExt;
419
420    #[test]
421    fn test_empty() {
422        let text = "";
423
424        let mut chars = text.char_ranges();
425        assert_eq!(chars.next(), None);
426        assert_eq!(chars.next_back(), None);
427        assert_eq!(chars.as_str(), "");
428
429        let mut chars = text.char_ranges();
430        assert_eq!(chars.next_back(), None);
431        assert_eq!(chars.next(), None);
432        assert_eq!(chars.as_str(), "");
433    }
434
435    #[test]
436    fn test_empty_single_char() {
437        let text = "a";
438
439        let mut chars = text.char_ranges();
440        assert_eq!(chars.as_str(), "a");
441        assert_eq!(chars.next(), Some((0..1, 'a')));
442        assert_eq!(chars.next_back(), None);
443        assert_eq!(chars.as_str(), "");
444
445        let mut chars = text.char_ranges();
446        assert_eq!(chars.as_str(), "a");
447        assert_eq!(chars.next_back(), Some((0..1, 'a')));
448        assert_eq!(chars.next(), None);
449        assert_eq!(chars.as_str(), "");
450    }
451
452    #[test]
453    fn test_empty_single_char_multi_byte() {
454        let text = "🌏";
455
456        let mut chars = text.char_ranges();
457        assert_eq!(chars.as_str(), "🌏");
458        assert_eq!(chars.next(), Some((0..4, '🌏')));
459        assert_eq!(chars.next_back(), None);
460        assert_eq!(chars.as_str(), "");
461
462        let mut chars = text.char_ranges();
463        assert_eq!(chars.as_str(), "🌏");
464        assert_eq!(chars.next_back(), Some((0..4, '🌏')));
465        assert_eq!(chars.next(), None);
466        assert_eq!(chars.as_str(), "");
467    }
468
469    #[test]
470    fn test_simple() {
471        let text = "Foo";
472        let mut chars = text.char_ranges();
473        assert_eq!(chars.as_str(), "Foo");
474
475        assert_eq!(chars.next(), Some((0..1, 'F')));
476        assert_eq!(chars.as_str(), "oo");
477
478        assert_eq!(chars.next(), Some((1..2, 'o')));
479        assert_eq!(chars.as_str(), "o");
480
481        assert_eq!(chars.next(), Some((2..3, 'o')));
482        assert_eq!(chars.as_str(), "");
483
484        assert_eq!(chars.next(), None);
485        assert_eq!(chars.as_str(), "");
486    }
487
488    #[test]
489    fn test_simple_multi_byte() {
490        let text = "🗻∈🌏";
491
492        let mut chars = text.char_ranges();
493        assert_eq!(chars.as_str(), "🗻∈🌏");
494
495        assert_eq!(chars.next(), Some((0..4, '🗻')));
496        assert_eq!(chars.next(), Some((4..7, '∈')));
497        assert_eq!(chars.next(), Some((7..11, '🌏')));
498        assert_eq!(chars.next(), None);
499    }
500
501    #[test]
502    fn test_simple_mixed_multi_byte() {
503        let text = "🗻12∈45🌏";
504        let mut chars = text.char_ranges();
505        assert_eq!(chars.as_str(), "🗻12∈45🌏");
506
507        assert_eq!(chars.next(), Some((0..4, '🗻')));
508        assert_eq!(chars.as_str(), "12∈45🌏");
509
510        assert_eq!(chars.next(), Some((4..5, '1')));
511        assert_eq!(chars.as_str(), "2∈45🌏");
512
513        assert_eq!(chars.next(), Some((5..6, '2')));
514        assert_eq!(chars.as_str(), "∈45🌏");
515
516        assert_eq!(chars.next(), Some((6..9, '∈')));
517        assert_eq!(chars.as_str(), "45🌏");
518
519        assert_eq!(chars.next(), Some((9..10, '4')));
520        assert_eq!(chars.as_str(), "5🌏");
521
522        assert_eq!(chars.next(), Some((10..11, '5')));
523        assert_eq!(chars.as_str(), "🌏");
524
525        assert_eq!(chars.next(), Some((11..15, '🌏')));
526        assert_eq!(chars.as_str(), "");
527
528        assert_eq!(chars.next(), None);
529        assert_eq!(chars.as_str(), "");
530    }
531
532    #[test]
533    fn test_simple_next_back() {
534        let text = "Foo";
535        let mut chars = text.char_ranges();
536        assert_eq!(chars.as_str(), "Foo");
537
538        assert_eq!(chars.next_back(), Some((2..3, 'o')));
539        assert_eq!(chars.as_str(), "Fo");
540
541        assert_eq!(chars.next_back(), Some((1..2, 'o')));
542        assert_eq!(chars.as_str(), "F");
543
544        assert_eq!(chars.next_back(), Some((0..1, 'F')));
545        assert_eq!(chars.as_str(), "");
546
547        assert_eq!(chars.next_back(), None);
548        assert_eq!(chars.as_str(), "");
549    }
550
551    #[test]
552    fn test_simple_next_back_multi_byte() {
553        let text = "🗻12∈45🌏";
554        let mut chars = text.char_ranges();
555        assert_eq!(chars.as_str(), "🗻12∈45🌏");
556
557        assert_eq!(chars.next_back(), Some((11..15, '🌏')));
558        assert_eq!(chars.as_str(), "🗻12∈45");
559
560        assert_eq!(chars.next_back(), Some((10..11, '5')));
561        assert_eq!(chars.as_str(), "🗻12∈4");
562
563        assert_eq!(chars.next_back(), Some((9..10, '4')));
564        assert_eq!(chars.as_str(), "🗻12∈");
565
566        assert_eq!(chars.next_back(), Some((6..9, '∈')));
567        assert_eq!(chars.as_str(), "🗻12");
568
569        assert_eq!(chars.next_back(), Some((5..6, '2')));
570        assert_eq!(chars.as_str(), "🗻1");
571
572        assert_eq!(chars.next_back(), Some((4..5, '1')));
573        assert_eq!(chars.as_str(), "🗻");
574
575        assert_eq!(chars.next_back(), Some((0..4, '🗻')));
576        assert_eq!(chars.as_str(), "");
577
578        assert_eq!(chars.next_back(), None);
579        assert_eq!(chars.as_str(), "");
580    }
581
582    #[test]
583    fn test_simple_next_and_next_back() {
584        let text = "Foo Bar";
585
586        let mut chars = text.char_ranges();
587        assert_eq!(chars.as_str(), "Foo Bar");
588
589        assert_eq!(chars.next_back(), Some((6..7, 'r')));
590        assert_eq!(chars.as_str(), "Foo Ba");
591
592        assert_eq!(chars.next_back(), Some((5..6, 'a')));
593        assert_eq!(chars.as_str(), "Foo B");
594
595        assert_eq!(chars.next(), Some((0..1, 'F')));
596        assert_eq!(chars.as_str(), "oo B");
597
598        assert_eq!(chars.next(), Some((1..2, 'o')));
599        assert_eq!(chars.as_str(), "o B");
600
601        assert_eq!(chars.next_back(), Some((4..5, 'B')));
602        assert_eq!(chars.as_str(), "o ");
603
604        assert_eq!(chars.next(), Some((2..3, 'o')));
605        assert_eq!(chars.as_str(), " ");
606
607        assert_eq!(chars.next(), Some((3..4, ' ')));
608        assert_eq!(chars.as_str(), "");
609
610        assert_eq!(chars.next(), None);
611        assert_eq!(chars.as_str(), "");
612    }
613
614    #[test]
615    fn test_simple_next_and_next_back_multi_byte() {
616        let text = "🗻12∈45🌏";
617        let mut chars = text.char_ranges();
618        assert_eq!(chars.as_str(), "🗻12∈45🌏");
619
620        assert_eq!(chars.next_back(), Some((11..15, '🌏')));
621        assert_eq!(chars.as_str(), "🗻12∈45");
622
623        assert_eq!(chars.next_back(), Some((10..11, '5')));
624        assert_eq!(chars.as_str(), "🗻12∈4");
625
626        assert_eq!(chars.next(), Some((0..4, '🗻')));
627        assert_eq!(chars.as_str(), "12∈4");
628
629        assert_eq!(chars.next(), Some((4..5, '1')));
630        assert_eq!(chars.as_str(), "2∈4");
631
632        assert_eq!(chars.next_back(), Some((9..10, '4')));
633        assert_eq!(chars.as_str(), "2∈");
634
635        assert_eq!(chars.next(), Some((5..6, '2')));
636        assert_eq!(chars.as_str(), "∈");
637
638        assert_eq!(chars.next(), Some((6..9, '∈')));
639        assert_eq!(chars.as_str(), "");
640
641        assert_eq!(chars.next(), None);
642        assert_eq!(chars.as_str(), "");
643    }
644
645    #[test]
646    fn test_last() {
647        let cases = [
648            ("Hello World", 10..11, 'd'),
649            ("Hello 👋 World 🌏", 17..21, '🌏'),
650            ("🗻12∈45🌏", 11..15, '🌏'),
651            ("Hello 🗻12∈45🌏 World", 26..27, 'd'),
652        ];
653        for (text, r, c) in cases {
654            let actual = text.char_ranges().last().unwrap();
655
656            assert_eq!(actual.0, r);
657            assert_eq!(actual.1, c);
658            assert!(text[r].chars().eq([c]));
659        }
660    }
661
662    #[test]
663    fn test_nth() {
664        let cases = [
665            "Hello World",
666            "Hello 👋 World 🌏",
667            "🗻12∈45🌏",
668            "Hello 🗻12∈45🌏 World",
669        ];
670        for text in cases {
671            // Since `nth()` doesn't use `next()` internally,
672            // then they are be compared
673            let char_ranges1 = text.char_ranges();
674            let char_ranges2 = iter::from_fn({
675                let mut char_ranges = text.char_ranges();
676                move || char_ranges.nth(0)
677            });
678            assert!(char_ranges1.eq(char_ranges2));
679        }
680    }
681
682    #[test]
683    fn test_nth_back() {
684        let cases = [
685            "Hello World",
686            "Hello 👋 World 🌏",
687            "🗻12∈45🌏",
688            "Hello 🗻12∈45🌏 World",
689        ];
690        for text in cases {
691            // Since `nth_back()` doesn't use `next_back()` internally,
692            // then they are be compared
693            let char_ranges1 = text.char_ranges().rev();
694            let char_ranges2 = iter::from_fn({
695                let mut char_ranges = text.char_ranges();
696                move || char_ranges.nth_back(0)
697            });
698            assert!(char_ranges1.eq(char_ranges2));
699        }
700    }
701
702    #[test]
703    fn test_char_ranges() {
704        let text = "Hello World";
705        for (r, c) in text.char_ranges() {
706            let mut chars = text[r].chars();
707            assert_eq!(chars.next(), Some(c));
708            assert_eq!(chars.next(), None);
709        }
710
711        let text = "🗻12∈45🌏";
712        for (r, c) in text.char_ranges() {
713            let mut chars = text[r].chars();
714            assert_eq!(chars.next(), Some(c));
715            assert_eq!(chars.next(), None);
716        }
717    }
718
719    #[test]
720    fn test_char_ranges_start() {
721        let text = "Hello 🗻12∈45🌏 World";
722        let mut chars = text.char_ranges();
723        while let Some((r, _c)) = chars.next_back() {
724            assert_eq!(chars.as_str(), &text[..r.start]);
725        }
726    }
727
728    #[test]
729    fn test_char_ranges_end() {
730        let text = "Hello 🗻12∈45🌏 World";
731        let mut chars = text.char_ranges();
732        while let Some((r, _c)) = chars.next() {
733            assert_eq!(chars.as_str(), &text[r.end..]);
734        }
735    }
736
737    #[test]
738    fn test_full_range() {
739        let text = "Hello 🗻12∈45🌏 World\n";
740        let mut chars = text.char_ranges();
741        while let Some((first, _)) = chars.next() {
742            let (last, _) = chars.next_back().unwrap();
743            assert_eq!(chars.as_str(), &text[first.end..last.start]);
744        }
745    }
746
747    #[test]
748    fn test_offset() {
749        let text = "Hello 👋 World 🌏";
750        let mut chars = text.char_ranges();
751
752        let emoji_end = {
753            assert_eq!(chars.next(), Some((0..1, 'H')));
754            assert_eq!(chars.next(), Some((1..2, 'e')));
755            assert_eq!(chars.next(), Some((2..3, 'l')));
756            assert_eq!(chars.next(), Some((3..4, 'l')));
757            assert_eq!(chars.next(), Some((4..5, 'o')));
758            assert_eq!(chars.next(), Some((5..6, ' ')));
759
760            let emoji_waving_hand = chars.next();
761            assert_eq!(emoji_waving_hand, Some((6..10, '👋')));
762
763            emoji_waving_hand.unwrap().0.end
764        };
765
766        let offset_chars = text[emoji_end..].char_ranges().offset(emoji_end);
767        assert_eq!(chars.as_str(), offset_chars.as_str());
768
769        for offset_char in offset_chars {
770            assert_eq!(chars.next(), Some(offset_char));
771        }
772
773        assert_eq!(chars.next(), None);
774    }
775}