duat_core/text/
bytes.rs

1use std::{iter::FusedIterator, ops::RangeBounds};
2
3use gapbuf::GapBuffer;
4use lender::{DoubleEndedLender, ExactSizeLender, Lender, Lending};
5
6use super::{Point, TextRange, records::Records, utf8_char_width};
7
8/// The bytes of a [`Text`], encoded in UTF-8
9///
10/// [`Text`]: super::Text
11#[derive(Default, Clone)]
12pub struct Bytes {
13    buf: GapBuffer<u8>,
14    records: Records<[usize; 3]>,
15}
16
17impl Bytes {
18    /// Returns a new instance of a [`Buffer`]
19    pub(crate) fn new(string: &str) -> Self {
20        let buf = GapBuffer::from_iter(string.bytes());
21
22        let len = buf.len();
23        let chars = string.chars().count();
24        let lines = buf.iter().filter(|b| **b == b'\n').count();
25        Self {
26            buf,
27            records: Records::with_max([len, chars, lines]),
28        }
29    }
30
31    ////////// Querying functions
32
33    /// The [`Point`] at the end of the text
34    pub fn len(&self) -> Point {
35        let [b, c, l] = self.records.max();
36        Point::from_raw(b, c, l)
37    }
38
39    /// Whether or not there are any characters in [`Bytes`]
40    ///
41    /// # Note
42    ///
43    /// This does not check for tags, so with a [`Tag::Ghost`],
44    /// there could actually be a "string" of characters on the
45    /// [`Text`], it just wouldn't be considered real "text".
46    ///
47    /// [`Tag::Ghost`]: super::Ghost
48    /// [`Text`]: super::Text
49    pub fn is_empty(&self) -> bool {
50        self.buf.is_empty()
51    }
52
53    /// The `char` at the [`Point`]'s position
54    pub fn char_at(&self, p: Point) -> Option<char> {
55        if p.byte() >= self.len().byte() {
56            return None;
57        }
58
59        let [s0, s1] = self.strs(..).to_array();
60        if p.byte() < s0.len() {
61            s0[p.byte()..].chars().next()
62        } else {
63            s1[p.byte() - s0.len()..].chars().next()
64        }
65    }
66
67    /// An [`Iterator`] over the bytes in a given [range]
68    ///
69    /// If the range is fully or partially out of bounds, one or both
70    /// of the slices might be empty.
71    ///
72    /// [range]: TextRange
73    pub fn buffers(&self, range: impl TextRange) -> Buffers<'_> {
74        let range = range.to_range(self.buf.len());
75        let (s0, s1) = self.buf.range(range).as_slices();
76        Buffers([s0.iter(), s1.iter()])
77    }
78
79    /// An [`Iterator`] over the [`&str`]s of the [`Text`]
80    ///
81    /// # Note
82    ///
83    /// The reason why this function returns two strings is that the
84    /// contents of the text are stored in a [`GapBuffer`], which
85    /// works with two strings.
86    ///
87    /// If you want to iterate over them, you can do the following:
88    ///
89    /// ```rust
90    /// # use duat_core::{text::Point, prelude::*};
91    /// # let (p0, p1) = (Point::default(), Point::default());
92    /// # let text = Text::new();
93    /// let bytes = text.bytes();
94    /// bytes.strs(p0..p1).flat_map(str::chars);
95    /// ```
96    ///
97    /// Do note that you should avoid iterators like [`str::lines`],
98    /// as they will separate the line that is partially owned by each
99    /// [`&str`]:
100    ///
101    /// ```rust
102    /// let broken_up_line = [
103    ///     "This is line 1, business as usual.\nThis is line 2, but it",
104    ///     "is broken into two separate strings.\nSo 4 lines would be counted, instead of 3",
105    /// ];
106    /// ```
107    ///
108    /// # [`TextRange`] behavior:
109    ///
110    /// If you give a single [`usize`]/[`Point`], it will be
111    /// interpreted as a range from.
112    ///
113    /// If you want the two full [`&str`]s, see [`strs`]
114    ///
115    /// [`&str`]: str
116    /// [`Text`]: super::Text
117    /// [range]: TextRange
118    /// [`strs`]: Self::strs
119    pub fn strs(&self, range: impl TextRange) -> Strs<'_> {
120        let range = range.to_range(self.buf.len());
121        Strs(self.strs_in_range_inner(range).into_iter())
122    }
123
124    /// Returns an iterator over the lines in a given range
125    ///
126    /// The lines are inclusive, that is, it will iterate over the
127    /// whole line, not just the parts within the range.
128    ///
129    /// [range]: TextRange
130    pub fn lines(&self, range: impl TextRange) -> Lines<'_> {
131        let range = range.to_range(self.len().byte());
132        let start = self.point_at_line(self.point_at(range.start).line());
133        let end = {
134            let end = self.point_at(range.end);
135            let line_start = self.point_at_line(end.line());
136            match line_start == end {
137                true => end,
138                false => self.point_at_line((end.line() + 1).min(self.len().line())),
139            }
140        };
141
142        // If the gap is outside of the range, we can just iterate through it
143        // regularly
144        let (fwd_i, rev_i) = (start.line(), end.line());
145        if let Some(str) = self.get_contiguous(start..end) {
146            let lines = [str.lines(), "".lines()];
147            Lines::new(lines, None, fwd_i, rev_i)
148        // If the gap is within the range, but on a line split, we
149        // can just iterate through two sets of lines.
150        } else if end.byte() > start.byte()
151            && self.buf[self.buf.gap() - 1] != b'\n'
152            && self.buf[self.buf.gap()] != b'\n'
153        {
154            let [str_0, str_1] = self.strs(start..end).to_array();
155            let lines = [str_0.lines(), str_1.lines()];
156            Lines::new(lines, None, fwd_i, rev_i)
157            // Otherwise, the line that was split will need to be
158            // allocated and returned separately.
159        } else {
160            let [str0, str1] = self.strs(start..end).to_array();
161
162            let (before, split0) = match str0.rsplit_once('\n') {
163                Some((before, split)) => (before, split),
164                None => ("", str0),
165            };
166            let (after, split1) = match str1.split_once('\n') {
167                Some((after, split)) => (after, split),
168                None => ("", str1),
169            };
170
171            let lines = [before.lines(), after.lines()];
172            let split_line = Some(split0.to_string() + split1);
173            Lines::new(lines, split_line, fwd_i, rev_i)
174        }
175    }
176
177    /// Returns the two `&str`s in the byte range.
178    fn strs_in_range_inner(&self, range: impl RangeBounds<usize>) -> [&str; 2] {
179        use std::str::from_utf8_unchecked;
180
181        let (s0, s1) = self.buf.as_slices();
182        let (start, end) = crate::get_ends(range, self.buf.len());
183        let (start, end) = (start, end);
184        // Make sure the start and end are in character bounds.
185        assert!(
186            [start, end]
187                .into_iter()
188                .filter_map(|b| self.buf.get(b))
189                .all(|b| utf8_char_width(*b) > 0),
190        );
191
192        unsafe {
193            let r0 = start.min(s0.len())..end.min(s0.len());
194            let r1 = start.saturating_sub(s0.len()).min(s1.len())
195                ..end.saturating_sub(s0.len()).min(s1.len());
196
197            [from_utf8_unchecked(&s0[r0]), from_utf8_unchecked(&s1[r1])]
198        }
199    }
200
201    /// The [`Point`] corresponding to the byte position, 0 indexed
202    ///
203    /// If the byte position would fall in between two characters
204    /// (because the first one comprises more than one byte), the
205    /// first character is chosen as the [`Point`] where the byte is
206    /// located.
207    ///
208    /// # Panics
209    ///
210    /// Will panic if `b` is greater than the length of the text
211    #[inline(always)]
212    pub fn point_at(&self, b: usize) -> Point {
213        assert!(
214            b <= self.len().byte(),
215            "byte out of bounds: the len is {}, but the byte is {b}",
216            self.len().byte()
217        );
218        let [c_b, c_c, mut c_l] = self.records.closest_to(b);
219
220        let found = if b >= c_b {
221            let [s0, s1] = self.strs_in_range_inner(c_b..);
222
223            s0.char_indices()
224                .chain(s1.char_indices().map(|(b, char)| (b + s0.len(), char)))
225                .enumerate()
226                .map(|(i, (this_b, char))| {
227                    c_l += (char == '\n') as usize;
228                    (c_b + this_b, c_c + i, c_l - (char == '\n') as usize)
229                })
230                .take_while(|&(rhs, ..)| b >= rhs)
231                .last()
232        } else {
233            let mut c_len = 0;
234            self.strs_in_range_inner(..c_b)
235                .into_iter()
236                .flat_map(str::chars)
237                .rev()
238                .enumerate()
239                .map(|(i, char)| {
240                    c_l -= (char == '\n') as usize;
241                    c_len += char.len_utf8();
242                    (c_b - c_len, c_c - (i + 1), c_l)
243                })
244                .take_while(|&(rhs, ..)| b <= rhs)
245                .last()
246        };
247
248        found
249            .map(|(b, c, l)| Point::from_raw(b, c, l))
250            .unwrap_or(self.len())
251    }
252
253    /// The [`Point`] associated with the `c`th char
254    ///
255    /// # Panics
256    ///
257    /// Will panic if `c` is greater than the number of chars in the
258    /// text.
259    #[inline(always)]
260    pub fn point_at_char(&self, c: usize) -> Point {
261        assert!(
262            c <= self.len().char(),
263            "char out of bounds: the len is {}, but the char is {c}",
264            self.len().char()
265        );
266        let [c_b, c_c, mut c_l] = self.records.closest_to_by_key(c, |[_, c, _]| *c);
267
268        let found = if c >= c_c {
269            let [s0, s1] = self.strs_in_range_inner(c_b..);
270
271            s0.char_indices()
272                .chain(s1.char_indices().map(|(b, char)| (b + s0.len(), char)))
273                .enumerate()
274                .map(|(i, (this_b, char))| {
275                    c_l += (char == '\n') as usize;
276                    (c_b + this_b, c_c + i, c_l - (char == '\n') as usize)
277                })
278                .take_while(|&(_, rhs, _)| c >= rhs)
279                .last()
280        } else {
281            let mut c_len = 0;
282            self.strs_in_range_inner(..)
283                .into_iter()
284                .flat_map(str::chars)
285                .rev()
286                .enumerate()
287                .map(|(i, char)| {
288                    c_l -= (char == '\n') as usize;
289                    c_len += char.len_utf8();
290                    (c_b - c_len, c_c - (i + 1), c_l)
291                })
292                .take_while(|&(_, rhs, _)| c <= rhs)
293                .last()
294        };
295
296        found
297            .map(|(b, c, l)| Point::from_raw(b, c, l))
298            .unwrap_or(self.len())
299    }
300
301    /// The [`Point`] where the `l`th line starts, 0 indexed
302    ///
303    /// If `l == number_of_lines`, returns the last point of the
304    /// text.
305    ///
306    /// # Panics
307    ///
308    /// Will panic if the number `l` is greater than the number of
309    /// lines on the text
310    #[inline(always)]
311    pub fn point_at_line(&self, l: usize) -> Point {
312        assert!(
313            l <= self.len().line(),
314            "line out of bounds: the len is {}, but the line is {l}",
315            self.len().line()
316        );
317        let (c_b, c_c, mut c_l) = {
318            let [mut b, mut c, l] = self.records.closest_to_by_key(l, |[.., l]| *l);
319            self.strs_in_range_inner(..b)
320                .into_iter()
321                .flat_map(str::chars)
322                .rev()
323                .take_while(|c| *c != '\n')
324                .for_each(|char| {
325                    b -= char.len_utf8();
326                    c -= 1;
327                });
328            (b, c, l)
329        };
330
331        let found = if l >= c_l {
332            let [s0, s1] = self.strs_in_range_inner(c_b..);
333
334            s0.char_indices()
335                .chain(s1.char_indices().map(|(b, char)| (b + s0.len(), char)))
336                .enumerate()
337                .map(|(i, (this_b, char))| {
338                    c_l += (char == '\n') as usize;
339                    (c_b + this_b, c_c + i, c_l - (char == '\n') as usize)
340                })
341                .find(|&(.., rhs)| l == rhs)
342        } else {
343            let mut c_len = 0;
344            self.strs_in_range_inner(..c_b)
345                .into_iter()
346                .flat_map(str::chars)
347                .rev()
348                .enumerate()
349                .map(|(i, char)| {
350                    c_l -= (char == '\n') as usize;
351                    c_len += char.len_utf8();
352                    (c_b - c_len, c_c - (i + 1), c_l)
353                })
354                .take_while(|&(.., rhs)| l <= rhs)
355                .last()
356        };
357
358        found
359            .map(|(b, c, l)| Point::from_raw(b, c, l))
360            .unwrap_or(self.len())
361    }
362
363    /// The start and end [`Point`]s for the `l`th line
364    ///
365    /// If `l == number_of_lines`, these points will be the same.
366    ///
367    /// # Panics
368    ///
369    /// Will panic if the number `l` is greater than the number of
370    /// lines on the text
371    #[inline(always)]
372    pub fn points_of_line(&self, l: usize) -> [Point; 2] {
373        assert!(
374            l <= self.len().line(),
375            "byte out of bounds: the len is {}, but the line is {l}",
376            self.len().line()
377        );
378
379        let start = self.point_at_line(l);
380        let end = self
381            .chars_fwd(start)
382            .find_map(|(p, _)| (p.line() > start.line()).then_some(p))
383            .unwrap_or(start);
384        [start, end]
385    }
386
387    /// The last [`Point`] associated with a `char`
388    ///
389    /// This will give the [`Point`] of the last `char` of the text.
390    /// The difference between this method and [`len`] is that
391    /// it will return a [`Point`] one position earlier than it. If
392    /// the text is completely empty, it will return [`None`].
393    ///
394    /// [`len`]: Self::len
395    pub fn last_point(&self) -> Option<Point> {
396        self.strs(..)
397            .chars()
398            .next_back()
399            .map(|char| self.len().rev(char))
400    }
401
402    /// A forward iterator of the [`char`]s of [`Bytes`]
403    ///
404    /// Each [`char`] will be accompanied by a [`Point`], which is the
405    /// position where said character starts, e.g.
406    /// [`Point::default()`] for the first character
407    pub fn chars_fwd(&self, p: Point) -> impl Iterator<Item = (Point, char)> + '_ {
408        self.strs_in_range_inner(p.byte()..)
409            .into_iter()
410            .flat_map(str::chars)
411            .scan(p, |p, char| {
412                let old_p = *p;
413                *p = p.fwd(char);
414                Some((old_p, char))
415            })
416    }
417
418    /// A reverse iterator of the [`char`]s in [`Bytes`]
419    ///
420    /// Each [`char`] will be accompanied by a [`Point`], which is the
421    /// position where said character starts, e.g.
422    /// [`Point::default()`] for the first character
423    pub fn chars_rev(&self, p: Point) -> impl Iterator<Item = (Point, char)> + '_ {
424        self.strs_in_range_inner(..p.byte())
425            .into_iter()
426            .flat_map(str::chars)
427            .rev()
428            .scan(p, |p, char| {
429                *p = p.rev(char);
430                Some((*p, char))
431            })
432    }
433
434    ////////// Modification functions
435
436    /// Applies a [`Change`] to the [`GapBuffer`] within
437    ///
438    /// [`Change`]: super::Change
439    pub(super) fn apply_change(&mut self, change: super::Change<&str>) {
440        let edit = change.added_str();
441        let start = change.start();
442
443        let new_len = {
444            let lines = edit.bytes().filter(|b| *b == b'\n').count();
445            [edit.len(), edit.chars().count(), lines]
446        };
447
448        let old_len = unsafe {
449            let range = start.byte()..change.taken_end().byte();
450            let str = String::from_utf8_unchecked(
451                self.buf
452                    .splice(range, edit.as_bytes().iter().cloned())
453                    .collect(),
454            );
455
456            let lines = str.bytes().filter(|b| *b == b'\n').count();
457            [str.len(), str.chars().count(), lines]
458        };
459
460        let start_rec = [start.byte(), start.char(), start.line()];
461        self.records.transform(start_rec, old_len, new_len);
462        self.records.insert(start_rec);
463    }
464
465    /// Extends this [`Bytes`] with another
466    pub(super) fn extend(&mut self, other: Self) {
467        self.buf.extend(other.buf);
468        self.records
469            .transform(self.records.max(), [0, 0, 0], other.records.max())
470    }
471
472    /// Adds a record in the given position
473    pub(super) fn add_record(&mut self, [b, c, l]: [usize; 3]) {
474        self.records.insert([b, c, l]);
475    }
476
477    ////////// One str functions
478
479    /// Gets a single [`&str`] from a given [range]
480    ///
481    /// This is the equivalent of calling
482    /// [`Bytes::make_contiguous`] and [`Bytes::get_contiguous`].
483    /// While this takes leas space in code, calling the other two
484    /// functions means that you won't be mutably borrowing the
485    /// [`Bytes`] anymore, so if that matters to you, you should do
486    /// that.
487    ///
488    /// [`&str`]: str
489    /// [range]: TextRange
490    pub fn contiguous(&mut self, range: impl TextRange) -> &str {
491        self.make_contiguous(range.clone());
492        self.get_contiguous(range).unwrap()
493    }
494
495    /// Moves the [`GapBuffer`]'s gap, so that the `range` is whole
496    ///
497    /// The return value is the value of the gap, if the second `&str`
498    /// is the contiguous one.
499    pub fn make_contiguous(&mut self, range: impl TextRange) {
500        let range = range.to_range(self.len().byte());
501        let gap = self.buf.gap();
502
503        if range.end <= gap || range.start >= gap {
504            return;
505        }
506
507        if gap.abs_diff(range.start) < gap.abs_diff(range.end) {
508            self.buf.set_gap(range.start);
509        } else {
510            self.buf.set_gap(range.end);
511        }
512    }
513
514    /// Tries to get a contiguous [`&str`] from the [`Bytes`]
515    ///
516    /// Returns [`None`] if the gap of the inner buffer was within the
517    /// given range.
518    ///
519    /// You can ensure that the gap is outside of that range by
520    /// calling [`make_contiguous`], although that requires a mutable
521    /// reference.
522    ///
523    /// [`&str`]: str
524    /// [`make_contiguous`]: Self::make_contiguous
525    pub fn get_contiguous(&self, range: impl TextRange) -> Option<&str> {
526        let range = range.to_range(self.len().byte());
527        let [s0, s1] = self.strs(..).to_array();
528
529        if range.end <= self.buf.gap() {
530            s0.get(range)
531        } else {
532            let gap = self.buf.gap();
533            s1.get(range.start - gap..range.end - gap)
534        }
535    }
536}
537
538/// A [`Lender`] over the lines on [`Bytes`]
539///
540/// The reason for this being a [`Lender`], rather than a regular
541/// [`Iterator`] is because the [`Bytes`] use a [`GapBuffer`] within,
542/// which means that any line may be split in two. In order to still
543/// return it as an `&str`, a new [`String`] needs to be allocated,
544/// which will be owned by the [`Lines`], hence the [`Lender`] trait.
545pub struct Lines<'a> {
546    lines: [std::str::Lines<'a>; 2],
547    split_line: Option<String>,
548    fwd_i: usize,
549    rev_i: usize,
550    split_line_used: bool,
551}
552
553impl<'a> Lines<'a> {
554    fn new(
555        lines: [std::str::Lines<'a>; 2],
556        split_line: Option<String>,
557        fwd_i: usize,
558        rev_i: usize,
559    ) -> Self {
560        Self {
561            lines,
562            split_line,
563            fwd_i,
564            rev_i,
565            split_line_used: false,
566        }
567    }
568}
569
570impl<'a, 'text> Lending<'a> for Lines<'text> {
571    type Lend = (usize, &'a str);
572}
573
574impl<'a> Lender for Lines<'a> {
575    fn next(&mut self) -> Option<lender::Lend<'_, Self>> {
576        self.lines[0]
577            .next()
578            .or_else(|| {
579                if self.split_line_used {
580                    None
581                } else {
582                    self.split_line_used = true;
583                    self.split_line.as_deref()
584                }
585            })
586            .or_else(|| self.lines[1].next())
587            .map(|line| {
588                self.fwd_i += 1;
589                (self.fwd_i - 1, line)
590            })
591    }
592
593    fn size_hint(&self) -> (usize, Option<usize>) {
594        (self.rev_i - self.fwd_i, Some(self.rev_i - self.fwd_i))
595    }
596}
597
598impl<'a> DoubleEndedLender for Lines<'a> {
599    fn next_back(&mut self) -> Option<lender::Lend<'_, Self>> {
600        self.lines[1]
601            .next_back()
602            .or_else(|| {
603                if self.split_line_used {
604                    None
605                } else {
606                    self.split_line_used = true;
607                    self.split_line.as_deref()
608                }
609            })
610            .or_else(|| self.lines[0].next_back())
611            .map(|line| {
612                self.rev_i -= 1;
613                (self.rev_i, line)
614            })
615    }
616}
617
618impl<'a> ExactSizeLender for Lines<'a> {}
619
620/// An [`Iterator`] over the bytes in a [`Text`]
621///
622/// [`Text`]: super::Text
623#[derive(Clone)]
624pub struct Buffers<'a>([std::slice::Iter<'a, u8>; 2]);
625
626impl<'a> Buffers<'a> {
627    /// Converts this [`Iterator`] into an array of its two parts
628    pub fn to_array(&self) -> [&'a [u8]; 2] {
629        self.0.clone().map(|iter| iter.as_slice())
630    }
631}
632
633impl<'a> Iterator for Buffers<'a> {
634    type Item = u8;
635
636    fn next(&mut self) -> Option<Self::Item> {
637        self.0[0].next().or_else(|| self.0[1].next()).copied()
638    }
639
640    fn size_hint(&self) -> (usize, Option<usize>) {
641        let (l0, u0) = self.0[0].size_hint();
642        let (l1, u1) = self.0[1].size_hint();
643        (l0 + l1, Some(u0.unwrap() + u1.unwrap()))
644    }
645}
646
647impl<'a> ExactSizeIterator for Buffers<'a> {}
648
649impl<'a> DoubleEndedIterator for Buffers<'a> {
650    fn next_back(&mut self) -> Option<Self::Item> {
651        self.0[1]
652            .next_back()
653            .or_else(|| self.0[0].next_back())
654            .copied()
655    }
656}
657
658/// An [`Iterator`] over the [`&str`]s in a [`Text`]
659///
660/// [`&str`]: str
661/// [`Text`]: super::Text
662#[derive(Clone)]
663pub struct Strs<'a>(std::array::IntoIter<&'a str, 2>);
664
665impl<'a> Strs<'a> {
666    /// Converts this [`Iterator`] into an array of its two parts
667    pub fn to_array(&self) -> [&'a str; 2] {
668        let strs = self.0.as_slice();
669        [
670            strs.first().copied().unwrap_or(""),
671            strs.last().copied().unwrap_or(""),
672        ]
673    }
674
675    /// Iterates over the [`char`]s of both [`&str`]s
676    ///
677    /// [`&str`]: str
678    pub fn chars(self) -> impl DoubleEndedIterator<Item = char> + 'a {
679        let [s0, s1] = self.to_array();
680        s0.chars().chain(s1.chars())
681    }
682}
683
684impl<'a> Iterator for Strs<'a> {
685    type Item = &'a str;
686
687    fn next(&mut self) -> Option<Self::Item> {
688        self.0.next()
689    }
690
691    fn size_hint(&self) -> (usize, Option<usize>) {
692        self.0.size_hint()
693    }
694}
695
696impl ExactSizeIterator for Strs<'_> {}
697
698impl DoubleEndedIterator for Strs<'_> {
699    fn next_back(&mut self) -> Option<Self::Item> {
700        self.0.next_back()
701    }
702}
703
704impl FusedIterator for Strs<'_> {}
705
706impl std::fmt::Display for Strs<'_> {
707    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
708        let [s0, s1] = self.to_array();
709        write!(f, "{s0}{s1}")
710    }
711}
712
713impl std::fmt::Debug for Bytes {
714    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
715        f.debug_struct("Bytes")
716            .field("buf", &self.strs(..).to_array())
717            .field("records", &self.records)
718            .finish()
719    }
720}
721
722impl PartialEq for Bytes {
723    fn eq(&self, other: &Self) -> bool {
724        self.buf.as_slices() == other.buf.as_slices()
725    }
726}
727
728impl PartialEq<&str> for Bytes {
729    fn eq(&self, other: &&str) -> bool {
730        let [s0, s1] = self.strs(..).to_array();
731        other.len() == s0.len() + s1.len() && &other[..s0.len()] == s0 && &other[s0.len()..] == s1
732    }
733}
734
735impl PartialEq<String> for Bytes {
736    fn eq(&self, other: &String) -> bool {
737        let [s0, s1] = self.strs(..).to_array();
738        other.len() == s0.len() + s1.len() && &other[..s0.len()] == s0 && &other[s0.len()..] == s1
739    }
740}
741
742impl PartialEq for Strs<'_> {
743    fn eq(&self, other: &Self) -> bool {
744        self.to_array() == other.to_array()
745    }
746}
747
748impl PartialEq<&str> for Strs<'_> {
749    fn eq(&self, other: &&str) -> bool {
750        let [s0, s1] = self.to_array();
751        other.len() == s0.len() + s1.len() && &other[..s0.len()] == s0 && &other[s0.len()..] == s1
752    }
753}
754
755impl PartialEq<String> for Strs<'_> {
756    fn eq(&self, other: &String) -> bool {
757        let [s0, s1] = self.to_array();
758        other.len() == s0.len() + s1.len() && &other[..s0.len()] == s0 && &other[s0.len()..] == s1
759    }
760}