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}