use std::collections::hash_map::DefaultHasher;
use std::collections::HashSet;
use std::hash::{Hash, Hasher};
use std::path::{Path, PathBuf};
use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
use rayon::prelude::*;
#[inline]
fn partition_bucket(key: &str, p: usize) -> usize {
if p <= 1 {
return 0;
}
let mut h = DefaultHasher::new();
key.hash(&mut h);
(h.finish() as usize) % p
}
fn puniq_sequential(list: Vec<String>) -> Vec<String> {
let mut seen = HashSet::<String>::new();
let mut out = Vec::new();
for v in list {
if seen.insert(v.clone()) {
out.push(v);
}
}
out
}
fn puniq_parallel_buckets(list: Vec<String>, p: usize) -> Vec<String> {
let mut buckets: Vec<Vec<(usize, String)>> = vec![vec![]; p];
for (i, v) in list.into_iter().enumerate() {
buckets[partition_bucket(&v, p)].push((i, v));
}
let partials: Vec<Vec<(usize, String)>> = buckets
.into_par_iter()
.map(|bucket| {
let mut seen = HashSet::<String>::new();
let mut out = Vec::new();
for (i, v) in bucket {
if seen.insert(v.clone()) {
out.push((i, v));
}
}
out
})
.collect();
let mut merged: Vec<(usize, String)> = partials.into_iter().flatten().collect();
merged.sort_by_key(|(i, _)| *i);
merged.into_iter().map(|(_, v)| v).collect()
}
pub fn puniq_run(list: Vec<String>, num_partitions: usize) -> Vec<String> {
let n = list.len();
if n == 0 {
return vec![];
}
let p = num_partitions.max(1);
if p <= 1 || n < p.saturating_mul(4) {
puniq_sequential(list)
} else {
puniq_parallel_buckets(list, p)
}
}
pub fn pany_run(list: &[String], test: impl Fn(&str) -> bool + Sync + Send) -> bool {
let found = AtomicBool::new(false);
list.par_iter().for_each(|item| {
if !found.load(Ordering::Relaxed) && test(item) {
found.store(true, Ordering::Relaxed);
}
});
found.load(Ordering::Relaxed)
}
pub fn pfirst_run(list: &[String], test: impl Fn(&str) -> bool + Sync + Send) -> Option<usize> {
if list.is_empty() {
return None;
}
let best = AtomicUsize::new(usize::MAX);
(0..list.len()).into_par_iter().for_each(|i| {
if i >= best.load(Ordering::Acquire) {
return;
}
if test(&list[i]) {
let mut b = best.load(Ordering::Relaxed);
while i < b {
match best.compare_exchange_weak(b, i, Ordering::Relaxed, Ordering::Relaxed) {
Ok(_) => break,
Err(x) => b = x,
}
}
}
});
match best.load(Ordering::Relaxed) {
usize::MAX => None,
b => Some(b),
}
}
#[derive(Copy, Clone, Default)]
pub struct SortSpec {
pub numeric: bool,
pub reverse: bool,
}
pub fn psort_run(mut list: Vec<String>, spec: SortSpec) -> Vec<String> {
let num = |s: &str| s.trim().parse::<f64>().unwrap_or(0.0);
list.par_sort_by(|a, b| {
let ord = if spec.numeric {
num(a)
.partial_cmp(&num(b))
.unwrap_or(std::cmp::Ordering::Equal)
.then_with(|| a.cmp(b))
} else {
a.cmp(b)
};
if spec.reverse {
ord.reverse()
} else {
ord
}
});
list
}
pub fn chunk_items(items: &[String], size: usize) -> Vec<&[String]> {
items.chunks(size.max(1)).collect()
}
pub fn auto_chunk_size(n: usize, threads: usize) -> usize {
let chunks = threads.saturating_mul(8).max(1);
n.div_ceil(chunks).max(1)
}
pub fn line_aligned_chunks(data: &[u8], max_chunks: usize) -> Vec<(usize, usize)> {
let len = data.len();
if len == 0 {
return vec![];
}
let k = max_chunks.max(1).min(len);
let mut splits: Vec<usize> = (0..=k).map(|i| i * len / k).collect();
for split in splits.iter_mut().take(k).skip(1) {
let mut p = *split;
while p < len && p > 0 && data[p - 1] != b'\n' {
p += 1;
}
*split = p;
}
for i in 1..=k {
if splits[i] < splits[i - 1] {
splits[i] = splits[i - 1];
}
}
let mut out = Vec::new();
for i in 0..k {
let (s, e) = (splits[i], splits[i + 1]);
if s < e {
out.push((s, e));
}
}
if out.is_empty() {
out.push((0, len));
}
out
}
pub fn lines_in_range(data: &[u8], start: usize, end: usize) -> Vec<Vec<u8>> {
let slice = &data[start..end];
let mut out = Vec::new();
let mut s = 0usize;
while s < slice.len() {
let e = slice[s..]
.iter()
.position(|&b| b == b'\n')
.map_or(slice.len(), |p| s + p);
let mut line = &slice[s..e];
if line.ends_with(b"\r") {
line = &line[..line.len() - 1];
}
out.push(line.to_vec());
if e >= slice.len() {
break;
}
s = e + 1;
}
out
}
pub fn collect_paths(roots: &[PathBuf]) -> Vec<PathBuf> {
roots.par_iter().flat_map_iter(|r| collect_under(r)).collect()
}
fn collect_under(path: &Path) -> Vec<PathBuf> {
if path.is_file() || (path.is_symlink() && !path.is_dir()) {
return vec![path.to_path_buf()];
}
if !path.is_dir() {
return vec![];
}
let Ok(read) = std::fs::read_dir(path) else {
return vec![];
};
let mut entries: Vec<_> = read.filter_map(|e| e.ok()).collect();
entries.sort_by_key(|e| e.file_name());
let mut out = vec![path.to_path_buf()];
let sub: Vec<PathBuf> = entries
.par_iter()
.flat_map_iter(|e| collect_under(&e.path()))
.collect();
out.extend(sub);
out
}
#[cfg(test)]
mod tests {
use super::*;
fn s(v: &[&str]) -> Vec<String> {
v.iter().map(|x| x.to_string()).collect()
}
#[test]
fn puniq_sequential_keeps_first_occurrence() {
assert_eq!(puniq_run(s(&["1", "2", "1", "3", "2"]), 1), s(&["1", "2", "3"]));
}
#[test]
fn puniq_parallel_matches_sequential_order() {
let list = s(&["1", "2", "1", "3", "2", "4", "1", "5"]);
assert_eq!(puniq_run(list.clone(), 2), puniq_sequential(list));
}
#[test]
fn puniq_parallel_large_input() {
let list: Vec<String> = (0..5000).map(|i| (i % 97).to_string()).collect();
assert_eq!(puniq_run(list.clone(), 8), puniq_sequential(list));
}
#[test]
fn pany_finds_and_misses() {
let l = s(&["1", "2", "3"]);
assert!(pany_run(&l, |v| v == "2"));
assert!(!pany_run(&l, |v| v == "4"));
assert!(!pany_run(&[], |_| true));
}
#[test]
fn pfirst_returns_lowest_passing_index() {
let l: Vec<String> = (0..200).map(|i| i.to_string()).collect();
assert_eq!(pfirst_run(&l, |v| v.parse::<i32>().unwrap() >= 40), Some(40));
assert_eq!(pfirst_run(&l, |_| false), None);
assert_eq!(pfirst_run(&[], |_| true), None);
}
#[test]
fn psort_byte_numeric_and_reverse() {
let l = s(&["10", "9", "100", "b", "a"]);
assert_eq!(psort_run(l.clone(), SortSpec::default()), s(&["10", "100", "9", "a", "b"]));
assert_eq!(
psort_run(l.clone(), SortSpec { numeric: true, reverse: false }),
s(&["a", "b", "9", "10", "100"])
);
assert_eq!(
psort_run(l, SortSpec { numeric: true, reverse: true }),
s(&["100", "10", "9", "b", "a"])
);
}
#[test]
fn psort_is_stable_for_equal_numeric_keys() {
let out = psort_run(s(&["1.0", "1"]), SortSpec { numeric: true, reverse: false });
assert_eq!(out, s(&["1", "1.0"]));
}
#[test]
fn chunking_covers_every_item_in_order() {
let l: Vec<String> = (0..10).map(|i| i.to_string()).collect();
let chunks = chunk_items(&l, 4);
assert_eq!(chunks.iter().map(|c| c.len()).collect::<Vec<_>>(), vec![4, 4, 2]);
let flat: Vec<&String> = chunks.iter().flat_map(|c| c.iter()).collect();
assert_eq!(flat, l.iter().collect::<Vec<_>>());
assert_eq!(chunk_items(&l, 0).len(), 10);
}
#[test]
fn auto_chunk_size_is_at_least_one() {
assert_eq!(auto_chunk_size(0, 4), 1);
assert_eq!(auto_chunk_size(1000, 4), 32);
assert_eq!(auto_chunk_size(3, 18), 1);
}
#[test]
fn line_chunks_never_split_a_line() {
let data = b"a\nbb\nccc\ndddd\n";
for k in 1..8 {
let chunks = line_aligned_chunks(data, k);
let rebuilt: Vec<u8> = chunks.iter().flat_map(|(a, b)| data[*a..*b].iter().copied()).collect();
assert_eq!(rebuilt, data);
for (a, _) in &chunks {
assert!(*a == 0 || data[*a - 1] == b'\n');
}
}
assert!(line_aligned_chunks(b"", 4).is_empty());
}
#[test]
fn lines_in_range_handles_missing_final_newline_and_crlf() {
let data = b"one\r\ntwo\nthree";
assert_eq!(
lines_in_range(data, 0, data.len()),
vec![b"one".to_vec(), b"two".to_vec(), b"three".to_vec()]
);
}
#[test]
fn collect_paths_walks_files_and_directories_in_name_order() {
let base = std::env::temp_dir().join(format!("zshrs_par_ops_walk_{}", std::process::id()));
let _ = std::fs::remove_dir_all(&base);
std::fs::create_dir_all(base.join("d")).unwrap();
std::fs::write(base.join("b.txt"), b"z").unwrap();
std::fs::write(base.join("d").join("a.txt"), b"z").unwrap();
let got = collect_paths(std::slice::from_ref(&base));
let want = vec![
base.clone(),
base.join("b.txt"),
base.join("d"),
base.join("d").join("a.txt"),
];
assert_eq!(got, want);
let _ = std::fs::remove_dir_all(&base);
}
#[test]
fn collect_paths_missing_root_is_empty() {
assert!(collect_paths(&[PathBuf::from("/nonexistent_zshrs_par_ops")]).is_empty());
}
}