Coverage Report

Created: 2026-10-28 06:37

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/rust/registry/src/index.crates.io-1949cf8c6b5b557f/pest-2.9.5/src/position.rs
Line
Count
Source
1
// pest. The Elegant Parser
2
// Copyright (c) 2018 Dragoș Tiselice
3
//
4
// Licensed under the Apache License, Version 2.0
5
// <LICENSE-APACHE or http://www.apache.org/licenses/LICENSE-2.0> or the MIT
6
// license <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
7
// option. All files in the project carrying such notice may not be copied,
8
// modified, or distributed except according to those terms.
9
10
use core::cmp::Ordering;
11
use core::fmt;
12
use core::hash::{Hash, Hasher};
13
use core::ops::Range;
14
use core::ptr;
15
use core::str;
16
17
use crate::span;
18
19
/// A cursor position in a `&str` which provides useful methods to manually parse that string.
20
#[derive(Clone, Copy)]
21
pub struct Position<'i> {
22
    input: &'i str,
23
    pos: usize,
24
}
25
26
impl<'i> Position<'i> {
27
    /// Create a new `Position` without checking invariants. (Checked with `debug_assertions`.)
28
0
    pub(crate) fn new_internal(input: &str, pos: usize) -> Position<'_> {
29
0
        debug_assert!(input.get(pos..).is_some());
30
0
        Position { input, pos }
31
0
    }
32
33
    /// Attempts to create a new `Position` at the given position. If the specified position is
34
    /// an invalid index, or the specified position is not a valid UTF8 boundary, then None is
35
    /// returned.
36
    ///
37
    /// # Examples
38
    /// ```
39
    /// # use pest::Position;
40
    /// let cheart = '💖';
41
    /// let heart = "💖";
42
    /// assert_eq!(Position::new(heart, 1), None);
43
    /// assert_ne!(Position::new(heart, cheart.len_utf8()), None);
44
    /// ```
45
0
    pub fn new(input: &str, pos: usize) -> Option<Position<'_>> {
46
0
        input.get(pos..).map(|_| Position { input, pos })
47
0
    }
48
49
    /// Creates a `Position` at the start of a `&str`.
50
    ///
51
    /// # Examples
52
    ///
53
    /// ```
54
    /// # use pest::Position;
55
    /// let start = Position::from_start("");
56
    /// assert_eq!(start.pos(), 0);
57
    /// ```
58
    #[inline]
