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
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
100// Code partially taken from rust/compiler/rustc_lint/src/nonstandard_style.rs
101// commit: 9626f2b
102fn 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    // Preserve leading underscores
110    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// Taken from rustc.
144#[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                // Preserve the case if an uppercase letter follows a lowercase letter, so that
158                // `camelCase` is converted to `CamelCase`.
159                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            // separate two components with an underscore if their boundary cannot
177            // be distinguished using an uppercase/lowercase case distinction
178            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// Taken from rustc.
193#[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/// A [`std::process::Child`] wrapper that will kill the child on drop.
262#[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        // SAFETY: repr transparent, except on WASM
295        unsafe { std::mem::transmute::<Self, std::process::Child>(self) }
296    }
297}
298
299// feature: iter_order_by
300// Iterator::eq_by
301pub 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
327/// Returns all final segments of the argument, longest first.
328pub 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}