//! Missing batteries for standard libraries. #![warn(rust_2018_idioms, unused_lifetimes)] use std::io as sio; use std::process::Command; use std::{cmp::Ordering, ops, time::Instant}; pub mod anymap; mod macros; pub mod non_empty_vec; pub mod panic_context; pub mod process; pub mod rand; pub mod thread; pub use always_assert::{always, never}; pub use itertools; #[inline(always)] pub fn is_ci() -> bool { option_env!("CI").is_some() } pub fn hash_once(thing: impl std::hash::Hash) -> u64 { std::hash::BuildHasher::hash_one(&std::hash::BuildHasherDefault::::default(), thing) } #[must_use] #[allow(clippy::print_stderr)] pub fn timeit(label: &'static str) -> impl Drop { let start = Instant::now(); defer(move || eprintln!("{}: {:.2?}", label, start.elapsed())) } /// Prints backtrace to stderr, useful for debugging. #[allow(clippy::print_stderr)] pub fn print_backtrace() { #[cfg(feature = "backtrace")] eprintln!("{:?}", backtrace::Backtrace::new()); #[cfg(not(feature = "backtrace"))] eprintln!( r#"Enable the backtrace feature. Uncomment `default = [ "backtrace" ]` in `crates/stdx/Cargo.toml`. "# ); } pub trait TupleExt { type Head; type Tail; fn head(self) -> Self::Head; fn tail(self) -> Self::Tail; } impl TupleExt for (T, U) { type Head = T; type Tail = U; fn head(self) -> Self::Head { self.0 } fn tail(self) -> Self::Tail { self.1 } } impl TupleExt for (T, U, V) { type Head = T; type Tail = V; fn head(self) -> Self::Head { self.0 } fn tail(self) -> Self::Tail { self.2 } } pub fn to_lower_snake_case(s: &str) -> String { to_snake_case(s, char::to_lowercase) } pub fn to_upper_snake_case(s: &str) -> String { to_snake_case(s, char::to_uppercase) } // Code partially taken from rust/compiler/rustc_lint/src/nonstandard_style.rs // commit: 9626f2b fn to_snake_case(mut s: &str, change_case: F) -> String where F: Fn(char) -> I, I: Iterator, { let mut words = vec![]; // Preserve leading underscores s = s.trim_start_matches(|c: char| { if c == '_' { words.push(String::new()); true } else { false } }); for s in s.split('_') { let mut last_upper = false; let mut buf = String::new(); if s.is_empty() { continue; } for ch in s.chars() { if !buf.is_empty() && buf != "'" && ch.is_uppercase() && !last_upper { words.push(buf); buf = String::new(); } last_upper = ch.is_uppercase(); buf.extend(change_case(ch)); } words.push(buf); } words.join("_") } // Taken from rustc. pub fn to_camel_case(ident: &str) -> String { ident .trim_matches('_') .split('_') .filter(|component| !component.is_empty()) .map(|component| { let mut camel_cased_component = String::with_capacity(component.len()); let mut new_word = true; let mut prev_is_lower_case = true; for c in component.chars() { // Preserve the case if an uppercase letter follows a lowercase letter, so that // `camelCase` is converted to `CamelCase`. if prev_is_lower_case && c.is_uppercase() { new_word = true; } if new_word { camel_cased_component.extend(c.to_uppercase()); } else { camel_cased_component.extend(c.to_lowercase()); } prev_is_lower_case = c.is_lowercase(); new_word = false; } camel_cased_component }) .fold((String::new(), None), |(acc, prev): (_, Option), next| { // separate two components with an underscore if their boundary cannot // be distinguished using an uppercase/lowercase case distinction let join = prev .and_then(|prev| { let f = next.chars().next()?; let l = prev.chars().last()?; Some(!char_has_case(l) && !char_has_case(f)) }) .unwrap_or(false); (acc + if join { "_" } else { "" } + &next, Some(next)) }) .0 } // Taken from rustc. pub fn char_has_case(c: char) -> bool { c.is_lowercase() || c.is_uppercase() } pub fn is_upper_snake_case(s: &str) -> bool { s.chars().all(|c| c.is_uppercase() || c == '_' || c.is_numeric()) } pub fn replace(buf: &mut String, from: char, to: &str) { if !buf.contains(from) { return; } // FIXME: do this in place. *buf = buf.replace(from, to); } pub fn trim_indent(mut text: &str) -> String { if text.starts_with('\n') { text = &text[1..]; } let indent = text .lines() .filter(|it| !it.trim().is_empty()) .map(|it| it.len() - it.trim_start().len()) .min() .unwrap_or(0); text.split_inclusive('\n') .map( |line| { if line.len() <= indent { line.trim_start_matches(' ') } else { &line[indent..] } }, ) .collect() } pub fn equal_range_by(slice: &[T], mut key: F) -> ops::Range where F: FnMut(&T) -> Ordering, { let start = slice.partition_point(|it| key(it) == Ordering::Less); let len = slice[start..].partition_point(|it| key(it) == Ordering::Equal); start..start + len } #[must_use] pub fn defer(f: F) -> impl Drop { struct D(Option); impl Drop for D { fn drop(&mut self) { if let Some(f) = self.0.take() { f(); } } } D(Some(f)) } /// A [`std::process::Child`] wrapper that will kill the child on drop. #[cfg_attr(not(target_arch = "wasm32"), repr(transparent))] #[derive(Debug)] pub struct JodChild(pub std::process::Child); impl ops::Deref for JodChild { type Target = std::process::Child; fn deref(&self) -> &std::process::Child { &self.0 } } impl ops::DerefMut for JodChild { fn deref_mut(&mut self) -> &mut std::process::Child { &mut self.0 } } impl Drop for JodChild { fn drop(&mut self) { let _ = self.0.kill(); let _ = self.0.wait(); } } impl JodChild { pub fn spawn(mut command: Command) -> sio::Result { command.spawn().map(Self) } pub fn into_inner(self) -> std::process::Child { if cfg!(target_arch = "wasm32") { panic!("no processes on wasm"); } // SAFETY: repr transparent, except on WASM unsafe { std::mem::transmute::(self) } } } // feature: iter_order_by // Iterator::eq_by pub fn iter_eq_by(this: I2, other: I, mut eq: F) -> bool where I: IntoIterator, I2: IntoIterator, F: FnMut(I2::Item, I::Item) -> bool, { let mut other = other.into_iter(); let mut this = this.into_iter(); loop { let x = match this.next() { None => return other.next().is_none(), Some(val) => val, }; let y = match other.next() { None => return false, Some(val) => val, }; if !eq(x, y) { return false; } } } /// Returns all final segments of the argument, longest first. pub fn slice_tails(this: &[T]) -> impl Iterator { (0..this.len()).map(|i| &this[i..]) } pub trait IsNoneOr { type Type; #[allow(clippy::wrong_self_convention)] fn is_none_or(self, s: impl FnOnce(Self::Type) -> bool) -> bool; } #[allow(unstable_name_collisions)] impl IsNoneOr for Option { type Type = T; fn is_none_or(self, f: impl FnOnce(T) -> bool) -> bool { match self { Some(v) => f(v), None => true, } } } #[cfg(test)] mod tests { use super::*; #[test] fn test_trim_indent() { assert_eq!(trim_indent(""), ""); assert_eq!( trim_indent( " hello world " ), "hello\nworld\n" ); assert_eq!( trim_indent( " hello world" ), "hello\nworld" ); assert_eq!(trim_indent(" hello\n world\n"), "hello\nworld\n"); assert_eq!( trim_indent( " fn main() { return 92; } " ), "fn main() {\n return 92;\n}\n" ); } }