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()
|
.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 {
|
pub fn parse_pallet_snapshot(raw_text: &str, pallet_id: &str) -> PalletSnapshot {
|
||||||
let flat = raw_text.split_whitespace().collect::<Vec<_>>().join(" ");
|
let flat = raw_text.split_whitespace().collect::<Vec<_>>().join(" ");
|
||||||
let mut snapshot = PalletSnapshot {
|
let mut snapshot = PalletSnapshot {
|
||||||
@ -51,6 +144,9 @@ 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);
|
snapshot.total = capture(&flat, r"(?i)\bTotal\s+([0-9]{1,5})\b", 1);
|
||||||
|
|
||||||
|
parse_layout_tables(raw_text, &mut snapshot);
|
||||||
|
|
||||||
|
if snapshot.grower.is_none() {
|
||||||
if let Ok(regex) = Regex::new(
|
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",
|
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",
|
||||||
) {
|
) {
|
||||||
@ -61,7 +157,9 @@ pub fn parse_pallet_snapshot(raw_text: &str, pallet_id: &str) -> PalletSnapshot
|
|||||||
snapshot.qty = captures.get(4).and_then(|m| clean(m.as_str()));
|
snapshot.qty = captures.get(4).and_then(|m| clean(m.as_str()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if snapshot.origin.is_none() {
|
||||||
if let Ok(regex) = Regex::new(
|
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|$)",
|
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|$)",
|
||||||
) {
|
) {
|
||||||
@ -72,6 +170,7 @@ pub fn parse_pallet_snapshot(raw_text: &str, pallet_id: &str) -> PalletSnapshot
|
|||||||
snapshot.class = captures.get(4).and_then(|m| clean(m.as_str()));
|
snapshot.class = captures.get(4).and_then(|m| clean(m.as_str()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if snapshot.variety.is_none() {
|
if snapshot.variety.is_none() {
|
||||||
if let Ok(regex) = Regex::new(r"(?i)^\s*Avocados?\s*-\s*([A-Z][A-Z -]{1,20})\s*$") {
|
if let Ok(regex) = Regex::new(r"(?i)^\s*Avocados?\s*-\s*([A-Z][A-Z -]{1,20})\s*$") {
|
||||||
@ -84,6 +183,7 @@ pub fn parse_pallet_snapshot(raw_text: &str, pallet_id: &str) -> PalletSnapshot
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if snapshot.base.is_none() {
|
||||||
if let Ok(regex) = Regex::new(
|
if let Ok(regex) = Regex::new(
|
||||||
r"(?i)\bBASE\s+PACK\s+([A-Z]{2,12})\s+([A-Z0-9-]{2,12})\b",
|
r"(?i)\bBASE\s+PACK\s+([A-Z]{2,12})\s+([A-Z0-9-]{2,12})\b",
|
||||||
) {
|
) {
|
||||||
@ -92,6 +192,7 @@ pub fn parse_pallet_snapshot(raw_text: &str, pallet_id: &str) -> PalletSnapshot
|
|||||||
snapshot.pack = captures.get(2).and_then(|m| clean(m.as_str()));
|
snapshot.pack = captures.get(2).and_then(|m| clean(m.as_str()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
snapshot
|
snapshot
|
||||||
}
|
}
|
||||||
@ -164,6 +265,18 @@ mod tests {
|
|||||||
Total 30
|
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]
|
#[test]
|
||||||
fn parses_export_card_fields() {
|
fn parses_export_card_fields() {
|
||||||
let snapshot = parse_pallet_snapshot(CARD, "40595983");
|
let snapshot = parse_pallet_snapshot(CARD, "40595983");
|
||||||
@ -182,6 +295,19 @@ mod tests {
|
|||||||
assert_eq!(snapshot.printed_time.as_deref(), Some("14:46:10"));
|
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]
|
#[test]
|
||||||
fn ignores_print_time_but_reports_business_field_changes() {
|
fn ignores_print_time_but_reports_business_field_changes() {
|
||||||
let previous = parse_pallet_snapshot(CARD, "40595983");
|
let previous = parse_pallet_snapshot(CARD, "40595983");
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user