Fix/noissue hyphenated print label ranges s20261010 t220019 z f82d4 (#942)

* fix(print): resolve ranges between hyphenated page labels

* test(print): resolve ambiguous hyphenated label boundaries
This commit is contained in:
Nigel Ihza Firdausy authored and GitHub committed 2026-10-11 13:06:56 -04:00
1 parent dd400d6b23
commit 96538312df
2 files changed
+49

No files matched your search

+32
View File
@@ -24,6 +24,26 @@ fn page_of(tok: &str, count: usize, labels: &[String]) -> Result<usize, PrintErr
}
}
/// Split a range whose endpoint is a logical page label containing a hyphen, such
/// as "A-1-A-3" or "A-2-". The plain range parser must not mistake the first
/// hyphen inside the label for the delimiter between endpoints.
fn hyphenated_label_range(part: &str, count: usize, labels: &[String]) -> Option<(usize, usize)> {
for (split, _) in part.match_indices('-') {
let (Some(first), Some(last)) = (part.get(..split), part.get(split + 1..)) else { continue };
let (first, last) = (first.trim(), last.trim());
// Avoid changing the meaning of ordinary numeric and open-ended ranges.
if !labels.iter().any(|l| l.contains('-') && (l == first || l == last)) {
continue;
}
let from = if first.is_empty() { Some(0) } else { page_of(first, count, labels).ok() };
let to = if last.is_empty() { count.checked_sub(1) } else { page_of(last, count, labels).ok() };
if let (Some(from), Some(to)) = (from, to) {
return Some((from, to));
}
}
None
}
/// The pages to print (0-based, in print order). `range` is `None` for all pages; it may list
/// numbers and labels with `-` ranges (open-ended allowed: `5-`, `-3`). The subset counts the
/// selected pages (the first selected is "odd"), as Acrobat does.
@@ -33,6 +53,18 @@ pub fn select_pages(count: usize, range: Option<&str>, labels: &[String], subset
None => pages.extend(0..count),
Some(r) => {
for part in r.split([',', ';']).map(str::trim).filter(|p| !p.is_empty()) {
// A complete label (e.g. "A-1") still wins over interpreting it as a range.
// A range of such labels (e.g. "A-1-A-3") needs a later '-' split.
if !labels.iter().any(|l| l == part)
&& let Some((from, to)) = hyphenated_label_range(part, count, labels)
{
if from <= to {
pages.extend(from..=to);
} else {
pages.extend((to..=from).rev());
}
continue;
}
match part.split_once('-') {
// A label may itself contain a dash ("A-1"): try the whole token first.
Some(_) if labels.iter().any(|l| l == part) => pages.push(page_of(part, count, labels)?),
+17
View File
@@ -77,6 +77,23 @@ fn page_selection() {
assert_eq!(select_listed(5, &[], Subset::All, false), Err(PrintError::NoPages));
}
#[test]
fn page_selection_handles_ranges_between_hyphenated_labels() {
let labels: Vec<String> = ["A-1", "A-2", "A-3", "B-1", "B-2"].iter().map(|s| s.to_string()).collect();
let pages = |range| select_pages(5, Some(range), &labels, Subset::All, false).unwrap();
assert_eq!(pages("A-1"), [0], "the complete label takes precedence over a range");
assert_eq!(pages("A-1-A-3"), [0, 1, 2]);
assert_eq!(pages("A-3-A-1"), [2, 1, 0], "reverse logical ranges");
assert_eq!(pages("A-2-"), [1, 2, 3, 4], "an open-ended range starting at a hyphenated label");
assert_eq!(pages("-A-2"), [0, 1], "an open-ended range ending at a hyphenated label");
assert_eq!(pages("A-2-5"), [1, 2, 3, 4], "a label followed by a numeric endpoint");
assert_eq!(pages("B-1-B-2"), [3, 4]);
// If an earlier dash yields one known label but an unknown endpoint, continue
// checking later dashes instead of rejecting the whole range.
let ambiguous: Vec<String> = ["A-1", "A-1-B", "C"].iter().map(|s| s.to_string()).collect();
assert_eq!(select_pages(3, Some("A-1-B-C"), &ambiguous, Subset::All, false).unwrap(), [1, 2]);
}
#[test]
fn size_modes() {
let sizes = [(200.0, 300.0), (1000.0, 1500.0)];