line_column/
lib.rs

1#![no_std]
2#![doc = include_str!("../README.md")]
3
4#[cfg(test)]
5mod tests;
6
7const UNINIT_LINE_COL: (u32, u32) = (0, 0);
8
9/// Get multiple pairs of lines and columns may be faster
10///
11/// Like [`line_column`]
12///
13/// # Panics
14///
15/// - index out of `0..s.len()`
16/// - index not on char boundary
17#[must_use]
18#[track_caller]
19pub fn line_columns<const N: usize>(
20    s: &str,
21    indexs: [usize; N],
22) -> [(u32, u32); N] {
23    let len = s.len();
24
25    for index in indexs {
26        assert!(index <= len,
27                "index {index} out of str length {len} of `{s:?}`");
28        assert!(s.is_char_boundary(index),
29                "byte index {index} is not a char boundary of `{s:?}`");
30    }
31
32    let result = line_columns_unchecked(s, indexs);
33
34    debug_assert!(! result.contains(&UNINIT_LINE_COL),
35                  "impl error, report bug issue");
36    result
37}
38
39/// Get multiple pairs of lines and columns may be faster
40///
41/// Like [`char_line_column`]
42///
43/// # Panics
44/// - `indexs` any index greater than `s.chars().count()`
45#[must_use]
46#[track_caller]
47pub fn char_line_columns<const N: usize>(
48    s: &str,
49    indexs: [usize; N],
50) -> [(u32, u32); N] {
51    let mut len = 0;
52    let mut result = [UNINIT_LINE_COL; N];
53
54    let last_loc = s.chars()
55        .enumerate()
56        .inspect(|&(i, _)| len = i+1)
57        .fold((1, 1), |(line, column), (cur, ch)|
58    {
59        for (i, &index) in indexs.iter().enumerate() {
60            if index == cur {
61                result[i] = (line, column);
62            }
63        }
64
65        if ch == '\n' {
66            (line+1, 1)
67        } else {
68            (line, column+1)
69        }
70    });
71
72    for index in indexs {
73        assert!(index <= len,
74                "char index {index} out of str length {len} of `{s:?}`");
75    }
76
77    for (i, &index) in indexs.iter().enumerate() {
78        if index >= len {
79            result[i] = last_loc;
80        }
81    }
82
83    result
84}
85
86/// Get multiple of lines and columns may be faster
87///
88/// Use byte index
89///
90/// If the index does not fall on the character boundary,
91/// the unspecified results
92#[must_use]
93pub fn line_columns_unchecked<const N: usize>(
94    s: &str,
95    indexs: [usize; N],
96) -> [(u32, u32); N] {
97    let len = s.len();
98    let mut result = [UNINIT_LINE_COL; N];
99
100    let last_loc = s.char_indices()
101        .fold((1, 1), |(line, column), (cur, ch)|
102    {
103        for (i, &index) in indexs.iter().enumerate() {
104            if index == cur {
105                result[i] = (line, column);
106            }
107        }
108
109        if ch == '\n' {
110            (line+1, 1)
111        } else {
112            (line, column+1)
113        }
114    });
115
116    for (i, &index) in indexs.iter().enumerate() {
117        if index == len {
118            result[i] = last_loc;
119        }
120    }
121
122    result
123}
124
125/// Get str byte index of line and column
126///
127/// If the line out the length of the `s`, return `s.len()`
128///
129/// # Panics
130/// - line by zero
131///
132/// # Examples
133/// ```
134/// # use line_column::index;
135/// assert_eq!(index("", 1, 1),             0);
136/// assert_eq!(index("a", 1, 1),            0);
137/// assert_eq!(index("a", 1, 2),            1);
138/// assert_eq!(index("a\n", 1, 2),          1);
139/// assert_eq!(index("a\n", 2, 1),          2);
140/// assert_eq!(index("a\nx", 2, 2),         3);
141/// assert_eq!(index("你好\n世界", 1, 2),   3); // byte index
142/// assert_eq!(index("你好\n世界", 1, 3),   6);
143/// assert_eq!(index("你好\n世界", 2, 1),   7);
144/// ```
145#[must_use]
146#[track_caller]
147pub fn index(s: &str, line: u32, column: u32) -> usize {
148    assert_ne!(line, 0);
149
150    let mut i = 0;
151    for _ in 1..line {
152        let Some(lf) = s[i..].find('\n') else { return s.len() };
153        i += lf+1;
154    }
155    if column == 0 {
156        return i.saturating_sub(1)
157    }
158    s[i..].chars()
159        .take_while(|ch| *ch != '\n')
160        .take(column as usize - 1)
161        .fold(i, |acc, ch| acc + ch.len_utf8())
162}
163
164/// Get str char index of line and column
165///
166/// If the line out the length of the `s`, return `s.chars().count()`
167///
168/// # Panics
169/// - line by zero
170///
171/// # Examples
172/// ```
173/// # use line_column::char_index;
174/// assert_eq!(char_index("", 1, 1),            0);
175/// assert_eq!(char_index("a", 1, 1),           0);
176/// assert_eq!(char_index("你好\n世界", 1, 2),  1);
177/// assert_eq!(char_index("你好\n世界", 1, 3),  2);
178/// assert_eq!(char_index("你好\n世界", 2, 1),  3);
179/// ```
180#[must_use]
181#[track_caller]
182pub fn char_index(s: &str, mut line: u32, mut column: u32) -> usize {
183    assert_ne!(line, 0);
184
185    let mut back_style = column == 0;
186    line -= 1;
187    column = column.saturating_sub(1);
188
189    let mut i = 0usize;
190    let mut chars = s.chars();
191    loop {
192        let Some(ch) = chars.next() else {
193            back_style &= line == 0;
194            break
195        };
196        if line == 0 {
197            if column == 0 || ch == '\n' { break }
198            column -= 1;
199        } else if ch == '\n' {
200            line -= 1;
201        }
202        i += 1;
203    }
204    i.saturating_sub(back_style.into())
205}
206
207/// Get tuple of line and column, use byte index
208///
209/// Use LF (0x0A) to split newline, also compatible with CRLF (0x0D 0x0A)
210///
211/// # Examples
212/// ```
213/// # use line_column::line_column;
214/// assert_eq!(line_column("", 0),     (1, 1));
215/// assert_eq!(line_column("a", 0),    (1, 1));
216/// assert_eq!(line_column("a", 1),    (1, 2));
217/// assert_eq!(line_column("ab", 1),   (1, 2));
218/// assert_eq!(line_column("a\n", 1),  (1, 2));
219/// assert_eq!(line_column("a\n", 2),  (2, 1));
220/// assert_eq!(line_column("a\nb", 2), (2, 1));
221/// ```
222#[inline]
223#[must_use]
224#[track_caller]
225pub fn line_column(s: &str, index: usize) -> (u32, u32) {
226    line_columns(s, [index])[0]
227}
228
229/// Get tuple of line and column, use char index
230///
231/// Use LF (0x0A) to split newline, also compatible with CRLF (0x0D 0x0A)
232///
233/// # Panics
234/// - `index > s.chars().count()`
235///
236/// # Examples
237/// ```
238/// # use line_column::char_line_column;
239/// assert_eq!(char_line_column("", 0),         (1, 1));
240/// assert_eq!(char_line_column("a", 0),        (1, 1));
241/// assert_eq!(char_line_column("a", 1),        (1, 2));
242/// assert_eq!(char_line_column("ab", 1),       (1, 2));
243/// assert_eq!(char_line_column("😀\n", 1),     (1, 2));
244/// assert_eq!(char_line_column("😀\n", 2),     (2, 1));
245/// assert_eq!(char_line_column("😀\n❓", 2),   (2, 1));
246/// assert_eq!(char_line_column("😀\n❓", 2),   (2, 1));
247/// ```
248#[inline]
249#[must_use]
250#[track_caller]
251pub fn char_line_column(s: &str, index: usize) -> (u32, u32) {
252    char_line_columns(s, [index])[0]
253}