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 if !buf.contains(from) {
191 return;
192 }
193 *buf = buf.replace(from, to);
195}
196
197#[must_use]
198pub fn trim_indent(mut text: &str) -> String {
199 if text.starts_with('\n') {
200 text = &text[1..];
201 }
202 let indent = text
203 .lines()
204 .filter(|it| !it.trim().is_empty())
205 .map(|it| it.len() - it.trim_start().len())
206 .min()
207 .unwrap_or(0);
208 text.split_inclusive('\n')
209 .map(
210 |line| {
211 if line.len() <= indent { line.trim_start_matches(' ') } else { &line[indent..] }
212 },
213 )
214 .collect()
215}
216
217pub fn equal_range_by<T, F>(slice: &[T], mut key: F) -> ops::Range<usize>
218where
219 F: FnMut(&T) -> Ordering,
220{
221 let start = slice.partition_point(|it| key(it) == Ordering::Less);
222 let len = slice[start..].partition_point(|it| key(it) == Ordering::Equal);
223 start..start + len
224}
225
226#[must_use]
227pub fn defer<F: FnOnce()>(f: F) -> impl Drop {
228 struct D<F: FnOnce()>(Option<F>);
229 impl<F: FnOnce()> Drop for D<F> {
230 fn drop(&mut self) {
231 if let Some(f) = self.0.take() {
232 f();
233 }
234 }
235 }
236 D(Some(f))
237}
238
239#[cfg_attr(not(target_arch = "wasm32"), repr(transparent))]
241#[derive(Debug)]
242pub struct JodChild(pub std::process::Child);
243
244impl ops::Deref for JodChild {
245 type Target = std::process::Child;
246 fn deref(&self) -> &std::process::Child {
247 &self.0
248 }
249}
250
251impl ops::DerefMut for JodChild {
252 fn deref_mut(&mut self) -> &mut std::process::Child {
253 &mut self.0
254 }
255}
256
257impl Drop for JodChild {
258 fn drop(&mut self) {
259 _ = self.0.kill();
260 _ = self.0.wait();
261 }
262}
263
264impl JodChild {
265 pub fn spawn(mut command: Command) -> sio::Result<Self> {
266 command.spawn().map(Self)
267 }
268
269 #[must_use]
270 #[cfg(not(target_arch = "wasm32"))]
271 pub fn into_inner(self) -> std::process::Child {
272 unsafe { std::mem::transmute::<Self, std::process::Child>(self) }
274 }
275}
276
277pub fn iter_eq_by<I, I2, F>(this: I2, other: I, mut eq: F) -> bool
280where
281 I: IntoIterator,
282 I2: IntoIterator,
283 F: FnMut(I2::Item, I::Item) -> bool,
284{
285 let mut other = other.into_iter();
286 let mut this = this.into_iter();
287
288 loop {
289 let x = match this.next() {
290 None => return other.next().is_none(),
291 Some(val) => val,
292 };
293
294 let y = match other.next() {
295 None => return false,
296 Some(val) => val,
297 };
298
299 if !eq(x, y) {
300 return false;
301 }
302 }
303}
304
305pub fn slice_tails<T>(this: &[T]) -> impl Iterator<Item = &[T]> {
307 (0..this.len()).map(|i| &this[i..])
308}
309
310#[cfg(test)]
311mod tests {
312 use super::*;
313
314 #[test]
315 fn test_trim_indent() {
316 assert_eq!(trim_indent(""), "");
317 assert_eq!(
318 trim_indent(
319 "
320 hello
321 world
322"
323 ),
324 "hello\nworld\n"
325 );
326 assert_eq!(
327 trim_indent(
328 "
329 hello
330 world"
331 ),
332 "hello\nworld"
333 );
334 assert_eq!(trim_indent(" hello\n world\n"), "hello\nworld\n");
335 assert_eq!(
336 trim_indent(
337 "
338 fn main() {
339 return 92;
340 }
341 "
342 ),
343 "fn main() {\n return 92;\n}\n"
344 );
345 }
346}