fix(import): Farbschlag aus Genotyp ableiten + Mamta-Eltern — Ticket-Triage
genotype.py: Python-Port von genotypeToFarbschlag (0 Abw. über 3402 Genotypen). resolve_color_and_genotype: bei vorhandenem Genotyp gewinnt der berechnete Farbschlag (Goldfuchs≠Gold, Dilute Agouti/Anthrazit, Blaufuchs statt -schimmel bei (schimmel), spsp statt Schecke). Mamta Mini: Ee + Eltern Geely×Gaida am Wurf. Regressionstests je Fall. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -5,6 +5,8 @@ import uuid
|
||||
import sys
|
||||
from datetime import datetime
|
||||
|
||||
import genotype as gt
|
||||
|
||||
# Prevent encoding crashes on Windows consoles when printing unicode
|
||||
if sys.platform.startswith('win'):
|
||||
try:
|
||||
@@ -422,33 +424,39 @@ def get_dedup_name_key(name):
|
||||
return "".join(c for c in n if c.isalnum())
|
||||
|
||||
def clean_color_name(c_desc):
|
||||
"""Normalise a free-text colour label to a catalog key + Schecke flag.
|
||||
|
||||
Returns (clean_name, is_schecke). A PARENTHETICAL „(schimmel)" is NOT a
|
||||
definitive Schimmel — the breeder writes it to mean „könnte sich später als
|
||||
Schimmel entpuppen" (ticket e22764aa). So we STRIP the „(…)" instead of
|
||||
folding it into the name (which used to turn „Blaufuchs(schimmel)" into the
|
||||
wrong „blaufuchsschimmel"); the still-uncertain Schimmel-modifier is carried
|
||||
by the genotype (ee[-] = Fuchs, Schimmel unknown), not the colour label.
|
||||
"""
|
||||
if not c_desc:
|
||||
return "", False
|
||||
|
||||
|
||||
# Lowercase and strip
|
||||
c = c_desc.lower().strip()
|
||||
|
||||
|
||||
# Check for Schecke
|
||||
is_schecke = False
|
||||
if re.search(r'\bsp\b|\bsp\d|\bsp[*(²³]|\bspotted|\bschecke|[- ]sp\b|\w+sp\b', c):
|
||||
is_schecke = True
|
||||
|
||||
# Standardize parentheticals for schimmel
|
||||
c = c.replace("(schimmel)", "schimmel")
|
||||
c = c.replace("(schimmel-hell)", "schimmel hell")
|
||||
c = c.replace("(schimmel hell)", "schimmel hell")
|
||||
|
||||
|
||||
# Strip schecke/sp markers and any trailing text starting from sp
|
||||
c = re.sub(r'\([- ]?sp(otted)?\)', '', c) # handles (-sp)
|
||||
c = re.sub(r'[- ]?sp(otted)?\b.*', '', c) # handles -sp(k), -sp*(k), -sp, etc.
|
||||
c = re.sub(r'[- ]?schecke\b.*', '', c)
|
||||
c = re.sub(r'[- ]?spotted\b.*', '', c)
|
||||
|
||||
# Strip any other parentheticals, symbols, or trailing stars/numbers
|
||||
|
||||
# Strip any other parentheticals (incl. „(schimmel)" = „möglich/unbestimmt"),
|
||||
# symbols, or trailing stars/numbers. The parenthetical is deliberately NOT
|
||||
# promoted to a definitive part of the colour name (ticket e22764aa).
|
||||
c = re.sub(r'\s*\(.*?\)\s*', ' ', c)
|
||||
c = re.sub(r'[²³*]', '', c)
|
||||
c = c.strip()
|
||||
|
||||
|
||||
# Mapping table for abbreviations, typos, and specific combinations
|
||||
mapping = {
|
||||
"antra": "anthrazit",
|
||||
@@ -476,27 +484,87 @@ def clean_color_name(c_desc):
|
||||
|
||||
return c, is_schecke
|
||||
|
||||
def resolve_color_and_genotype(color_val, existing_genotype, variety_map, variety_genotypes):
|
||||
if not color_val:
|
||||
return None, existing_genotype
|
||||
color_str = str(color_val).strip()
|
||||
clean_name, is_schecke = clean_color_name(color_str)
|
||||
# Match color in variety_map
|
||||
color_variety_id = None
|
||||
def _match_color_label(clean_name, variety_map):
|
||||
"""Map a cleaned colour label to a ColorVariety id (text-only path).
|
||||
|
||||
Exact name wins; otherwise pick the LONGEST/most-specific substring match
|
||||
(ticket 3f5942a2 — the old code broke on the FIRST substring hit, so „Goldfuchs"
|
||||
matched the shorter „Gold" first). Among substring candidates the longest seed
|
||||
name wins, then the longest clean_name overlap; ties broken deterministically.
|
||||
"""
|
||||
if not clean_name:
|
||||
return None
|
||||
if clean_name in variety_map:
|
||||
color_variety_id = variety_map[clean_name]
|
||||
else:
|
||||
for seed_name, seed_id in variety_map.items():
|
||||
if seed_name in clean_name or clean_name in seed_name:
|
||||
color_variety_id = seed_id
|
||||
break
|
||||
# Update genotype if it's a Schecke
|
||||
return variety_map[clean_name]
|
||||
candidates = []
|
||||
for seed_name, seed_id in variety_map.items():
|
||||
if not seed_name:
|
||||
continue
|
||||
if seed_name in clean_name or clean_name in seed_name:
|
||||
# Specificity score: prefer the longer seed name (more specific),
|
||||
# then the closeness of lengths so „goldfuchs" beats „gold" for the
|
||||
# label „goldfuchs".
|
||||
candidates.append((len(seed_name), -abs(len(seed_name) - len(clean_name)),
|
||||
seed_name, seed_id))
|
||||
if not candidates:
|
||||
return None
|
||||
candidates.sort(reverse=True)
|
||||
return candidates[0][3]
|
||||
|
||||
|
||||
def resolve_color_and_genotype(color_val, existing_genotype, variety_map, variety_genotypes):
|
||||
"""Resolve a gerbil's stored ColorVariety id + genotype.
|
||||
|
||||
GENOTYPE WINS (ticket cluster genetics-farbschlag): when a parseable genotype
|
||||
is present and the genetics engine (genotype.genotype_to_farbschlag — a faithful
|
||||
Python mirror of catalog.ts) computes a KNOWN catalog variety, that variety is
|
||||
authoritative for colorVarietyId. The free-text colour label is only a fallback
|
||||
(no genotype, or genotype resolves to „Unbekannt"). This fixes the imports where
|
||||
the source label ignored a locus (dd → „Agouti" instead of „Dilute Agouti",
|
||||
ee → „Gold" instead of „Goldfuchs", parenthetical „(schimmel)", …).
|
||||
|
||||
Returns (color_variety_id, genotype). `genotype` is the (possibly Schecke-
|
||||
annotated) genotype STRING — never silently flips an explicit spsp to Spsp.
|
||||
"""
|
||||
if not color_val and not existing_genotype:
|
||||
return None, existing_genotype
|
||||
|
||||
clean_name, is_schecke = clean_color_name(str(color_val).strip()) if color_val else ("", False)
|
||||
|
||||
# 1) Genotype-derived variety (authoritative when it resolves to a known name).
|
||||
# GUARD (VORSICHTIG): only trust the genotype when it parsed CLEANLY enough to
|
||||
# decide a colour — both the C and E loci must be mapped. The breeder sometimes
|
||||
# writes the genotype in the COMPACT catalog notation („cchmcchm", „efef",
|
||||
# „chch") which this parser leaves UNMAPPED (it expects the bracketed „c[chm]"
|
||||
# form); a dropped C/E locus would silently read as wild-type and mis-recolour
|
||||
# an otherwise-correct animal (e.g. Marder→Schwarz, Orangeschimmel→Agouti). When
|
||||
# the parse is incomplete we keep the source text label instead.
|
||||
color_variety_id = None
|
||||
geno_name = None
|
||||
if existing_genotype:
|
||||
try:
|
||||
mapped = gt.parse(existing_genotype).get("mapped8locus") or {}
|
||||
except Exception:
|
||||
mapped = {}
|
||||
if mapped.get("C") and mapped.get("E"):
|
||||
fs = gt.genotype_to_farbschlag(mapped)
|
||||
if fs and fs != gt.UNKNOWN_FARBSCHLAG:
|
||||
geno_name = fs
|
||||
color_variety_id = variety_map.get(fs.strip().lower())
|
||||
|
||||
# 2) Fall back to the text label when the genotype gave nothing usable.
|
||||
if not color_variety_id:
|
||||
color_variety_id = _match_color_label(clean_name, variety_map)
|
||||
|
||||
# Update genotype if the LABEL says Schecke — but never override an explicit
|
||||
# Sp-locus already present in the source genotype (ticket e09d6f22: a source
|
||||
# „spsp" must NOT be flipped to „Spsp" just because the label looked scheckig;
|
||||
# the source genotype is authoritative for the Sp-locus). Only ADD Spsp when
|
||||
# the genotype carries no Sp token at all.
|
||||
genotype = existing_genotype
|
||||
if is_schecke:
|
||||
if genotype:
|
||||
if "spsp" in genotype:
|
||||
genotype = genotype.replace("spsp", "Spsp")
|
||||
elif "Spsp" not in genotype and "Sp" not in genotype:
|
||||
if "Sp" not in genotype and "sp" not in genotype:
|
||||
genotype = f"{genotype} Spsp".strip()
|
||||
else:
|
||||
canonical = variety_genotypes.get(color_variety_id)
|
||||
|
||||
Reference in New Issue
Block a user