EXTRACT band-aware Farbschlag: deep bands (gen>=2) have no colour cell
Julian-confirmed against the real xlsx (Kentucky, Akio Kids): pedigree blocks have two shapes. EARLY bands (gen 0-1, cols B/E/H) are 5-cell Name/DOB/Farbschlag/Genotype/Zucht — real Farbschlag present. DEEP bands (gen>=2, cols K/N/Q...) are 3-cell Name/DOB/Genotype with NO Farbschlag — the colour is derived from the genotype. The old extractor grabbed the NEXT block's name or a stray health note (e.g. "DD-Tumor", "Chronische Ohrenentzündung...") as the deep-band Farbschlag — source of the D1 'name-in-Farbschlag' conflicts and a big chunk of farbschlagUnmatched. Fix: gen_of(col)>=2 => never assign a Farbschlag in that block (Name/DOB/Genotype only); the cell is left for the next block's name search. Deep-band animals load with empty Farbschlag and the UI computes 'Errechnet' from the stored genotype (lossless). Dedup already prefers a real (non-empty) early-band Farbschlag, so an animal appearing in both an early band (Chesnut -> 'Kohlfuchsschimmel') and deep bands keeps the real value; Chesnut and Tennessee stay separate (distinct genotype + name+DOB key). Result: 248/306 loadable animals now have empty Farbschlag (genotype- derived), 0 suspicious name/note values remain, 0 Farbschlag-based conflicts (D1 dissolved). Regression test test_extract.py builds a two-band xlsx and asserts the deep band yields no Farbschlag + the stray note doesn't bleed. Extractor-only (no C# change). python test_genotype + test_extract green; dotnet 118/118. Batches with Kevin's GEN-3f seed for one re-import. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -211,6 +211,11 @@ def extract_stammbaum(path):
|
|||||||
farbschlag = ""
|
farbschlag = ""
|
||||||
geno = geno0
|
geno = geno0
|
||||||
breeder = ""
|
breeder = ""
|
||||||
|
# BAND-AWARE (Julian-confirmed): early bands (gen 0-1, cols B/E/H) are 5-cell blocks
|
||||||
|
# WITH a Farbschlag cell; deep bands (gen >= 2, cols K/N/Q...) are 3-cell blocks
|
||||||
|
# (Name/DOB/Genotype) with NO Farbschlag — colour is derived from the genotype. So in
|
||||||
|
# deep bands we must NOT grab the next block's name or a stray health note as Farbschlag.
|
||||||
|
deep_band = gen_of(c) >= 2
|
||||||
for rr in range(r + 1, r + 4):
|
for rr in range(r + 1, r + 4):
|
||||||
cell = cells.get((c, rr))
|
cell = cells.get((c, rr))
|
||||||
if not cell:
|
if not cell:
|
||||||
@@ -221,7 +226,7 @@ def extract_stammbaum(path):
|
|||||||
elif re.search(r"\b(Zucht|Privatzucht)\b", cell) or cell.startswith("("):
|
elif re.search(r"\b(Zucht|Privatzucht)\b", cell) or cell.startswith("("):
|
||||||
breeder = cell
|
breeder = cell
|
||||||
used.add((c, rr))
|
used.add((c, rr))
|
||||||
elif not farbschlag and not re.match(r"^\*?\s?\d", cell) \
|
elif not deep_band and not farbschlag and not re.match(r"^\*?\s?\d", cell) \
|
||||||
and not looks_like_animal_name(cell):
|
and not looks_like_animal_name(cell):
|
||||||
farbschlag = cell
|
farbschlag = cell
|
||||||
used.add((c, rr))
|
used.add((c, rr))
|
||||||
|
|||||||
83
tools/import/test_extract.py
Normal file
83
tools/import/test_extract.py
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
"""Zero-dep tests for extract.py band-aware Farbschlag + name-bleed guard.
|
||||||
|
|
||||||
|
Run: python test_extract.py (exit 0 = all pass)
|
||||||
|
Covers (PEDIGREE-LINK / Julian-confirmed): deep pedigree bands (gen >= 2, cols K/N/Q...)
|
||||||
|
are Name/DOB/Genotype ONLY — no Farbschlag cell — so a stray health note or the next
|
||||||
|
block's name must NOT be captured as Farbschlag; early bands (gen 0-1) keep their real
|
||||||
|
Farbschlag. Plus the looks_like_animal_name guard (a parent name must not be a Farbschlag).
|
||||||
|
"""
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import zipfile
|
||||||
|
import tempfile
|
||||||
|
import extract as e
|
||||||
|
|
||||||
|
failed = 0
|
||||||
|
|
||||||
|
|
||||||
|
def check(name, cond):
|
||||||
|
global failed
|
||||||
|
print(("ok: " if cond else "FAIL: ") + name)
|
||||||
|
if not cond:
|
||||||
|
failed += 1
|
||||||
|
|
||||||
|
|
||||||
|
def _cell(ref, text):
|
||||||
|
return f'<c r="{ref}" t="inlineStr"><is><t>{text}</t></is></c>'
|
||||||
|
|
||||||
|
|
||||||
|
def _make_xlsx(path, cells):
|
||||||
|
"""cells: {(colLetter+row): text}. Build a minimal single-sheet xlsx (no styles)."""
|
||||||
|
rows = {}
|
||||||
|
for ref, text in cells.items():
|
||||||
|
r = int("".join(ch for ch in ref if ch.isdigit()))
|
||||||
|
rows.setdefault(r, []).append(_cell(ref, text))
|
||||||
|
body = "".join(f'<row r="{r}">{"".join(cs)}</row>' for r, cs in sorted(rows.items()))
|
||||||
|
sheet = ('<?xml version="1.0"?><worksheet xmlns="http://x"><sheetData>'
|
||||||
|
+ body + "</sheetData></worksheet>")
|
||||||
|
with zipfile.ZipFile(path, "w") as z:
|
||||||
|
z.writestr("xl/worksheets/sheet1.xml", sheet)
|
||||||
|
|
||||||
|
|
||||||
|
# --- band-aware Farbschlag ---
|
||||||
|
# col E = gen 0 (early, HAS Farbschlag); col K = col 11 = gen 2 (deep, NO Farbschlag).
|
||||||
|
tmp = os.path.join(tempfile.gettempdir(), "bandtest.xlsx")
|
||||||
|
_make_xlsx(tmp, {
|
||||||
|
# early band (E): Name / *DOB / Farbschlag / Genotype
|
||||||
|
"E10": "Chesnut",
|
||||||
|
"E11": "*13.11.2019",
|
||||||
|
"E12": "Kohlfuchsschimmel",
|
||||||
|
"E13": "aa CC DD ee GG PP spsp rere",
|
||||||
|
# deep band (K): Name / *DOB / Genotype / stray NOTE (must NOT become Farbschlag)
|
||||||
|
"K10": "DeepAnimal",
|
||||||
|
"K11": "*01.01.2020",
|
||||||
|
"K12": "aa CC DD EE GG PP spsp rere",
|
||||||
|
"K13": "DD-Tumor",
|
||||||
|
})
|
||||||
|
try:
|
||||||
|
animals = e.extract_stammbaum(tmp)
|
||||||
|
by_name = {a["name"]: a for a in animals}
|
||||||
|
check("early band keeps real Farbschlag",
|
||||||
|
by_name.get("Chesnut", {}).get("farbschlag") == "Kohlfuchsschimmel")
|
||||||
|
check("deep band has NO Farbschlag (note not grabbed)",
|
||||||
|
by_name.get("DeepAnimal", {}).get("farbschlag") == "")
|
||||||
|
check("deep-band animal still parsed (Name/DOB/Genotype)",
|
||||||
|
"DeepAnimal" in by_name and by_name["DeepAnimal"]["dob"].startswith("01.01"))
|
||||||
|
finally:
|
||||||
|
try: os.remove(tmp)
|
||||||
|
except OSError: pass
|
||||||
|
|
||||||
|
gen = e.gen_of
|
||||||
|
check("gen_of: early bands < 2 (E,H)", gen(5) < 2 and gen(8) < 2)
|
||||||
|
check("gen_of: deep bands >= 2 (K,N,Q)", gen(11) >= 2 and gen(14) >= 2)
|
||||||
|
|
||||||
|
# --- name-bleed guard (a parent name is not a Farbschlag) ---
|
||||||
|
check("v.d. name rejected", e.looks_like_animal_name("Tennessee von den Kleinen Chaoten"))
|
||||||
|
check("gen.+v.d. name rejected", e.looks_like_animal_name("Victoria Welby gen. Welby v.d. Kleinen Chaoten"))
|
||||||
|
check("real Farbschlag accepted", not e.looks_like_animal_name("Kohlfuchsschimmel"))
|
||||||
|
check("real Farbschlag accepted 2", not e.looks_like_animal_name("Orangeschimmel, hell"))
|
||||||
|
|
||||||
|
if failed:
|
||||||
|
print(f"\n{failed} test(s) FAILED")
|
||||||
|
sys.exit(1)
|
||||||
|
print("\nALL PASS")
|
||||||
Reference in New Issue
Block a user