Skip to main content

ra_ap_stdx/
lib.rs

1//! Missing batteries for standard libraries.
2
3use std::borrow::Cow;
4use std::io as sio;
5use std::process::Command;
6use std::{cmp::Ordering, ops, time::Instant};
7
8mod macros;
9
10pub mod anymap;
11pub mod assert;
12pub mod non_empty_vec;
13pub mod panic_context;
14pub mod process;
15pub mod rand;
16pub mod thread;
17pub mod variance;
18
19pub use itertools;
20
21#[inline(always)]
22pub const fn is_ci() -> bool {
23    option_env!("CI").is_some()
24}
25
26pub fn hash_once<Hasher: std::hash::Hasher + Default>(thing: impl std::hash::Hash) -> u64 {
27    std::hash::BuildHasher::hash_one(&std::hash::BuildHasherDefault::<Hasher>::default(), thing)
28}
29
30#[must_use]
31#[expect(clippy::print_stderr, reason = "only visible to developers")]
32pub fn timeit(label: &'static str) -> impl Drop {
33    let start = Instant::now();
34    defer(move || eprintln!("{}: {:.2}", label, start.elapsed().as_nanos()))
35}
36
37/// Prints backtrace to stderr, useful for debugging.
38#[expect(clippy::print_stderr, reason = "only visible to developers")]
39pub fn print_backtrace() {
40    #[cfg(feature = "backtrace")]
41    eprintln!("{:?}", backtrace::Backtrace::new());
42
43    #[cfg(not(feature = "backtrace"))]
44    eprintln!(
45        r#"Enable the backtrace feature.
46Uncomment `default = [ "backtrace" ]` in `crates/stdx/Cargo.toml`.
47"#
48    );
49}
50
51pub trait TupleExt {
52    type Head;
53    type Tail;
54    fn head(self) -> Self::Head;
55    fn tail(self) -> Self::Tail;
56}
57
58impl<T, U> TupleExt for (T, U) {
59    type Head = T;
60    type Tail = U;
61    fn head(self) -> Self::Head {
62        self.0
63    }
64    fn tail(self) -> Self::Tail {
65        self.1
66    }
67}
68
69impl<T, U, V> TupleExt for (T, U, V) {
70    type Head = T;
71    type Tail = V;
72    fn head(self) -> Self::Head {
73        self.0
74    }
75    fn tail(self) -> Self::Tail {
76        self.2
77    }
78}
79
80impl<T> TupleExt for &T
81where
82    T: TupleExt + Copy,
83{
84    type Head = T::Head;
85    type Tail = T::Tail;
86    fn head(self) -> Self::Head {
87        (*self).head()
88    }
89    fn tail(self) -> Self::Tail {
90        (*self).tail()
91    }
92}
93
94pub fn to_lower_snake_case(s: &str) -> String {
95    to_snake_case(s, char::to_lowercase)
96}
97pub fn to_upper_snake_case(s: &str) -> String {
98    to_snake_case(s, char::to_uppercase)
99}
100
101// Code partially taken from rust/compiler/rustc_lint/src/nonstandard_style.rs
102// commit: 9626f2b
103fn to_snake_case<F, I>(mut s: &str, change_case: F) -> String
104where
105    F: Fn(char) -> I,
106    I: Iterator<Item = char>,
107{
108    let mut words = vec![];
109
110    // Preserve leading underscores
111    s = s.trim_start_matches(|c: char| {
112        if c == '_' {
113            words.push(String::new());
114            true
115        } else {
116            false
117        }
118    });
119
120    for s in s.split('_') {
121        let mut last_upper = false;
122        let mut buf = String::new();
123
124        if s.is_empty() {
125            continue;
126        }
127
128        for ch in s.chars() {
129            if !buf.is_empty() && buf != "'" && ch.is_uppercase() && !last_upper {
130                words.push(buf);
131                buf = String::new();
132            }
133
134            last_upper = ch.is_uppercase();
135            buf.extend(change_case(ch));
136        }
137
138        words.push(buf);
139    }
140
141    words.join("_")
142}
143
144// Taken from rustc.
145#[must_use]
146pub fn to_camel_case(ident: &str) -> String {
147    ident
148        .trim_matches('_')
149        .split('_')
150        .filter(|component| !component.is_empty())
151        .map(|component| {
152            let mut camel_cased_component = String::with_capacity(component.len());
153
154            let mut new_word = true;
155            let mut prev_is_lower_case = true;
156
157            for c in component.chars() {
158                // Preserve the case if an uppercase letter follows a lowercase letter, so that
159                // `camelCase` is converted to `CamelCase`.
160                if prev_is_lower_case && c.is_uppercase() {
161                    new_word = true;
162                }
163
164                if new_word {
165                    camel_cased_component.extend(c.to_uppercase());
166                } else {
167                    camel_cased_component.extend(c.to_lowercase());
168                }
169
170                prev_is_lower_case = c.is_lowercase();
171                new_word = false;
172            }
173
174            camel_cased_component
175        })
176        .fold((String::new(), None), |(mut acc, prev): (_, Option<String>), next| {
177            // separate two components with an underscore if their boundary cannot
178            // be distinguished using an uppercase/lowercase case distinction
179            let join = prev
180                .and_then(|prev| {
181                    let f = next.chars().next()?;
182                    let l = prev.chars().last()?;
183                    Some(!char_has_case(l) && !char_has_case(f))
184                })
185                .unwrap_or(false);
186            acc.push_str(if join { "_" } else { "" });
187            acc.push_str(&next);
188            (acc, Some(next))
189        })
190        .0
191}
192
193// Taken from rustc.
194#[must_use]
195pub const fn char_has_case(c: char) -> bool {
196    c.is_lowercase() || c.is_uppercase()
197}
198
199#[must_use]
200pub fn is_upper_snake_case(s: &str) -> bool {
201    s.chars().all(|c| c.is_uppercase() || c == '_' || c.is_numeric())
202}
203
204pub fn replace(buf: &mut String, from: char, to: &str) {
205    let replace_count = buf.chars().filter(|&ch| ch == from).count();
206    if replace_count == 0 {
207        return;
208    }
209    let from_len = from.len_utf8();
210    let additional = to.len().saturating_sub(from_len);
211    buf.reserve(additional * replace_count);
212
213    let mut end = buf.len();
214    while let Some(i) = buf[..end].rfind(from) {
215        buf.replace_range(i..i + from_len, to);
216        end = i;
217    }
218}
219
220#[must_use]
221pub fn trim_indent(mut text: &str) -> String {
222    if text.starts_with('\n') {
223        text = &text[1..];
224    }
225    let indent = indent_of(text);
226    text.split_inclusive('\n')
227        .map(
228            |line| {
229                if line.len() <= indent { line.trim_start_matches(' ') } else { &line[indent..] }
230            },
231        )
232        .collect()
233}
234
235#[must_use]
236fn indent_of(text: &str) -> usize {
237    text.lines()
238        .filter(|it| !it.trim().is_empty())
239        .map(|it| it.len() - it.trim_start().len())
240        .min()
241        .unwrap_or(0)
242}
243
244#[must_use]
245pub fn dedent_by(spaces: usize, text: &str) -> String {
246    text.split_inclusive('\n')
247        .map(|line| {
248            let trimmed = line.trim_start_matches(' ');
249            if line.len() - trimmed.len() <= spaces { trimmed } else { &line[spaces..] }
250        })
251        .collect()
252}
253
254/// Indent non empty lines, including the first line
255#[must_use]
256pub fn indent_string(s: &str, indent_level: u8) -> String {
257    if indent_level == 0 || s.is_empty() {
258        return s.to_owned();
259    }
260    let indent_str = "    ".repeat(indent_level as usize);
261    s.split_inclusive("\n")
262        .map(|line| {
263            if line.trim_end().is_empty() {
264                Cow::Borrowed(line)
265            } else {
266                format!("{indent_str}{line}").into()
267            }
268        })
269        .collect()
270}
271
272pub fn equal_range_by<T, F>(slice: &[T], mut key: F) -> ops::Range<usize>
273where
274    F: FnMut(&T) -> Ordering,
275{
276    let start = slice.partition_point(|it| key(it) == Ordering::Less);
277    let len = slice[start..].partition_point(|it| key(it) == Ordering::Equal);
278    start..start + len
279}
280
281#[must_use]
282pub fn defer<F: FnOnce()>(f: F) -> impl Drop {
283    struct D<F: FnOnce()>(Option<F>);
284    impl<F: FnOnce()> Drop for D<F> {
285        fn drop(&mut self) {
286            if let Some(f) = self.0.take() {
287                f();
288            }
289        }
290    }
291    D(Some(f))
292}
293
294/// A [`std::process::Child`] wrapper that will kill the child on drop.
295#[cfg_attr(not(target_arch = "wasm32"), repr(transparent))]
296#[derive(Debug)]
297pub struct JodChild(pub std::process::Child);
298
299impl ops::Deref for JodChild {
300    type Target = std::process::Child;
301    fn deref(&self) -> &std::process::Child {
302        &self.0
303    }
304}
305
306impl ops::DerefMut for JodChild {
307    fn deref_mut(&mut self) -> &mut std::process::Child {
308        &mut self.0
309    }
310}
311
312impl Drop for JodChild {
313    fn drop(&mut self) {
314        _ = self.0.kill();
315        _ = self.0.wait();
316    }
317}
318
319impl JodChild {
320    pub fn spawn(mut command: Command) -> sio::Result<Self> {
321        command.spawn().map(Self)
322    }
323
324    #[must_use]
325    #[cfg(not(target_arch = "wasm32"))]
326    pub fn into_inner(self) -> std::process::Child {
327        // SAFETY: repr transparent, except on WASM
328        unsafe { std::mem::transmute::<Self, std::process::Child>(self) }
329    }
330}
331
332// feature: iter_order_by
333// Iterator::eq_by
334pub fn iter_eq_by<I, I2, F>(this: I2, other: I, mut eq: F) -> bool
335where
336    I: IntoIterator,
337    I2: IntoIterator,
338    F: FnMut(I2::Item, I::Item) -> bool,
339{
340    let mut other = other.into_iter();
341    let mut this = this.into_iter();
342
343    loop {
344        let x = match this.next() {
345            None => return other.next().is_none(),
346            Some(val) => val,
347        };
348
349        let y = match other.next() {
350            None => return false,
351            Some(val) => val,
352        };
353
354        if !eq(x, y) {
355            return false;
356        }
357    }
358}
359
360/// Returns all final segments of the argument, longest first.
361pub fn slice_tails<T>(this: &[T]) -> impl Iterator<Item = &[T]> {
362    (0..this.len()).map(|i| &this[i..])
363}
364
365#[cfg(test)]
366mod tests {
367    use super::*;
368
369    #[test]
370    fn test_trim_indent() {
371        assert_eq!(trim_indent(""), "");
372        assert_eq!(
373            trim_indent(
374                "
375            hello
376            world
377"
378            ),
379            "hello\nworld\n"
380        );
381        assert_eq!(
382            trim_indent(
383                "
384            hello
385            world"
386            ),
387            "hello\nworld"
388        );
389        assert_eq!(trim_indent("    hello\n    world\n"), "hello\nworld\n");
390        assert_eq!(
391            trim_indent(
392                "
393            fn main() {
394                return 92;
395            }
396        "
397            ),
398            "fn main() {\n    return 92;\n}\n"
399        );
400    }
401
402    #[test]
403    fn test_dedent() {
404        assert_eq!(dedent_by(0, ""), "");
405        assert_eq!(dedent_by(1, ""), "");
406        assert_eq!(dedent_by(2, ""), "");
407        assert_eq!(dedent_by(0, "foo"), "foo");
408        assert_eq!(dedent_by(2, "foo"), "foo");
409        assert_eq!(dedent_by(2, "  foo"), "foo");
410        assert_eq!(dedent_by(2, "    foo"), "  foo");
411        assert_eq!(dedent_by(2, "    foo\nbar"), "  foo\nbar");
412        assert_eq!(dedent_by(2, "foo\n    bar"), "foo\n  bar");
413        assert_eq!(dedent_by(2, "foo\n\n    bar"), "foo\n\n  bar");
414        assert_eq!(dedent_by(2, "foo\n.\n    bar"), "foo\n.\n  bar");
415        assert_eq!(dedent_by(2, "foo\n .\n    bar"), "foo\n.\n  bar");
416        assert_eq!(dedent_by(2, "foo\n   .\n    bar"), "foo\n .\n  bar");
417    }
418
419    #[test]
420    fn test_indent_of() {
421        assert_eq!(indent_of(""), 0);
422        assert_eq!(indent_of(" "), 0);
423        assert_eq!(indent_of(" x"), 1);
424        assert_eq!(indent_of(" x\n"), 1);
425        assert_eq!(indent_of(" x\ny"), 0);
426        assert_eq!(indent_of(" x\n y"), 1);
427        assert_eq!(indent_of(" x\n  y"), 1);
428        assert_eq!(indent_of("  x\n  y"), 2);
429        assert_eq!(indent_of("  x\n  y\n"), 2);
430        assert_eq!(indent_of("  x\n\n  y\n"), 2);
431    }
432
433    #[test]
434    fn test_replace() {
435        #[track_caller]
436        fn test_replace(src: &str, from: char, to: &str, expected: &str) {
437            let mut s = src.to_owned();
438            replace(&mut s, from, to);
439            assert_eq!(s, expected, "from: {from:?}, to: {to:?}");
440        }
441
442        test_replace("", 'a', "b", "");
443        test_replace("", 'a', "😀", "");
444        test_replace("", '😀', "a", "");
445        test_replace("a", 'a', "b", "b");
446        test_replace("aa", 'a', "b", "bb");
447        test_replace("ada", 'a', "b", "bdb");
448        test_replace("a", 'a', "😀", "😀");
449        test_replace("😀", '😀', "a", "a");
450        test_replace("😀x", '😀', "a", "ax");
451        test_replace("y😀x", '😀', "a", "yax");
452        test_replace("a,b,c", ',', ".", "a.b.c");
453        test_replace("a,b,c", ',', "..", "a..b..c");
454        test_replace("a.b.c", '.', "..", "a..b..c");
455        test_replace("a.b.c", '.', "..", "a..b..c");
456        test_replace("a😀b😀c", '😀', ".", "a.b.c");
457        test_replace("a.b.c", '.', "😀", "a😀b😀c");
458        test_replace("a.b.c", '.', "😀😀", "a😀😀b😀😀c");
459        test_replace(".a.b.c.", '.', "()", "()a()b()c()");
460        test_replace(".a.b.c.", '.', "", "abc");
461    }
462}