Skip to main content

rlsp_yaml_parser/
pos.rs

1// SPDX-License-Identifier: MIT
2
3/// A position within the input stream.
4///
5/// `line` is 1-based; `column` is 0-based.
6#[derive(Debug, Clone, Copy, PartialEq, Eq)]
7pub struct Pos {
8    pub byte_offset: usize,
9    pub char_offset: usize,
10    pub line: usize,
11    pub column: usize,
12}
13
14impl Pos {
15    /// The position representing the start of a document.
16    pub const ORIGIN: Self = Self {
17        byte_offset: 0,
18        char_offset: 0,
19        line: 1,
20        column: 0,
21    };
22
23    /// Advance the position by one character.
24    ///
25    /// If `ch` is a line feed (`\n`) the line counter is incremented and the
26    /// column is reset to 0.  For all other characters the column advances by
27    /// one.  `byte_offset` advances by `ch.len_utf8()` and `char_offset`
28    /// always advances by 1.
29    #[must_use]
30    pub const fn advance(self, ch: char) -> Self {
31        let byte_offset = self.byte_offset + ch.len_utf8();
32        let char_offset = self.char_offset + 1;
33        if ch == '\n' {
34            Self {
35                byte_offset,
36                char_offset,
37                line: self.line + 1,
38                column: 0,
39            }
40        } else {
41            Self {
42                byte_offset,
43                char_offset,
44                line: self.line,
45                column: self.column + 1,
46            }
47        }
48    }
49}
50
51/// A half-open span `[start, end)` within the input stream.
52#[derive(Debug, Clone, Copy, PartialEq, Eq)]
53pub struct Span {
54    pub start: Pos,
55    pub end: Pos,
56}
57
58#[cfg(test)]
59mod tests {
60    use super::*;
61
62    #[test]
63    fn pos_origin_is_start_of_document() {
64        let pos = Pos::ORIGIN;
65        assert_eq!(pos.byte_offset, 0);
66        assert_eq!(pos.char_offset, 0);
67        assert_eq!(pos.line, 1);
68        assert_eq!(pos.column, 0);
69    }
70
71    #[test]
72    fn pos_fields_are_accessible() {
73        let pos = Pos {
74            byte_offset: 10,
75            char_offset: 8,
76            line: 3,
77            column: 4,
78        };
79        assert_eq!(pos.byte_offset, 10);
80        assert_eq!(pos.char_offset, 8);
81        assert_eq!(pos.line, 3);
82        assert_eq!(pos.column, 4);
83    }
84
85    #[test]
86    fn pos_is_copy() {
87        let pos = Pos::ORIGIN;
88        let pos2 = pos;
89        let _ = pos.byte_offset;
90        let _ = pos2.byte_offset;
91    }
92
93    #[test]
94    fn span_is_copy() {
95        let span = Span {
96            start: Pos::ORIGIN,
97            end: Pos::ORIGIN,
98        };
99        let span2 = span;
100        let _ = span.start;
101        let _ = span2.start;
102    }
103
104    #[test]
105    fn advance_ascii_increments_byte_and_char_and_column() {
106        let pos = Pos::ORIGIN.advance('a');
107        assert_eq!(pos.byte_offset, 1);
108        assert_eq!(pos.char_offset, 1);
109        assert_eq!(pos.line, 1);
110        assert_eq!(pos.column, 1);
111    }
112
113    #[test]
114    fn advance_newline_increments_line_and_resets_column() {
115        let pos = Pos::ORIGIN.advance('a').advance('\n');
116        assert_eq!(pos.byte_offset, 2);
117        assert_eq!(pos.char_offset, 2);
118        assert_eq!(pos.line, 2);
119        assert_eq!(pos.column, 0);
120    }
121
122    #[test]
123    fn advance_multibyte_char_increments_byte_offset_by_utf8_len() {
124        // '中' is 3 bytes in UTF-8
125        let pos = Pos::ORIGIN.advance('中');
126        assert_eq!(pos.byte_offset, 3);
127        assert_eq!(pos.char_offset, 1);
128        assert_eq!(pos.line, 1);
129        assert_eq!(pos.column, 1);
130    }
131
132    #[test]
133    fn advance_multiple_lines() {
134        let pos = Pos::ORIGIN
135            .advance('a')
136            .advance('\n')
137            .advance('b')
138            .advance('\n')
139            .advance('c');
140        assert_eq!(pos.line, 3);
141        assert_eq!(pos.column, 1);
142    }
143}