From 3d2ff64eb44f7d27ea98bd1664e81ca2eb239eed Mon Sep 17 00:00:00 2001 From: YPARK Date: Fri, 16 Oct 2026 16:59:49 -0700 Subject: [PATCH 1/5] Tests: a tab-separated membership label keeps its commas --- src/aux/gene_sets.rs | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/src/aux/gene_sets.rs b/src/aux/gene_sets.rs index e268471..b404fdc 100644 --- a/src/aux/gene_sets.rs +++ b/src/aux/gene_sets.rs @@ -433,6 +433,24 @@ mod tests { ); } + #[test] + fn a_tab_separated_label_keeps_its_commas() { + let f = tmp( + "CD4\tCD4-positive, alpha-beta T cell\nCD8A\tCD8-positive, alpha-beta T cell\nMS4A1,B cell\n", + ".tsv", + ); + let pairs = read_membership_pairs(f.path().to_str().unwrap()).unwrap(); + assert_eq!( + pairs, + vec![ + (Box::from("CD4"), Box::from("CD4-positive, alpha-beta T cell")), + (Box::from("CD8A"), Box::from("CD8-positive, alpha-beta T cell")), + (Box::from("MS4A1"), Box::from("B cell")), + ], + "a line with a tab splits on the tab only; one without, on its comma" + ); + } + fn tmp(contents: &str, suffix: &str) -> tempfile::NamedTempFile { let mut f = tempfile::Builder::new().suffix(suffix).tempfile().unwrap(); f.write_all(contents.as_bytes()).unwrap(); From fb3399842a5fec9b63f387854cb78b8db675a4f4 Mon Sep 17 00:00:00 2001 From: YPARK Date: Fri, 16 Oct 2026 17:00:18 -0700 Subject: [PATCH 2/5] A tab-separated membership line splits on its tabs only, so a label keeps its commas --- src/aux/gene_sets.rs | 35 +++++++++++++++++++++-------------- 1 file changed, 21 insertions(+), 14 deletions(-) diff --git a/src/aux/gene_sets.rs b/src/aux/gene_sets.rs index b404fdc..200abd6 100644 --- a/src/aux/gene_sets.rs +++ b/src/aux/gene_sets.rs @@ -94,20 +94,21 @@ pub fn read_gmt(path: &str) -> Result { /// Parse a two-column membership file — `gene label` (a marker panel, /// a TF→target list, any gene→category table) — into `(gene, label)` pairs -/// via the shared, gz-aware line reader (tab or comma delimited). Takes the -/// first two tokens per line; skips blank lines, `#` comments, a -/// `gene`/`symbol` header row and rows missing a label. Labels are kept -/// verbatim; genes are not case-folded. +/// via the shared, gz-aware line reader. A line with a tab splits on its +/// tabs only, so a label keeps its commas (`CD4-positive, alpha-beta T +/// cell`); a line without one splits on its commas. Takes the first two +/// fields per line; skips blank lines, `#` comments, a `gene`/`symbol` +/// header row and rows missing a label. Labels are kept verbatim; genes are +/// not case-folded. pub fn read_membership_pairs(path: &str) -> Result, Box)>> { - let lines = - legume_numeric::matrix::common_io::read_lines_of_words_delim(path, &['\t', ','][..], -1) - .with_context(|| format!("reading membership pairs from {path}"))? - .lines; + let lines = legume_numeric::matrix::common_io::read_lines(path) + .with_context(|| format!("reading membership pairs from {path}"))?; Ok(lines - .into_iter() - .filter_map(|words| { - let gene = words.first()?.trim(); - let label = words.get(1)?.trim(); + .iter() + .filter_map(|line| { + let mut words = line.split(if line.contains('\t') { '\t' } else { ',' }); + let gene = words.next()?.trim(); + let label = words.next()?.trim(); let gl = gene.to_lowercase(); if gene.is_empty() || gene.starts_with('#') @@ -443,8 +444,14 @@ mod tests { assert_eq!( pairs, vec![ - (Box::from("CD4"), Box::from("CD4-positive, alpha-beta T cell")), - (Box::from("CD8A"), Box::from("CD8-positive, alpha-beta T cell")), + ( + Box::from("CD4"), + Box::from("CD4-positive, alpha-beta T cell") + ), + ( + Box::from("CD8A"), + Box::from("CD8-positive, alpha-beta T cell") + ), (Box::from("MS4A1"), Box::from("B cell")), ], "a line with a tab splits on the tab only; one without, on its comma" From 08cf774762fc22d147f8212955ad7dd4cd557e84 Mon Sep 17 00:00:00 2001 From: YPARK Date: Fri, 16 Oct 2026 17:17:20 -0700 Subject: [PATCH 3/5] Tests: a trailing tab does not make a comma line tab-separated; cells keep their columns --- src/aux/gene_sets.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/aux/gene_sets.rs b/src/aux/gene_sets.rs index 200abd6..71eecd7 100644 --- a/src/aux/gene_sets.rs +++ b/src/aux/gene_sets.rs @@ -437,7 +437,7 @@ mod tests { #[test] fn a_tab_separated_label_keeps_its_commas() { let f = tmp( - "CD4\tCD4-positive, alpha-beta T cell\nCD8A\tCD8-positive, alpha-beta T cell\nMS4A1,B cell\n", + "CD4\tCD4-positive, alpha-beta T cell\nCD8A\tCD8-positive, alpha-beta T cell\nMS4A1,B cell\t\nCD14\t\t1.8\n\tCD4-positive, x\n", ".tsv", ); let pairs = read_membership_pairs(f.path().to_str().unwrap()).unwrap(); @@ -454,7 +454,8 @@ mod tests { ), (Box::from("MS4A1"), Box::from("B cell")), ], - "a line with a tab splits on the tab only; one without, on its comma" + "a line with a tab splits on the tab only; one without (trailing space aside), on its \ + comma; cells keep their columns, so an empty gene or label skips the row" ); } From ce374b82626a623e2f64565b115237725633544e Mon Sep 17 00:00:00 2001 From: YPARK Date: Fri, 16 Oct 2026 17:17:24 -0700 Subject: [PATCH 4/5] Trailing whitespace does not choose a membership line's split --- src/aux/gene_sets.rs | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/aux/gene_sets.rs b/src/aux/gene_sets.rs index 71eecd7..314868e 100644 --- a/src/aux/gene_sets.rs +++ b/src/aux/gene_sets.rs @@ -94,9 +94,10 @@ pub fn read_gmt(path: &str) -> Result { /// Parse a two-column membership file — `gene label` (a marker panel, /// a TF→target list, any gene→category table) — into `(gene, label)` pairs -/// via the shared, gz-aware line reader. A line with a tab splits on its -/// tabs only, so a label keeps its commas (`CD4-positive, alpha-beta T -/// cell`); a line without one splits on its commas. Takes the first two +/// via the shared, gz-aware line reader. A line with a tab (trailing +/// whitespace aside) splits on its tabs only, so a label keeps its commas +/// (`CD4-positive, alpha-beta T cell`); a line without one splits on its +/// commas. Cells keep their columns: an empty gene or label skips the row. Takes the first two /// fields per line; skips blank lines, `#` comments, a `gene`/`symbol` /// header row and rows missing a label. Labels are kept verbatim; genes are /// not case-folded. @@ -106,6 +107,8 @@ pub fn read_membership_pairs(path: &str) -> Result, Box)>> { Ok(lines .iter() .filter_map(|line| { + // Trailing whitespace (a stray tab, a CR) does not choose the split. + let line = line.trim_end(); let mut words = line.split(if line.contains('\t') { '\t' } else { ',' }); let gene = words.next()?.trim(); let label = words.next()?.trim(); From 6fc960314ddf1df214e393bc29edea2a1f400039 Mon Sep 17 00:00:00 2001 From: YPARK Date: Fri, 16 Oct 2026 17:30:06 -0700 Subject: [PATCH 5/5] Version 0.8.5 --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 817295c..0e856d1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,7 +9,7 @@ edition = "2021" readme = "README.md" # Own version (decoupled from workspace.version): data-beans evolves its CLI / # QC API independently of the shared utility crates. -version = "0.8.4" +version = "0.8.5" rust-version = "1.91" # Ship only the sources, README and license: nothing else in the working tree # (data stores, saved name lists, scratch files) can end up on crates.io.