use crate::sparse_io::ROW_SEP;
use rayon::prelude::*;
use rustc_hash::FxHashMap as HashMap;
pub fn make_names_unique(names: &mut [Box<str>]) -> usize {
let mut counts: HashMap<Box<str>, usize> = HashMap::default();
let mut num_duped = 0usize;
for name in names.iter_mut() {
if let Some(count) = counts.get_mut(name.as_ref()) {
if *count == 1 {
num_duped += 1;
}
*name = format!("{}-{}", name, count).into_boxed_str();
*count += 1;
} else {
counts.insert(name.clone(), 1);
}
}
if num_duped > 0 {
log::warn!(
"{} names had duplicates and were made unique with -N suffixes",
num_duped
);
}
num_duped
}
pub fn compose_id_name(ids: Vec<Box<str>>, names: Vec<Box<str>>) -> Vec<Box<str>> {
ids.into_iter()
.zip(names)
.map(|(id, name)| {
if name.is_empty() || name.as_ref() == id.as_ref() {
id
} else {
format!("{}_{}", id, name).into_boxed_str()
}
})
.collect()
}
pub fn split_id_name(composite: &str) -> (&str, &str) {
match composite.split_once(ROW_SEP) {
Some((id, name)) if !name.is_empty() => (id, name),
_ => (composite, composite),
}
}
pub struct RowTypeFilter {
patterns: Vec<Box<str>>,
}
impl RowTypeFilter {
pub fn parse(s: &str) -> Self {
let patterns = s
.split(',')
.map(|p| p.trim())
.filter(|p| !p.is_empty())
.map(|p| p.to_ascii_lowercase().into_boxed_str())
.collect();
Self { patterns }
}
pub fn is_empty(&self) -> bool {
self.patterns.is_empty()
}
pub fn matches(&self, s: &str) -> bool {
self.patterns
.iter()
.any(|p| contains_ignore_ascii_case(s, p))
}
}
pub fn contains_ignore_ascii_case(haystack: &str, needle: &str) -> bool {
let n = needle.len();
if n == 0 {
return true;
}
let h = haystack.as_bytes();
if h.len() < n {
return false;
}
h.windows(n)
.any(|w| w.eq_ignore_ascii_case(needle.as_bytes()))
}
pub fn filter_row_indices_by_type(
row_types: &[Box<str>],
select: &str,
remove: &str,
) -> Vec<usize> {
let sel = RowTypeFilter::parse(select);
let rem = RowTypeFilter::parse(remove);
if sel.is_empty() && rem.is_empty() {
return (0..row_types.len()).collect();
}
row_types
.iter()
.enumerate()
.filter_map(|(i, x)| {
let selected = sel.is_empty() || sel.matches(x);
let removed = !rem.is_empty() && rem.matches(x);
if selected && !removed {
Some(i)
} else {
None
}
})
.collect()
}
#[allow(dead_code)]
pub fn flexible_name_match(query: &str, target: &str) -> bool {
let q = query.to_lowercase();
let t = target.to_lowercase();
t == q
|| t.ends_with(&format!("_{}", q))
|| t.starts_with(&format!("{}_", q))
|| t.contains(&format!("_{}_", q))
}
fn is_ensembl_id(s: &str) -> bool {
s.len() >= 8 && s.starts_with("ens") && s.bytes().any(|b| b.is_ascii_digit())
}
static HGNC_RENAMES: &[(&str, &str)] = &[
("h1f0", "h1-0"),
("h1fx", "h1-10"),
("hist1h1b", "h1-5"),
("hist1h1c", "h1-2"),
("hist1h1d", "h1-3"),
("hist1h1e", "h1-4"),
("hist1h2ac", "h2ac6"),
("hist1h2bk", "h2bc12"),
("hist1h4c", "h4c3"),
("hist2h2be", "h2bc21"),
("hist3h2a", "h2ac25"),
("h2afx", "h2ax"),
("h2afv", "h2az2"),
("h2afz", "h2az1"),
("h2afy", "macroh2a1"),
("h3f3a", "h3-3a"),
("h3f3b", "h3-3b"),
("marc1", "mtarc1"),
("marc2", "mtarc2"),
("sepp1", "selenop"),
("selt", "selenot"),
("sepw1", "selenow"),
("fam129a", "niban1"),
("fam129b", "niban2"),
("fam129c", "niban3"),
("rarres3", "plaat4"),
("fyb", "fyb1"),
("cd97", "adgre5"),
("gpr56", "adgrg1"),
("kiaa0101", "pclaf"),
("c10orf54", "vsir"),
("tmem66", "saraf"),
("atpif1", "atp5if1"),
("fam46c", "tent5c"),
("whsc1", "nsd2"),
];
struct RenameMaps {
fwd: HashMap<&'static str, &'static str>,
rev: HashMap<&'static str, &'static str>,
}
fn rename_maps() -> &'static RenameMaps {
static MAPS: std::sync::OnceLock<RenameMaps> = std::sync::OnceLock::new();
MAPS.get_or_init(|| RenameMaps {
fwd: HGNC_RENAMES.iter().copied().collect(),
rev: HGNC_RENAMES.iter().map(|&(o, n)| (n, o)).collect(),
})
}
fn numeric_suffix(sym: &str, prefix: &str) -> Option<u32> {
sym.strip_prefix(prefix)
.filter(|d| !d.is_empty() && d.bytes().all(|b| b.is_ascii_digit()))
.and_then(|d| d.parse().ok())
}
fn alias_candidates(sym: &str) -> Vec<String> {
let maps = rename_maps();
let mut out = Vec::new();
if let Some(&new) = maps.fwd.get(sym) {
out.push(new.to_string());
}
if let Some(&old) = maps.rev.get(sym) {
out.push(old.to_string());
}
for (old, new) in [("march", "marchf"), ("sept", "septin")] {
if let Some(n) = numeric_suffix(sym, old) {
out.push(format!("{new}{n}"));
}
if let Some(n) = numeric_suffix(sym, new) {
out.push(format!("{old}{n}"));
}
}
out
}
#[allow(dead_code)] pub struct GeneIndex {
lowered: Vec<String>,
exact: HashMap<String, usize>,
symbol: HashMap<String, usize>,
ensg: HashMap<String, usize>,
}
#[allow(dead_code)] impl GeneIndex {
#[must_use]
pub fn build(gene_names: &[Box<str>]) -> Self {
let lowered: Vec<String> = gene_names.par_iter().map(|g| g.to_lowercase()).collect();
let mut exact: HashMap<String, usize> = HashMap::default();
let mut symbol: HashMap<String, usize> = HashMap::default();
let mut ensg: HashMap<String, usize> = HashMap::default();
for (i, low) in lowered.iter().enumerate() {
exact.entry(low.clone()).or_insert(i);
let core = low.split('/').next().unwrap_or(low);
if let Some(sym) = core.rsplit('_').next() {
symbol.entry(sym.to_string()).or_insert(i);
}
let head = core.split('_').next().unwrap_or(core);
if is_ensembl_id(head) {
ensg.entry(head.to_string()).or_insert(i);
}
}
Self {
lowered,
exact,
symbol,
ensg,
}
}
#[must_use]
pub fn match_gene(&self, gene: &str) -> Option<usize> {
let gl = gene.to_lowercase();
if let Some(&i) = self.exact.get(&gl) {
return Some(i);
}
if let Some(&i) = self.symbol.get(&gl) {
return Some(i);
}
if let Some(&i) = self.ensg.get(&gl) {
return Some(i);
}
let core = gl.split('/').next().unwrap_or(&gl);
if let Some(sym) = core.rsplit('_').next() {
if sym != gl {
if let Some(&i) = self.symbol.get(sym) {
return Some(i);
}
}
}
let head = core.split('_').next().unwrap_or(core);
if is_ensembl_id(head) {
if let Some(&i) = self.ensg.get(head) {
return Some(i);
}
}
let sym = core.rsplit('_').next().unwrap_or(core);
for alias in alias_candidates(sym) {
if let Some(&i) = self.exact.get(&alias).or_else(|| self.symbol.get(&alias)) {
return Some(i);
}
}
let suffix = format!("_{gl}");
let prefix = format!("{gl}_");
let middle = format!("_{gl}_");
self.lowered
.iter()
.position(|t| t.ends_with(&suffix) || t.starts_with(&prefix) || t.contains(&middle))
}
}
#[allow(dead_code)] #[must_use]
pub fn idf_weight(n_types: usize, df: usize) -> f32 {
(n_types as f32 / df.max(1) as f32).ln()
}
pub fn match_by_substring(
all_names: &[Box<str>],
queries: &[Box<str>],
entity_type: &str,
) -> anyhow::Result<(Vec<usize>, Vec<Box<str>>)> {
let mut matched_indices = Vec::new();
for query in queries.iter() {
for (idx, name) in all_names.iter().enumerate() {
if name.contains(query.as_ref()) {
matched_indices.push(idx);
}
}
}
if matched_indices.is_empty() {
return Err(anyhow::anyhow!(
"No {} names matched the provided queries",
entity_type
));
}
let matched_names: Vec<Box<str>> = matched_indices
.iter()
.map(|&i| all_names[i].clone())
.collect();
Ok((matched_indices, matched_names))
}
#[cfg(test)]
mod tests;