59
0
    pub fn from_start(input: &'i str) -> Position<'i> {
60
        // Position 0 is always safe because it's always a valid UTF-8 border.
61
0
        Position { input, pos: 0 }
62
0
    }
Unexecuted instantiation: <pest::position::Position>::from_start
Unexecuted instantiation: <pest::position::Position>::from_start
63
64
    /// Returns the byte position of this `Position` as a `usize`.
65
    ///
66
    /// # Examples
67
    ///
68
    /// ```
69
    /// # use pest::Position;
70
    /// let input = "ab";
71
    /// let mut start = Position::from_start(input);
72
    ///
73
    /// assert_eq!(start.pos(), 0);
74
    /// ```
75
    #[inline]
76
0
    pub fn pos(&self) -> usize {
77
0
        self.pos
78
0
    }
Unexecuted instantiation: <pest::position::Position>::pos
Unexecuted instantiation: <pest::position::Position>::pos
79
80
    /// Creates a `Span` from two `Position`s.
81
    ///
82
    /// # Panics
83
    ///
84
    /// Panics if the positions come from different inputs.
85
    ///
86
    /// # Examples
87
    ///
88
    /// ```
89
    /// # use pest::Position;
90
    /// let input = "ab";
91
    /// let start = Position::from_start(input);
92
    /// let span = start.span(&start.clone());
93
    ///
94
    /// assert_eq!(span.start(), 0);
95
    /// assert_eq!(span.end(), 0);
96
    /// ```
97
    #[inline]
98
0
    pub fn span(&self, other: &Position<'i>) -> span::Span<'i> {
99
0
        if ptr::eq(self.input, other.input)
100
        /* && self.input.get(self.pos..other.pos).is_some() */
101
        {
102
0
            span::Span::new_internal(self.input, self.pos, other.pos)
103
        } else {
104
            // TODO: maybe a panic if self.pos < other.pos
105
0
            panic!("span created from positions from different inputs")
106
        }
107
0
    }
108
109
    /// Returns the line and column number of this `Position`.
110
    ///
111
    /// This is an O(n) operation, where n is the number of chars in the input.
112
    /// You better use [`pair.line_col()`](struct.Pair.html#method.line_col) instead.
113
    ///
114
    /// # Panics
115
    ///
116
    /// Panics if the position is out of bounds.
117
    ///
118
    /// # Examples
119
    ///
120
    /// ```
121
    /// # use pest;
122
    /// # #[allow(non_camel_case_types)]
123
    /// # #[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
124
    /// enum Rule {}
125
    ///
126
    /// let input = "\na";
127
    /// let mut state: Box<pest::ParserState<'_, Rule>> = pest::ParserState::new(input);
128
    /// let mut result = state.match_string("\na");
129
    /// assert!(result.is_ok());
130
    /// assert_eq!(result.unwrap().position().line_col(), (2, 2));
131
    /// ```
132
    #[inline]
133
0
    pub fn line_col(&self) -> (usize, usize) {
134
0
        if self.pos > self.input.len() {
135
0
            panic!("position out of bounds");
136
0
        }
137
0
        let mut pos = self.pos;
138
0
        let slice = &self.input[..pos];
139
0
        let mut chars = slice.chars().peekable();
140
141
0
        let mut line_col = (1, 1);
142
143
0
        while pos != 0 {
144
0
            match chars.next() {
145
                Some('\r') => {
146
0
                    if let Some(&'\n') = chars.peek() {
147
0
                        chars.next();
148
149
0
                        if pos == 1 {
150
0
                            pos -= 1;
151
0
                        } else {
152
0
                            pos -= 2;
153
0
                        }
154
155
0
                        line_col = (line_col.0 + 1, 1);
156
0
                    } else {
157
0
                        pos -= 1;
158
0
                        line_col = (line_col.0, line_col.1 + 1);
159
0
                    }
160
                }
161
0
                Some('\n') => {
162
0
                    pos -= 1;
163
0
                    line_col = (line_col.0 + 1, 1);
164
0
                }
165
0
                Some(c) => {
166
0
                    pos -= c.len_utf8();
167
0
                    line_col = (line_col.0, line_col.1 + 1);
168
0
                }
169
0
                None => unreachable!(),
170
            }
171
        }
172
173
0
        line_col
174
0
    }
Unexecuted instantiation: <pest::position::Position>::line_col
Unexecuted instantiation: <pest::position::Position>::line_col
175
176
    /// Returns the entire line of the input that contains this `Position`.
177
    ///
178
    /// # Panics
179
    ///
180
    /// Panics if the position is out of bounds (e.g., beyond the end of the input).
181
    ///
182
    /// # Examples
183
    ///
184
    /// ```
185
    /// # use pest;
186
    /// # #[allow(non_camel_case_types)]
187
    /// # #[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
188
    /// enum Rule {}
189
    ///
190
    /// let input = "\na";
191
    /// let mut state: Box<pest::ParserState<'_, Rule>> = pest::ParserState::new(input);
192
    /// let mut result = state.match_string("\na");
193
    /// assert!(result.is_ok());
194
    /// assert_eq!(result.unwrap().position().line_of(), "a");
195
    /// ```
196
    #[inline]
197
0
    pub fn line_of(&self) -> &'i str {
198
0
        if self.pos > self.input.len() {
199
0
            panic!("position out of bounds");
200
0
        };
201
        // Safe since start and end can only be valid UTF-8 borders.
202
0
        &self.input[self.find_line_start()..self.find_line_end()]
203
0
    }
Unexecuted instantiation: <pest::position::Position>::line_of
Unexecuted instantiation: <pest::position::Position>::line_of
204
205
0
    pub(crate) fn find_line_start(&self) -> usize {
206
0
        if self.input.is_empty() {
207
0
            return 0;
208
0
        };
209
        // Position's pos is always a UTF-8 border.
210
0
        let start = self
211
0
            .input
212
0
            .char_indices()
213
0
            .rev()
214
0
            .skip_while(|&(i, _)| i >= self.pos)
215
0
            .find(|&(_, c)| c == '\n');
216
0
        match start {
217
0
            Some((i, _)) => i + 1,
218
0
            None => 0,
219
        }
220
0
    }
221
222
0
    pub(crate) fn find_line_end(&self) -> usize {
223
0
        if self.input.is_empty() {
224
0
            0
225
0
        } else if self.pos == self.input.len() - 1 {
226
0
            self.input.len()
227
        } else {
228
            // Position's pos is always a UTF-8 border.
229
0
            let end = self
230
0
                .input
231
0
                .char_indices()
232
0
                .skip_while(|&(i, _)| i < self.pos)
233
0
                .find(|&(_, c)| c == '\n');
234
0
            match end {
235
0
                Some((i, _)) => i + 1,
236
0
                None => self.input.len(),
237
            }
238
        }
239
0
    }
240
241
    /// Returns `true` when the `Position` points to the start of the input `&str`.
242
    #[inline]
243
0
    pub(crate) fn at_start(&self) -> bool {
244
0
        self.pos == 0
245
0
    }
Unexecuted instantiation: <pest::position::Position>::at_start
Unexecuted instantiation: <pest::position::Position>::at_start
246
247
    /// Returns `true` when the `Position` points to the end of the input `&str`.
248
    #[inline]
249
0
    pub(crate) fn at_end(&self) -> bool {
250
0
        self.pos == self.input.len()
251
0
    }
Unexecuted instantiation: <pest::position::Position>::at_end
Unexecuted instantiation: <pest::position::Position>::at_end
252
253
    /// Skips `n` `char`s from the `Position` and returns `true` if the skip was possible or `false`
254
    /// otherwise. If the return value is `false`, `pos` will not be updated.
255
    #[inline]
256
0
    pub(crate) fn skip(&mut self, n: usize) -> bool {
257
0
        let skipped = {
258
0
            let mut len = 0;
259
            // Position's pos is always a UTF-8 border.
260
0
            let mut chars = self.input[self.pos..].chars();
261
0
            for _ in 0..n {
262
0
                if let Some(c) = chars.next() {
263
0
                    len += c.len_utf8();
264
0
                } else {
265
0
                    return false;
266
                }
267
            }
268
0
            len
269
        };
270
271
0
        self.pos += skipped;
272
0
        true
273
0
    }
274
275
    /// Goes back `n` `char`s from the `Position` and returns `true` if the skip was possible or `false`
276
    /// otherwise. If the return value is `false`, `pos` will not be updated.
277
    #[inline]
278
0
    pub(crate) fn skip_back(&mut self, n: usize) -> bool {
279
0
        let skipped = {
280
0
            let mut len = 0;
281
            // Position's pos is always a UTF-8 border.
282
0
            let mut chars = self.input[..self.pos].chars().rev();
283
0
            for _ in 0..n {
284
0
                if let Some(c) = chars.next() {
285
0
                    len += c.len_utf8();
286
0
                } else {
287
0
                    return false;
288
                }
289
            }
290
0
            len
291
        };
292
293
0
        self.pos -= skipped;
294
0
        true
295
0
    }
296
297
    /// Skips until one of the given `strings` is found. If none of the `strings` can be found,
298
    /// this function will return `false` but its `pos` will *still* be updated.
299
    #[inline]
300
0
    pub(crate) fn skip_until(&mut self, strings: &[&str]) -> bool {
301
        #[cfg(not(feature = "memchr"))]
302
        {
303
            self.skip_until_basic(strings)
304
        }
305
        #[cfg(feature = "memchr")]
306
        {
307
0
            match strings {
308
0
                [] => (),
309
0
                [s1] => {
310
0
                    if let Some(from) =
311
0
                        memchr::memmem::find(&self.input.as_bytes()[self.pos..], s1.as_bytes())
312
                    {
313
0
                        self.pos += from;
314
0
                        return true;
315
0
                    }
316
                }
317
0
                [s1, s2] if !s1.is_empty() && !s2.is_empty() => {
318
0
                    let b1 = s1.as_bytes()[0];
319
0
                    let b2 = s2.as_bytes()[0];
320
0
                    let miter = memchr::memchr2_iter(b1, b2, &self.input.as_bytes()[self.pos..]);
321
0
                    for from in miter {
322
0
                        let start = &self.input[self.pos + from..];
323
0
                        if start.starts_with(s1) || start.starts_with(s2) {
324
0
                            self.pos += from;
325
0
                            return true;
326
0
                        }
327
                    }
328
                }
329
0
                [s1, s2, s3] if !s1.is_empty() && !s2.is_empty() && !s3.is_empty() => {
330
0
                    let b1 = s1.as_bytes()[0];
331
0
                    let b2 = s2.as_bytes()[0];
332
0
                    let b3 = s3.as_bytes()[0];
333
0
                    let miter =
334
0
                        memchr::memchr3_iter(b1, b2, b3, &self.input.as_bytes()[self.pos..]);
335
0
                    for from in miter {
336
0
                        let start = &self.input[self.pos + from..];
337
0
                        if start.starts_with(s1) || start.starts_with(s2) || start.starts_with(s3) {
338
0
                            self.pos += from;
339
0
                            return true;
340
0
                        }
341
                    }
342
                }
343
                _ => {
344
0
                    return self.skip_until_basic(strings);
345
                }
346
            }
347
0
            self.pos = self.input.len();
348
0
            false
349
        }
350
0
    }
351
352
    #[inline]
353
0
    fn skip_until_basic(&mut self, strings: &[&str]) -> bool {
354
        // TODO: optimize with Aho-Corasick, e.g. https://crates.io/crates/daachorse?
355
0
        for from in self.pos..self.input.len() {
356
0
            let bytes = if let Some(string) = self.input.get(from..) {
357
0
                string.as_bytes()
358
            } else {
359
0
                continue;
360
            };
361
362
0
            for slice in strings.iter() {
363
0
                let to = slice.len();
364
0
                if Some(slice.as_bytes()) == bytes.get(0..to) {
365
0
                    self.pos = from;
366
0
                    return true;
367
0
                }
368
            }
369
        }
370
371
0
        self.pos = self.input.len();
372
0
        false
373
0
    }
374
375
    /// Matches the char at the `Position` against a specified character and returns `true` if a match
376
    /// was made. If no match was made, returns `false`.
377
    /// `pos` will not be updated in either case.
378
    #[inline]
379
0
    pub(crate) fn match_char(&self, c: char) -> bool {
380
0
        matches!(self.input[self.pos..].chars().next(), Some(cc) if c == cc)
381
0
    }
Unexecuted instantiation: <pest::position::Position>::match_char
Unexecuted instantiation: <pest::position::Position>::match_char
382
383
    /// Matches the char at the `Position` against a filter function and returns `true` if a match
384
    /// was made. If no match was made, returns `false` and `pos` will not be updated.
385
    #[inline]
386
0
    pub(crate) fn match_char_by<F>(&mut self, f: F) -> bool
387
0
    where
388
0
        F: FnOnce(char) -> bool,
389
    {
390
0
        if let Some(c) = self.input[self.pos..].chars().next() {
391
0
            if f(c) {
392
0
                self.pos += c.len_utf8();
393
0
                true
394
            } else {
395
0
                false
396
            }
397
        } else {
398
0
            false
399
        }
400
0
    }
401
402
    /// Matches `string` from the `Position` and returns `true` if a match was made or `false`
403
    /// otherwise. If no match was made, `pos` will not be updated.
404
    #[inline]
405
0
    pub(crate) fn match_string(&mut self, string: &str) -> bool {
406
0
        let to = self.pos + string.len();
407
408
0
        if Some(string.as_bytes()) == self.input.as_bytes().get(self.pos..to) {
409
0
            self.pos = to;
410
0
            true
411
        } else {
412
0
            false
413
        }
414
0
    }
Unexecuted instantiation: <pest::position::Position>::match_string
Unexecuted instantiation: <pest::position::Position>::match_string
415
416
    /// Case-insensitively matches `string` from the `Position` and returns `true` if a match was
417
    /// made or `false` otherwise. If no match was made, `pos` will not be updated.
418
    #[inline]
419
0
    pub(crate) fn match_insensitive(&mut self, string: &str) -> bool {
420
0
        let matched = {
421
0
            let slice = &self.input[self.pos..];
422
0
            if let Some(slice) = slice.get(0..string.len()) {
423
0
                slice.eq_ignore_ascii_case(string)
424
            } else {
425
0
                false
426
            }
427
        };
428
429
0
        if matched {
430
0
            self.pos += string.len();
431
0
            true
432
        } else {
433
0
            false
434
        }
435
0
    }
436
437
    /// Matches `char` `range` from the `Position` and returns `true` if a match was made or `false`
438
    /// otherwise. If no match was made, `pos` will not be updated.
439
    #[inline]
440
0
    pub(crate) fn match_range(&mut self, range: Range<char>) -> bool {
441
0
        if let Some(c) = self.input[self.pos..].chars().next() {
442
0
            if range.start <= c && c <= range.end {
443
0
                self.pos += c.len_utf8();
444
0
                return true;
445
0
            }
446
0
        }
447
448
0
        false
449
0
    }
Unexecuted instantiation: <pest::position::Position>::match_range
Unexecuted instantiation: <pest::position::Position>::match_range
450
}
451
452
impl fmt::Debug for Position<'_> {
453
0
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
454
0
        f.debug_struct("Position").field("pos", &self.pos).finish()
