Skip to main content

line_column/
lib.rs

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