line_column/
lib.rs

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