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