ra_ap_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;
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/// Prints backtrace to stderr, useful for debugging.
36#[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
85// Code partially taken from rust/compiler/rustc_lint/src/nonstandard_style.rs
86// commit: 9626f2b
87fn 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    // Preserve leading underscores
95    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// Taken from rustc.
129#[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                // Preserve the case if an uppercase letter follows a lowercase letter, so that
143                // `camelCase` is converted to `CamelCase`.
144                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            // separate two components with an underscore if their boundary cannot
162            // be distinguished using an uppercase/lowercase case distinction
163            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// Taken from rustc.
178#[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    // FIXME: do this in place.
193    *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/// A [`std::process::Child`] wrapper that will kill the child on drop.
239#[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        // SAFETY: repr transparent, except on WASM
272        unsafe { std::mem::transmute::<Self, std::process::Child>(self) }
273    }
274}
275
276// feature: iter_order_by
277// Iterator::eq_by
278pub 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
304/// Returns all final segments of the argument, longest first.
305pub 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}