1pub fn len(text: &str) -> usize {
7 text.encode_utf16().count()
8}
9
10pub fn utf16_index(text: &str, byte: usize) -> usize {
13 len(&text[..text.floor_char_boundary(byte)])
14}
15
16pub fn byte_index(text: &str, units: usize) -> usize {
19 let mut count = 0;
20 for (index, c) in text.char_indices() {
21 if count >= units {
22 return index;
23 }
24 count += c.len_utf16();
25 }
26 text.len()
27}
28
29pub fn slice(text: &str, start: usize, end: usize) -> String {
32 let units: Vec<u16> = text.encode_utf16().collect();
33 let end = end.min(units.len());
34 let start = start.min(end);
35 String::from_utf16_lossy(&units[start..end])
36}
37
38pub fn decode_utf8_stream(pending: &mut Vec<u8>, bytes: &[u8]) -> String {
42 pending.extend_from_slice(bytes);
43 let valid = match std::str::from_utf8(pending) {
44 Ok(text) => text.len(),
45 Err(error) if error.error_len().is_none() => error.valid_up_to(),
46 Err(_) => pending.len(),
47 };
48 let text = String::from_utf8_lossy(&pending[..valid]).into_owned();
49 pending.drain(..valid);
50 text
51}
52
53pub fn round(value: f64) -> f64 {
55 (value + 0.5).floor()
56}
57
58pub fn to_fixed(value: f64, digits: usize) -> String {
60 if !value.is_finite() {
61 return value.to_string();
62 }
63 if value < 0.0 {
64 let positive = to_fixed(-value, digits);
65 return if positive.bytes().all(|byte| matches!(byte, b'0' | b'.')) {
66 positive
67 } else {
68 format!("-{positive}")
69 };
70 }
71 let wide = format!("{value:.40}");
72 let (whole, fraction) = wide.split_once('.').unwrap_or((&wide, ""));
73 let mut number: Vec<u8> = whole
74 .bytes()
75 .chain(fraction.bytes().chain(std::iter::repeat(b'0')).take(digits))
76 .collect();
77 if fraction
78 .as_bytes()
79 .get(digits)
80 .is_some_and(|digit| *digit >= b'5')
81 {
82 let mut index = number.len();
83 loop {
84 if index == 0 {
85 number.insert(0, b'1');
86 break;
87 }
88 index -= 1;
89 if number[index] == b'9' {
90 number[index] = b'0';
91 } else {
92 number[index] += 1;
93 break;
94 }
95 }
96 }
97 let text = String::from_utf8_lossy(&number).into_owned();
98 if digits == 0 {
99 return text;
100 }
101 let split = text.len() - digits;
102 format!("{}.{}", &text[..split], &text[split..])
103}
104
105pub fn errno(kind: std::io::ErrorKind) -> Option<(&'static str, &'static str)> {
108 use std::io::ErrorKind;
109 Some(match kind {
110 ErrorKind::NotFound => ("ENOENT", "no such file or directory"),
111 ErrorKind::PermissionDenied => ("EACCES", "permission denied"),
112 ErrorKind::AlreadyExists => ("EEXIST", "file already exists"),
113 ErrorKind::IsADirectory => ("EISDIR", "illegal operation on a directory"),
114 ErrorKind::NotADirectory => ("ENOTDIR", "not a directory"),
115 ErrorKind::DirectoryNotEmpty => ("ENOTEMPTY", "directory not empty"),
116 ErrorKind::InvalidInput => ("EINVAL", "invalid argument"),
117 ErrorKind::ReadOnlyFilesystem => ("EROFS", "read-only file system"),
118 _ => return None,
119 })
120}
121
122pub fn node_error(err: &std::io::Error, syscall: &str, path: &std::path::Path) -> String {
125 let path = path.display();
126 match errno(err.kind()) {
127 Some(("EISDIR", text)) if syscall == "read" => format!("EISDIR: {text}, read"),
129 Some((code, text)) => format!("{code}: {text}, {syscall} '{path}'"),
130 None => format!("{err}, {syscall} '{path}'"),
131 }
132}
133
134#[cfg(test)]
135mod tests {
136 use super::*;
137
138 #[test]
139 fn byte_utf16_indices() {
140 let text = "é😀x";
141 assert_eq!(utf16_index(text, 2), 1);
142 assert_eq!(utf16_index(text, 6), 3);
143 assert_eq!(byte_index(text, 3), 6);
144 assert_eq!(byte_index(text, 9), text.len());
145 }
146
147 #[test]
148 fn io_errors_like_node() {
149 let error = |kind| std::io::Error::from(kind);
150 let path = std::path::Path::new("/a/b");
151 assert_eq!(
152 node_error(&error(std::io::ErrorKind::NotFound), "open", path),
153 "ENOENT: no such file or directory, open '/a/b'"
154 );
155 assert_eq!(
156 node_error(&error(std::io::ErrorKind::IsADirectory), "read", path),
157 "EISDIR: illegal operation on a directory, read"
158 );
159 assert_eq!(
160 node_error(&error(std::io::ErrorKind::AlreadyExists), "mkdir", path),
161 "EEXIST: file already exists, mkdir '/a/b'"
162 );
163 assert_eq!(
164 node_error(&error(std::io::ErrorKind::ReadOnlyFilesystem), "open", path),
165 "EROFS: read-only file system, open '/a/b'"
166 );
167 assert_eq!(
168 node_error(&error(std::io::ErrorKind::DirectoryNotEmpty), "rmdir", path),
169 "ENOTEMPTY: directory not empty, rmdir '/a/b'"
170 );
171 assert_eq!(
172 errno(std::io::ErrorKind::InvalidInput),
173 Some(("EINVAL", "invalid argument"))
174 );
175 assert_eq!(errno(std::io::ErrorKind::Other), None);
176 }
177
178 #[test]
179 fn counts_slices_utf16() {
180 assert_eq!(len("a😀"), 3);
181 assert_eq!(slice("a😀b", 0, 3), "a😀");
182 assert_eq!(slice("a😀b", 0, 2), "a\u{fffd}");
183 assert_eq!(slice("abc", 2, 10), "c");
184 }
185
186 #[test]
187 fn decodes_utf8_streams() {
188 let mut pending = Vec::new();
189 let euro = "€".as_bytes();
190 assert_eq!(decode_utf8_stream(&mut pending, &euro[..2]), "");
191 assert_eq!(decode_utf8_stream(&mut pending, &euro[2..]), "€");
192 assert_eq!(decode_utf8_stream(&mut pending, b"a\xffb"), "a\u{fffd}b");
193 assert!(pending.is_empty());
194 }
195
196 #[test]
197 fn rounds_like_javascript() {
198 assert_eq!(to_fixed(0.05, 1), "0.1");
199 assert_eq!(to_fixed(1.25, 1), "1.3");
200 assert_eq!(to_fixed(1.005, 2), "1.00");
201 assert_eq!(to_fixed(9.96, 1), "10.0");
202 assert_eq!(to_fixed(0.0, 1), "0.0");
203 assert_eq!(to_fixed(2.5, 0), "3");
204 assert_eq!(to_fixed(-1.25, 1), "-1.3");
205 assert_eq!(round(2.5), 3.0);
206 assert_eq!(round(-2.5), -2.0);
207 }
208}