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}