455
0
    }
456
}
457
458
impl<'i> PartialEq for Position<'i> {
459
0
    fn eq(&self, other: &Position<'i>) -> bool {
460
0
        ptr::eq(self.input, other.input) && self.pos == other.pos
461
0
    }
462
}
463
464
impl Eq for Position<'_> {}
465
466
#[allow(clippy::non_canonical_partial_ord_impl)]
467
impl<'i> PartialOrd for Position<'i> {
468
0
    fn partial_cmp(&self, other: &Position<'i>) -> Option<Ordering> {
469
0
        if ptr::eq(self.input, other.input) {
470
0
            self.pos.partial_cmp(&other.pos)
471
        } else {
472
0
            None
473
        }
474
0
    }
475
}
476
477
/// # Panics
478
///
479
/// Panics when comparing positions from different input strings.
480
impl<'i> Ord for Position<'i> {
481
0
    fn cmp(&self, other: &Position<'i>) -> Ordering {
482
0
        self.partial_cmp(other)
483
0
            .expect("cannot compare positions from different strs")
484
0
    }
485
}
486
487
impl Hash for Position<'_> {
488
0
    fn hash<H: Hasher>(&self, state: &mut H) {
489
0
        (self.input as *const str).hash(state);
490
0
        self.pos.hash(state);
491
0
    }
