1use std::io as sio;
4use std::process::Command;
5use std::{cmp::Ordering, ops, time::Instant};
6
7mod macros;
8
9pub mod anymap;
10pub mod assert;
11pub mod non_empty_vec;
12pub mod panic_context;
13pub mod process;
14pub mod rand;
15pub mod thread;
16pub mod variance;
17
18pub use itertools;
19
20#[inline(always)]
21pub const fn is_ci() -> bool {
22 option_env!("CI").is_some()
23}
24
25pub fn hash_once<Hasher: std::hash::Hasher + Default>(thing: impl std::hash::Hash) -> u64 {
26 std::hash::BuildHasher::hash_one(&std::hash::BuildHasherDefault::<Hasher>::default(), thing)
27}
28
29#[must_use]
30#[expect(clippy::print_stderr, reason = "only visible to developers")]
31pub fn timeit(label: &'static str) -> impl Drop {
32 let start = Instant::now();
33 defer(move || eprintln!("{}: {:.2}", label, start.elapsed().as_nanos()))
34}
35
36#[expect(clippy::print_stderr, reason = "only visible to developers")]
38pub fn print_backtrace() {
39 #[cfg(feature = "backtrace")]
40 eprintln!("{:?}", backtrace::Backtrace::new());
41
42 #[cfg(not(feature = "backtrace"))]
43 eprintln!(
44 r#"Enable the backtrace feature.
45Uncomment `default = [ "backtrace" ]` in `crates/stdx/Cargo.toml`.
46"#
47 );
48}
49
50pub trait TupleExt {
51 type Head;
52 type Tail;
53 fn head(self) -> Self::Head;
54 fn tail(self) -> Self::Tail;
55}
56
57impl<T, U> TupleExt for (T, U) {
58 type Head = T;
59 type Tail = U;
60 fn head(self) -> Self::Head {
61 self.0
62 }
63 fn tail(self) -> Self::Tail {
64 self.1
65 }
66}
67
68impl<T, U, V> TupleExt for (T, U, V) {
69 type Head = T;
70 type Tail = V;
71 fn head(self) -> Self::Head {
72 self.0
73 }
74 fn tail(self) -> Self::Tail {
75 self.2
76 }
77}
78
79impl<T> TupleExt for &T
80where
81 T: TupleExt + Copy,
82{
83 type Head = T::Head;
84 type Tail = T::Tail;
85 fn head(self) -> Self::Head {
86 (*self).head()
87 }
88 fn tail(self) -> Self::Tail {
89 (*self).tail()
90 }
91}
92
93pub fn to_lower_snake_case(s: &str) -> String {
94 to_snake_case(s, char::to_lowercase)
95}
96pub fn to_upper_snake_case(s: &str) -> String {
97 to_snake_case(s, char::to_uppercase)
98}
99
100fn to_snake_case<F, I>(mut s: &str, change_case: F) -> String
103where
104 F: Fn(char) -> I,
105 I: Iterator<Item = char>,
106{
107 let mut words = vec![];
108
109 s = s.trim_start_matches(|c: char| {
111 if c == '_' {
112 words.push(String::new());
113 true
114 } else {
115 false
116 }
117 });
118
119 for s in s.split('_') {
120 let mut last_upper = false;
121 let mut buf = String::new();
122
123 if s.is_empty() {
124 continue;
125 }
126
127 for ch in s.chars() {
128 if !buf.is_empty() && buf != "'" && ch.is_uppercase() && !last_upper {
129 words.push(buf);
130 buf = String::new();
131 }
132
133 last_upper = ch.is_uppercase();
134 buf.extend(change_case(ch));
135 }
136
137 words.push(buf);
138 }
139
140 words.join("_")
141}
142
143#[must_use]
145pub fn to_camel_case(ident: &str) -> String {
146 ident
147 .trim_matches('_')
148 .split('_')
149 .filter(|component| !component.is_empty())
150 .map(|component| {
151 let mut camel_cased_component = String::with_capacity(component.len());
152
153 let mut new_word = true;
154 let mut prev_is_lower_case = true;
155
156 for c in component.chars() {
157 if prev_is_lower_case && c.is_uppercase() {
160 new_word = true;
161 }
162
163 if new_word {
164 camel_cased_component.extend(c.to_uppercase());
165 } else {
166 camel_cased_component.extend(c.to_lowercase());
167 }
168
169 prev_is_lower_case = c.is_lowercase();
170 new_word = false;
171 }
172
173 camel_cased_component
174 })
175 .fold((String::new(), None), |(mut acc, prev): (_, Option<String>), next| {
176 let join = prev
179 .and_then(|prev| {
180 let f = next.chars().next()?;
181 let l = prev.chars().last()?;
182 Some(!char_has_case(l) && !char_has_case(f))
183 })
184 .unwrap_or(false);
185 acc.push_str(if join { "_" } else { "" });
186 acc.push_str(&next);
187 (acc, Some(next))
188 })
189 .0
190}
191
192#[must_use]
194pub const fn char_has_case(c: char) -> bool {
195 c.is_lowercase() || c.is_uppercase()
196}
197
198#[must_use]
199pub fn is_upper_snake_case(s: &str) -> bool {
200 s.chars().all(|c| c.is_uppercase() || c == '_' || c.is_numeric())
201}
202
203pub fn replace(buf: &mut String, from: char, to: &str) {
204 let replace_count = buf.chars().filter(|&ch| ch == from).count();
205 if replace_count == 0 {
206 return;
207 }
208 let from_len = from.len_utf8();
209 let additional = to.len().saturating_sub(from_len);
210 buf.reserve(additional * replace_count);
211
212 let mut end = buf.len();
213 while let Some(i) = buf[..end].rfind(from) {
214 buf.replace_range(i..i + from_len, to);
215 end = i;
216 }
217}
218
219#[must_use]
220pub fn trim_indent(mut text: &str) -> String {
221 if text.starts_with('\n') {
222 text = &text[1..];
223 }
224 let indent = text
225 .lines()
226 .filter(|it| !it.trim().is_empty())
227 .map(|it| it.len() - it.trim_start().len())
228 .min()
229 .unwrap_or(0);
230 text.split_inclusive('\n')
231 .map(
232 |line| {
233 if line.len() <= indent { line.trim_start_matches(' ') } else { &line[indent..] }
234 },
235 )
236 .collect()
237}
238
239pub fn equal_range_by<T, F>(slice: &[T], mut key: F) -> ops::Range<usize>
240where
241 F: FnMut(&T) -> Ordering,
242{
243 let start = slice.partition_point(|it| key(it) == Ordering::Less);
244 let len = slice[start..].partition_point(|it| key(it) == Ordering::Equal);
245 start..start + len
246}
247
248#[must_use]
249pub fn defer<F: FnOnce()>(f: F) -> impl Drop {
250 struct D<F: FnOnce()>(Option<F>);
251 impl<F: FnOnce()> Drop for D<F> {
252 fn drop(&mut self) {
253 if let Some(f) = self.0.take() {
254 f();
255 }
256 }
257 }
258 D(Some(f))
259}
260
261#[cfg_attr(not(target_arch = "wasm32"), repr(transparent))]
263#[derive(Debug)]
264pub struct JodChild(pub std::process::Child);
265
266impl ops::Deref for JodChild {
267 type Target = std::process::Child;
268 fn deref(&self) -> &std::process::Child {
269 &self.0
270 }
271}
272
273impl ops::DerefMut for JodChild {
274 fn deref_mut(&mut self) -> &mut std::process::Child {
275 &mut self.0
276 }
277}
278
279impl Drop for JodChild {
280 fn drop(&mut self) {
281 _ = self.0.kill();
282 _ = self.0.wait();
283 }
284}
285
286impl JodChild {
287 pub fn spawn(mut command: Command) -> sio::Result<Self> {
288 command.spawn().map(Self)
289 }
290
291 #[must_use]
292 #[cfg(not(target_arch = "wasm32"))]
293 pub fn into_inner(self) -> std::process::Child {
294 unsafe { std::mem::transmute::<Self, std::process::Child>(self) }
296 }
297}
298
299pub fn iter_eq_by<I, I2, F>(this: I2, other: I, mut eq: F) -> bool
302where
303 I: IntoIterator,
304 I2: IntoIterator,
305 F: FnMut(I2::Item, I::Item) -> bool,
306{
307 let mut other = other.into_iter();
308 let mut this = this.into_iter();
309
310 loop {
311 let x = match this.next() {
312 None => return other.next().is_none(),
313 Some(val) => val,
314 };
315
316 let y = match other.next() {
317 None => return false,
318 Some(val) => val,
319 };
320
321 if !eq(x, y) {
322 return false;
323 }
324 }
325}
326
327pub fn slice_tails<T>(this: &[T]) -> impl Iterator<Item = &[T]> {
329 (0..this.len()).map(|i| &this[i..])
330}
331
332#[cfg(test)]
333mod tests {
334 use super::*;
335
336 #[test]
337 fn test_trim_indent() {
338 assert_eq!(trim_indent(""), "");
339 assert_eq!(
340 trim_indent(
341 "
342 hello
343 world
344"
345 ),
346 "hello\nworld\n"
347 );
348 assert_eq!(
349 trim_indent(
350 "
351 hello
352 world"
353 ),
354 "hello\nworld"
355 );
356 assert_eq!(trim_indent(" hello\n world\n"), "hello\nworld\n");
357 assert_eq!(
358 trim_indent(
359 "
360 fn main() {
361 return 92;
362 }
363 "
364 ),
365 "fn main() {\n return 92;\n}\n"
366 );
367 }
368
369 #[test]
370 fn test_replace() {
371 #[track_caller]
372 fn test_replace(src: &str, from: char, to: &str, expected: &str) {
373 let mut s = src.to_owned();
374 replace(&mut s, from, to);
375 assert_eq!(s, expected, "from: {from:?}, to: {to:?}");
376 }
377
378 test_replace("", 'a', "b", "");
379 test_replace("", 'a', "😀", "");
380 test_replace("", '😀', "a", "");
381 test_replace("a", 'a', "b", "b");
382 test_replace("aa", 'a', "b", "bb");
383 test_replace("ada", 'a', "b", "bdb");
384 test_replace("a", 'a', "😀", "😀");
385 test_replace("😀", '😀', "a", "a");
386 test_replace("😀x", '😀', "a", "ax");
387 test_replace("y😀x", '😀', "a", "yax");
388 test_replace("a,b,c", ',', ".", "a.b.c");
389 test_replace("a,b,c", ',', "..", "a..b..c");
390 test_replace("a.b.c", '.', "..", "a..b..c");
391 test_replace("a.b.c", '.', "..", "a..b..c");
392 test_replace("a😀b😀c", '😀', ".", "a.b.c");
393 test_replace("a.b.c", '.', "😀", "a😀b😀c");
394 test_replace("a.b.c", '.', "😀😀", "a😀😀b😀😀c");
395 test_replace(".a.b.c.", '.', "()", "()a()b()c()");
396 test_replace(".a.b.c.", '.', "", "abc");
397 }
398}