blob: 76dbd42ff6b24885ecde71fd78bc609cad714642 [file] [log] [blame]
//! Missing batteries for standard libraries.
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<Hasher: std::hash::Hasher + Default>(thing: impl std::hash::Hash) -> u64 {
std::hash::BuildHasher::hash_one(&std::hash::BuildHasherDefault::<Hasher>::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<T, U> 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<T, U, V> 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<F, I>(mut s: &str, change_case: F) -> String
where
F: Fn(char) -> I,
I: Iterator<Item = char>,
{
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<String>), 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<T, F>(slice: &[T], mut key: F) -> ops::Range<usize>
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: FnOnce()>(f: F) -> impl Drop {
struct D<F: FnOnce()>(Option<F>);
impl<F: FnOnce()> Drop for D<F> {
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<Self> {
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::<JodChild, std::process::Child>(self) }
}
}
// feature: iter_order_by
// Iterator::eq_by
pub fn iter_eq_by<I, I2, F>(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<T>(this: &[T]) -> impl Iterator<Item = &[T]> {
(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<T> IsNoneOr for Option<T> {
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"
);
}
}