492
}
493
494
#[cfg(test)]
495
mod tests {
496
    use super::*;
497
498
    #[test]
499
    fn empty() {
500
        let input = "";
501
        assert!(Position::new(input, 0).unwrap().match_string(""));
502
        assert!(!Position::new(input, 0).unwrap().match_string("a"));
503
    }
504
505
    #[test]
506
    fn parts() {
507
        let input = "asdasdf";
508
509
        assert!(Position::new(input, 0).unwrap().match_string("asd"));
510
        assert!(Position::new(input, 3).unwrap().match_string("asdf"));
511
    }
512
513
    #[test]
514
    fn line_col() {
515
        let input = "a\rb\nc\r\nd嗨";
516
517
        assert_eq!(Position::new(input, 0).unwrap().line_col(), (1, 1));
518
        assert_eq!(Position::new(input, 1).unwrap().line_col(), (1, 2));
519
        assert_eq!(Position::new(input, 2).unwrap().line_col(), (1, 3));
520
        assert_eq!(Position::new(input, 3).unwrap().line_col(), (1, 4));
521
        assert_eq!(Position::new(input, 4).unwrap().line_col(), (2, 1));
522
        assert_eq!(Position::new(input, 5).unwrap().line_col(), (2, 2));
523
        assert_eq!(Position::new(input, 6).unwrap().line_col(), (2, 3));
524
        assert_eq!(Position::new(input, 7).unwrap().line_col(), (3, 1));
525
        assert_eq!(Position::new(input, 8).unwrap().line_col(), (3, 2));
526
        assert_eq!(Position::new(input, 11).unwrap().line_col(), (3, 3));
527
        let input = "abcd嗨";
528
        assert_eq!(Position::new(input, 7).unwrap().line_col(), (1, 6));
529
    }
530
531
    #[test]
532
    fn line_of() {
533
        let input = "a\rb\nc\r\nd嗨";
534
535
        assert_eq!(Position::new(input, 0).unwrap().line_of(), "a\rb\n");
536
        assert_eq!(Position::new(input, 1).unwrap().line_of(), "a\rb\n");
537
        assert_eq!(Position::new(input, 2).unwrap().line_of(), "a\rb\n");
538
        assert_eq!(Position::new(input, 3).unwrap().line_of(), "a\rb\n");
539
        assert_eq!(Position::new(input, 4).unwrap().line_of(), "c\r\n");
540
        assert_eq!(Position::new(input, 5).unwrap().line_of(), "c\r\n");
541
        assert_eq!(Position::new(input, 6).unwrap().line_of(), "c\r\n");
542
        assert_eq!(Position::new(input, 7).unwrap().line_of(), "d嗨");
543
        assert_eq!(Position::new(input, 8).unwrap().line_of(), "d嗨");
544
        assert_eq!(Position::new(input, 11).unwrap().line_of(), "d嗨");
545
    }
546
547
    #[test]
548
    fn line_of_empty() {
549
        let input = "";
550
551
        assert_eq!(Position::new(input, 0).unwrap().line_of(), "");
552
    }
553
554
    #[test]
555
    fn line_of_new_line() {
556
        let input = "\n";
557
558
        assert_eq!(Position::new(input, 0).unwrap().line_of(), "\n");
559
    }
560
561
    #[test]
562
    fn line_of_between_new_line() {
563
        let input = "\n\n";
564
565
        assert_eq!(Position::new(input, 1).unwrap().line_of(), "\n");
566
    }
567
568
    fn measure_skip(input: &str, pos: usize, n: usize) -> Option<usize> {
569
        let mut p = Position::new(input, pos).unwrap();
570
        if p.skip(n) {
571
            Some(p.pos - pos)
572
        } else {
573
            None
574
        }
575
    }
576
577
    #[test]
578
    fn skip_empty() {
579
        let input = "";
580
581
        assert_eq!(measure_skip(input, 0, 0), Some(0));
582
        assert_eq!(measure_skip(input, 0, 1), None);
583
    }
584
585
    #[test]
586
    fn skip() {
587
        let input = "d嗨";
588
589
        assert_eq!(measure_skip(input, 0, 0), Some(0));
590
        assert_eq!(measure_skip(input, 0, 1), Some(1));
591
        assert_eq!(measure_skip(input, 1, 1), Some(3));
592
    }
593
594
    #[test]
595
    fn skip_until() {
596
        let input = "ab ac";
597
        let pos = Position::from_start(input);
598
599
        let mut test_pos = pos;
600
        test_pos.skip_until(&["a", "b"]);
601
        assert_eq!(test_pos.pos(), 0);
602
603
        test_pos = pos;
604
        test_pos.skip_until(&["b"]);
605
        assert_eq!(test_pos.pos(), 1);
606
607
        test_pos = pos;
608
        test_pos.skip_until(&["ab"]);
609
        assert_eq!(test_pos.pos(), 0);
610
611
        test_pos = pos;
612
        test_pos.skip_until(&["ac", "z"]);
613
        assert_eq!(test_pos.pos(), 3);
614
615
        test_pos = pos;
616
        assert!(!test_pos.skip_until(&["z"]));
617
        assert_eq!(test_pos.pos(), 5);
618
    }
619
620
    #[test]
621
    fn skip_until_three_strings() {
622
        let input = "xx yy b";
623
        let pos = Position::from_start(input);
624
625
        // All three strings non-empty: the three-string fast path must find "b".
626
        let mut test_pos = pos;
627
        assert!(test_pos.skip_until(&["a", "c", "b"]));
628
        assert_eq!(test_pos.pos(), 6);
629
630
        // A string that does not occur: nothing matches, position goes to the end.
631
        test_pos = pos;
632
        assert!(!test_pos.skip_until(&["q", "r", "z"]));
633
        assert_eq!(test_pos.pos(), input.len());
634
    }
635
636
    #[test]
637
    fn skip_until_empty_third_string() {
638
        // The empty string matches at the current position, so skip_until must
639
        // return true without advancing. Regression test for the inverted guard
640
        // and the wrong `b3` binding in the three-string fast path.
641
        let input = "xyb";
642
        let pos = Position::from_start(input);
643
644
        let mut test_pos = pos;
645
        assert!(test_pos.skip_until(&["a", "b", ""]));
646
        assert_eq!(test_pos.pos(), 0);
647
648
        // Empty string first: still matches immediately.
649
        test_pos = pos;
650
        assert!(test_pos.skip_until(&["", "a", "b"]));
651
        assert_eq!(test_pos.pos(), 0);
652
653
        // Only the third string is empty, no "a"/"b" present: still matches at once.
654
        let input2 = "xyz";
655
        let pos2 = Position::from_start(input2);
656
        let mut test_pos = pos2;
657
        assert!(test_pos.skip_until(&["a", "b", ""]));
658
        assert_eq!(test_pos.pos(), 0);
659
    }
660
661
    #[test]
662
    fn match_range() {
663
        let input = "b";
664
665
        assert!(Position::new(input, 0).unwrap().match_range('a'..'c'));
666
        assert!(Position::new(input, 0).unwrap().match_range('b'..'b'));
667
        assert!(!Position::new(input, 0).unwrap().match_range('a'..'a'));
668
        assert!(!Position::new(input, 0).unwrap().match_range('c'..'c'));
669
        assert!(Position::new(input, 0).unwrap().match_range('a'..'嗨'));
670
    }
671
672
    #[test]
673
    fn match_insensitive() {
674
        let input = "AsdASdF";
675
676
        assert!(Position::new(input, 0).unwrap().match_insensitive("asd"));
677
        assert!(Position::new(input, 3).unwrap().match_insensitive("asdf"));
678
    }
679
680
    #[test]
681
    fn cmp() {
682
        let input = "a";
683
        let start = Position::from_start(input);
684
        let mut end = start;
685
686
        assert!(end.skip(1));
687
        let result = start.cmp(&end);
688
689
        assert_eq!(result, Ordering::Less);
690
    }
691
692
    #[test]
693
    #[should_panic]
694
    fn cmp_panic() {
695
        let input1 = "a";
696
        let input2 = "b";
697
        let pos1 = Position::from_start(input1);
698
        let pos2 = Position::from_start(input2);
699
700
        let _ = pos1.cmp(&pos2);
701
    }
702
703
    #[test]
704
    #[cfg(feature = "std")]
705
    fn hash() {
706
        use std::collections::HashSet;
707
708
        let input = "a";
709
        let start = Position::from_start(input);
710
        let mut positions = HashSet::new();
711
712
        positions.insert(start);
713
    }
714
}