test: harden pallet card layout parsing
Some checks failed
Desktop Release / Bootstrap updater signing key (push) Has been cancelled
Desktop Release / Windows x64 NSIS (push) Has been cancelled
Desktop Release / Linux x64 DEB + AppImage (push) Has been cancelled
Desktop Release / Publish public desktop release (push) Has been cancelled
Some checks failed
Desktop Release / Bootstrap updater signing key (push) Has been cancelled
Desktop Release / Windows x64 NSIS (push) Has been cancelled
Desktop Release / Linux x64 DEB + AppImage (push) Has been cancelled
Desktop Release / Publish public desktop release (push) Has been cancelled
This commit is contained in:
parent
288ad0b3a5
commit
ba534cfdb7
@ -27,6 +27,99 @@ fn canonical(value: &str) -> String {
|
||||
.to_ascii_uppercase()
|
||||
}
|
||||
|
||||
fn find_ci(line: &str, needle: &str) -> Option<usize> {
|
||||
line.to_ascii_uppercase().find(&needle.to_ascii_uppercase())
|
||||
}
|
||||
|
||||
fn column_slice(line: &str, start: usize, end: Option<usize>) -> &str {
|
||||
let start = start.min(line.len());
|
||||
let end = end.unwrap_or(line.len()).min(line.len());
|
||||
if start >= end {
|
||||
""
|
||||
} else {
|
||||
line.get(start..end).unwrap_or("")
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_layout_tables(raw_text: &str, snapshot: &mut PalletSnapshot) {
|
||||
let lines: Vec<&str> = raw_text.lines().collect();
|
||||
let right_col = lines
|
||||
.iter()
|
||||
.filter_map(|line| find_ci(line, "Pallet:").or_else(|| find_ci(line, "Grower")))
|
||||
.min();
|
||||
|
||||
if let Some((index, header)) = lines.iter().enumerate().find(|(_, line)| {
|
||||
find_ci(line, "ORIGIN").is_some()
|
||||
&& find_ci(line, "VAR").is_some()
|
||||
&& find_ci(line, "BRAND").is_some()
|
||||
&& find_ci(line, "CLASS").is_some()
|
||||
}) {
|
||||
let start = find_ci(header, "ORIGIN").unwrap_or(0);
|
||||
for line in lines.iter().skip(index + 1).take(4) {
|
||||
let tokens: Vec<&str> = column_slice(line, start, right_col)
|
||||
.split_whitespace()
|
||||
.collect();
|
||||
if tokens.len() >= 5
|
||||
&& (2..=3).contains(&tokens[0].len())
|
||||
&& tokens[0].chars().all(|ch| ch.is_ascii_alphabetic())
|
||||
&& tokens[3].eq_ignore_ascii_case("CLASS")
|
||||
{
|
||||
snapshot.origin = clean(tokens[0]);
|
||||
snapshot.variety = clean(tokens[1]);
|
||||
snapshot.brand = clean(tokens[2]);
|
||||
snapshot.class = clean(&format!("{} {}", tokens[3], tokens[4]));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some((index, header)) = lines.iter().enumerate().find(|(_, line)| {
|
||||
find_ci(line, "BASE").is_some() && find_ci(line, "PACK").is_some()
|
||||
}) {
|
||||
let start = find_ci(header, "BASE").unwrap_or(0);
|
||||
for line in lines.iter().skip(index + 1).take(4) {
|
||||
let tokens: Vec<&str> = column_slice(line, start, right_col)
|
||||
.split_whitespace()
|
||||
.collect();
|
||||
if tokens.len() >= 2
|
||||
&& tokens[0].len() >= 2
|
||||
&& tokens[0].chars().all(|ch| ch.is_ascii_alphabetic())
|
||||
&& tokens[1].chars().all(|ch| ch.is_ascii_alphanumeric() || ch == '-')
|
||||
{
|
||||
snapshot.base = clean(tokens[0]);
|
||||
snapshot.pack = clean(tokens[1]);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some((index, header)) = lines.iter().enumerate().find(|(_, line)| {
|
||||
find_ci(line, "Grower").is_some()
|
||||
&& find_ci(line, "Packrun").is_some()
|
||||
&& find_ci(line, "Size").is_some()
|
||||
&& find_ci(line, "Qty").is_some()
|
||||
}) {
|
||||
let start = find_ci(header, "Grower").unwrap_or_else(|| right_col.unwrap_or(0));
|
||||
for line in lines.iter().skip(index + 1).take(5) {
|
||||
let tokens: Vec<&str> = column_slice(line, start, None)
|
||||
.split_whitespace()
|
||||
.collect();
|
||||
if tokens.len() >= 4
|
||||
&& tokens[0].chars().all(|ch| ch.is_ascii_alphanumeric() || ch == '-')
|
||||
&& tokens[1].contains('/')
|
||||
&& tokens[2].chars().all(|ch| ch.is_ascii_digit())
|
||||
&& tokens[3].chars().all(|ch| ch.is_ascii_digit())
|
||||
{
|
||||
snapshot.grower = clean(tokens[0]);
|
||||
snapshot.packrun = clean(tokens[1]);
|
||||
snapshot.size = clean(tokens[2]);
|
||||
snapshot.qty = clean(tokens[3]);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn parse_pallet_snapshot(raw_text: &str, pallet_id: &str) -> PalletSnapshot {
|
||||
let flat = raw_text.split_whitespace().collect::<Vec<_>>().join(" ");
|
||||
let mut snapshot = PalletSnapshot {
|
||||
@ -51,25 +144,31 @@ pub fn parse_pallet_snapshot(raw_text: &str, pallet_id: &str) -> PalletSnapshot
|
||||
);
|
||||
snapshot.total = capture(&flat, r"(?i)\bTotal\s+([0-9]{1,5})\b", 1);
|
||||
|
||||
if let Ok(regex) = Regex::new(
|
||||
r"(?i)\bGrower\s+Packrun\s+Size\s+Qty\s+([A-Z0-9-]+)\s+([A-Z0-9/._-]+)\s+([0-9]{1,3})\s+([0-9]{1,5})\b",
|
||||
) {
|
||||
if let Some(captures) = regex.captures(&flat) {
|
||||
snapshot.grower = captures.get(1).and_then(|m| clean(m.as_str()));
|
||||
snapshot.packrun = captures.get(2).and_then(|m| clean(m.as_str()));
|
||||
snapshot.size = captures.get(3).and_then(|m| clean(m.as_str()));
|
||||
snapshot.qty = captures.get(4).and_then(|m| clean(m.as_str()));
|
||||
parse_layout_tables(raw_text, &mut snapshot);
|
||||
|
||||
if snapshot.grower.is_none() {
|
||||
if let Ok(regex) = Regex::new(
|
||||
r"(?i)\bGrower\s+Packrun\s+Size\s+Qty\s+([A-Z0-9-]+)\s+([A-Z0-9/._-]+)\s+([0-9]{1,3})\s+([0-9]{1,5})\b",
|
||||
) {
|
||||
if let Some(captures) = regex.captures(&flat) {
|
||||
snapshot.grower = captures.get(1).and_then(|m| clean(m.as_str()));
|
||||
snapshot.packrun = captures.get(2).and_then(|m| clean(m.as_str()));
|
||||
snapshot.size = captures.get(3).and_then(|m| clean(m.as_str()));
|
||||
snapshot.qty = captures.get(4).and_then(|m| clean(m.as_str()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Ok(regex) = Regex::new(
|
||||
r"(?i)\bORIGIN\s+VAR\s+BRAND\s+CLASS(?:\s+Pallet\s*:\s*[A-Z0-9-]+)?\s+([A-Z]{2,3})\s+([A-Z][A-Z -]{1,20}?)\s+([A-Z0-9]{2,12})\s+(CLASS\s*\.?\s*[0-9]+\.?)(?:\s|$)",
|
||||
) {
|
||||
if let Some(captures) = regex.captures(&flat) {
|
||||
snapshot.origin = captures.get(1).and_then(|m| clean(m.as_str()));
|
||||
snapshot.variety = captures.get(2).and_then(|m| clean(m.as_str()));
|
||||
snapshot.brand = captures.get(3).and_then(|m| clean(m.as_str()));
|
||||
snapshot.class = captures.get(4).and_then(|m| clean(m.as_str()));
|
||||
if snapshot.origin.is_none() {
|
||||
if let Ok(regex) = Regex::new(
|
||||
r"(?i)\bORIGIN\s+VAR\s+BRAND\s+CLASS(?:\s+Pallet\s*:\s*[A-Z0-9-]+)?\s+([A-Z]{2,3})\s+([A-Z][A-Z -]{1,20}?)\s+([A-Z0-9]{2,12})\s+(CLASS\s*\.?\s*[0-9]+\.?)(?:\s|$)",
|
||||
) {
|
||||
if let Some(captures) = regex.captures(&flat) {
|
||||
snapshot.origin = captures.get(1).and_then(|m| clean(m.as_str()));
|
||||
snapshot.variety = captures.get(2).and_then(|m| clean(m.as_str()));
|
||||
snapshot.brand = captures.get(3).and_then(|m| clean(m.as_str()));
|
||||
snapshot.class = captures.get(4).and_then(|m| clean(m.as_str()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -84,12 +183,14 @@ pub fn parse_pallet_snapshot(raw_text: &str, pallet_id: &str) -> PalletSnapshot
|
||||
}
|
||||
}
|
||||
|
||||
if let Ok(regex) = Regex::new(
|
||||
r"(?i)\bBASE\s+PACK\s+([A-Z]{2,12})\s+([A-Z0-9-]{2,12})\b",
|
||||
) {
|
||||
if let Some(captures) = regex.captures(&flat) {
|
||||
snapshot.base = captures.get(1).and_then(|m| clean(m.as_str()));
|
||||
snapshot.pack = captures.get(2).and_then(|m| clean(m.as_str()));
|
||||
if snapshot.base.is_none() {
|
||||
if let Ok(regex) = Regex::new(
|
||||
r"(?i)\bBASE\s+PACK\s+([A-Z]{2,12})\s+([A-Z0-9-]{2,12})\b",
|
||||
) {
|
||||
if let Some(captures) = regex.captures(&flat) {
|
||||
snapshot.base = captures.get(1).and_then(|m| clean(m.as_str()));
|
||||
snapshot.pack = captures.get(2).and_then(|m| clean(m.as_str()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -164,6 +265,18 @@ mod tests {
|
||||
Total 30
|
||||
"#;
|
||||
|
||||
const LAYOUT_CARD: &str = r#"
|
||||
Avocados - Hass
|
||||
ORIGIN VAR BRAND CLASS Pallet: 40595983
|
||||
NZ Hass NZAG CLASS 3. Grower Packrun Size Qty
|
||||
BASE PACK 89930 89930/16/17/26 20 30
|
||||
CHEP B37
|
||||
Pick Date: 16/09/2026
|
||||
Pack Date: 17/09/2026
|
||||
Time: 14:46:10
|
||||
Total 30
|
||||
"#;
|
||||
|
||||
#[test]
|
||||
fn parses_export_card_fields() {
|
||||
let snapshot = parse_pallet_snapshot(CARD, "40595983");
|
||||
@ -182,6 +295,19 @@ mod tests {
|
||||
assert_eq!(snapshot.printed_time.as_deref(), Some("14:46:10"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parses_interleaved_pdf_layout_columns() {
|
||||
let snapshot = parse_pallet_snapshot(LAYOUT_CARD, "40595983");
|
||||
assert_eq!(snapshot.origin.as_deref(), Some("NZ"));
|
||||
assert_eq!(snapshot.brand.as_deref(), Some("NZAG"));
|
||||
assert_eq!(snapshot.base.as_deref(), Some("CHEP"));
|
||||
assert_eq!(snapshot.pack.as_deref(), Some("B37"));
|
||||
assert_eq!(snapshot.grower.as_deref(), Some("89930"));
|
||||
assert_eq!(snapshot.packrun.as_deref(), Some("89930/16/17/26"));
|
||||
assert_eq!(snapshot.size.as_deref(), Some("20"));
|
||||
assert_eq!(snapshot.qty.as_deref(), Some("30"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ignores_print_time_but_reports_business_field_changes() {
|
||||
let previous = parse_pallet_snapshot(CARD, "40595983");
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user