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
79pub fn to_lower_snake_case(s: &str) -> String {
80 to_snake_case(s, char::to_lowercase)
81}
82pub fn to_upper_snake_case(s: &str) -> String {
83 to_snake_case(s, char::to_uppercase)
84}
85
86fn to_snake_case<F, I>(mut s: &str, change_case: F) -> String
89where
90 F: Fn(char) -> I,
91 I: Iterator<Item = char>,
92{
93 let mut words = vec![];
94
95 s = s.trim_start_matches(|c: char| {
97 if c == '_' {
98 words.push(String::new());
99 true
100 } else {
101 false
102 }
103 });
104
105 for s in s.split('_') {
106 let mut last_upper = false;
107 let mut buf = String::new();
108
109 if s.is_empty() {
110 continue;
111 }
112
113 for ch in s.chars() {
114 if !buf.is_empty() && buf != "'" && ch.is_uppercase() && !last_upper {
115 words.push(buf);
116 buf = String::new();
117 }
118
119 last_upper = ch.is_uppercase();
120 buf.extend(change_case(ch));
121 }
122
123 words.push(buf);
124 }
125
126 words.join("_")
127}
128
129#[must_use]
131pub fn to_camel_case(ident: &str) -> String {
132 ident
133 .trim_matches('_')
134 .split('_')
135 .filter(|component| !component.is_empty())
136 .map(|component| {
137 let mut camel_cased_component = String::with_capacity(component.len());
138
139 let mut new_word = true;
140 let mut prev_is_lower_case = true;
141
142 for c in component.chars() {
143 if prev_is_lower_case && c.is_uppercase() {
146 new_word = true;
147 }
148
149 if new_word {
150 camel_cased_component.extend(c.to_uppercase());
151 } else {
152 camel_cased_component.extend(c.to_lowercase());
153 }
154
155 prev_is_lower_case = c.is_lowercase();
156 new_word = false;
157 }
158
159 camel_cased_component
160 })
161 .fold((String::new(), None), |(mut acc, prev): (_, Option<String>), next| {
162 let join = prev
165 .and_then(|prev| {
166 let f = next.chars().next()?;
167 let l = prev.chars().last()?;
168 Some(!char_has_case(l) && !char_has_case(f))
169 })
170 .unwrap_or(false);
171 acc.push_str(if join { "_" } else { "" });
172 acc.push_str(&next);
173 (acc, Some(next))
174 })
175 .0
176}
177
178#[must_use]
180pub const fn char_has_case(c: char) -> bool {
181 c.is_lowercase() || c.is_uppercase()
182}
183
184#[must_use]
185pub fn is_upper_snake_case(s: &str) -> bool {
186 s.chars().all(|c| c.is_uppercase() || c == '_' || c.is_numeric())
187}
188
189pub fn replace(buf: &mut String, from: char, to: &str) {
190 let replace_count = buf.chars().filter(|&ch| ch == from).count();
191 if replace_count == 0 {
192 return;
193 }
194 let from_len = from.len_utf8();
195 let additional = to.len().saturating_sub(from_len);
196 buf.reserve(additional * replace_count);
197
198 let mut end = buf.len();
199 while let Some(i) = buf[..end].rfind(from) {
200 buf.replace_range(i..i + from_len, to);
201 end = i;
202 }
203}
204
205#[must_use]
206pub fn trim_indent(mut text: &str) -> String {
207 if text.starts_with('\n') {
208 text = &text[1..];
209 }
210 let indent = text
211 .lines()
212 .filter(|it| !it.trim().is_empty())
213 .map(|it| it.len() - it.trim_start().len())
214 .min()
215 .unwrap_or(0);
216 text.split_inclusive('\n')
217 .map(
218 |line| {
219 if line.len() <= indent { line.trim_start_matches(' ') } else { &line[indent..] }
220 },
221 )
222 .collect()
223}
224
225pub fn equal_range_by<T, F>(slice: &[T], mut key: F) -> ops::Range<usize>
226where
227 F: FnMut(&T) -> Ordering,
228{
229 let start = slice.partition_point(|it| key(it) == Ordering::Less);
230 let len = slice[start..].partition_point(|it| key(it) == Ordering::Equal);
231 start..start + len
232}
233
234#[must_use]
235pub fn defer<F: FnOnce()>(f: F) -> impl Drop {
236 struct D<F: FnOnce()>(Option<F>);
237 impl<F: FnOnce()> Drop for D<F> {
238 fn drop(&mut self) {
239 if let Some(f) = self.0.take() {
240 f();
241 }
242 }
243 }
244 D(Some(f))
245}
246
247#[cfg_attr(not(target_arch = "wasm32"), repr(transparent))]
249#[derive(Debug)]
250pub struct JodChild(pub std::process::Child);
251
252impl ops::Deref for JodChild {
253 type Target = std::process::Child;
254 fn deref(&self) -> &std::process::Child {
255 &self.0
256 }
257}
258
259impl ops::DerefMut for JodChild {
260 fn deref_mut(&mut self) -> &mut std::process::Child {
261 &mut self.0
262 }
263}
264
265impl Drop for JodChild {
266 fn drop(&mut self) {
267 _ = self.0.kill();
268 _ = self.0.wait();
269 }
270}
271
272impl JodChild {
273 pub fn spawn(mut command: Command) -> sio::Result<Self> {
274 command.spawn().map(Self)
275 }
276
277 #[must_use]
278 #[cfg(not(target_arch = "wasm32"))]
279 pub fn into_inner(self) -> std::process::Child {
280 unsafe { std::mem::transmute::<Self, std::process::Child>(self) }
282 }
283}
284
285pub fn iter_eq_by<I, I2, F>(this: I2, other: I, mut eq: F) -> bool
288where
289 I: IntoIterator,
290 I2: IntoIterator,
291 F: FnMut(I2::Item, I::Item) -> bool,
292{
293 let mut other = other.into_iter();
294 let mut this = this.into_iter();
295
296 loop {
297 let x = match this.next() {
298 None => return other.next().is_none(),
299 Some(val) => val,
300 };
301
302 let y = match other.next() {
303 None => return false,
304 Some(val) => val,
305 };
306
307 if !eq(x, y) {
308 return false;
309 }
310 }
311}
312
313pub fn slice_tails<T>(this: &[T]) -> impl Iterator<Item = &[T]> {
315 (0..this.len()).map(|i| &this[i..])
316}
317
318#[cfg(test)]
319mod tests {
320 use super::*;
321
322 #[test]
323 fn test_trim_indent() {
324 assert_eq!(trim_indent(""), "");
325 assert_eq!(
326 trim_indent(
327 "
328 hello
329 world
330"
331 ),
332 "hello\nworld\n"
333 );
334 assert_eq!(
335 trim_indent(
336 "
337 hello
338 world"
339 ),
340 "hello\nworld"
341 );
342 assert_eq!(trim_indent(" hello\n world\n"), "hello\nworld\n");
343 assert_eq!(
344 trim_indent(
345 "
346 fn main() {
347 return 92;
348 }
349 "
350 ),
351 "fn main() {\n return 92;\n}\n"
352 );
353 }
354
355 #[test]
356 fn test_replace() {
357 #[track_caller]
358 fn test_replace(src: &str, from: char, to: &str, expected: &str) {
359 let mut s = src.to_owned();
360 replace(&mut s, from, to);
361 assert_eq!(s, expected, "from: {from:?}, to: {to:?}");
362 }
363
364 test_replace("", 'a', "b", "");
365 test_replace("", 'a', "😀", "");
366 test_replace("", '😀', "a", "");
367 test_replace("a", 'a', "b", "b");
368 test_replace("aa", 'a', "b", "bb");
369 test_replace("ada", 'a', "b", "bdb");
370 test_replace("a", 'a', "😀", "😀");
371 test_replace("😀", '😀', "a", "a");
372 test_replace("😀x", '😀', "a", "ax");
373 test_replace("y😀x", '😀', "a", "yax");
374 test_replace("a,b,c", ',', ".", "a.b.c");
375 test_replace("a,b,c", ',', "..", "a..b..c");
376 test_replace("a.b.c", '.', "..", "a..b..c");
377 test_replace("a.b.c", '.', "..", "a..b..c");
378 test_replace("a😀b😀c", '😀', ".", "a.b.c");
379 test_replace("a.b.c", '.', "😀", "a😀b😀c");
380 test_replace("a.b.c", '.', "😀😀", "a😀😀b😀😀c");
381 test_replace(".a.b.c.", '.', "()", "()a()b()c()");
382 test_replace(".a.b.c.", '.', "", "abc");
383 }
384}