stdx/
lib.rs

1//! Missing batteries for standard libraries.
2
3use 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/// Prints backtrace to stderr, useful for debugging.
37#[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
86// Code partially taken from rust/compiler/rustc_lint/src/nonstandard_style.rs
87// commit: 9626f2b
88fn 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    // Preserve leading underscores
96    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// Taken from rustc.
130#[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                // Preserve the case if an uppercase letter follows a lowercase letter, so that
144                // `camelCase` is converted to `CamelCase`.
145                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            // separate two components with an underscore if their boundary cannot
163            // be distinguished using an uppercase/lowercase case distinction
164            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// Taken from rustc.
179#[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/// A [`std::process::Child`] wrapper that will kill the child on drop.
248#[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        // SAFETY: repr transparent, except on WASM
281        unsafe { std::mem::transmute::<Self, std::process::Child>(self) }
282    }
283}
284
285// feature: iter_order_by
286// Iterator::eq_by
287pub 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
313/// Returns all final segments of the argument, longest first.
314pub 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}