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    if !buf.contains(from) {
191        return;
192    }
193    // FIXME: do this in place.
194    *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/// A [`std::process::Child`] wrapper that will kill the child on drop.
240#[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        // SAFETY: repr transparent, except on WASM
273        unsafe { std::mem::transmute::<Self, std::process::Child>(self) }
274    }
275}
276
277// feature: iter_order_by
278// Iterator::eq_by
279pub 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
305/// Returns all final segments of the argument, longest first.
306pub 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}