Merge feature/gen-3e [god-QA pending]
This commit is contained in:
@@ -174,7 +174,7 @@ describe('Farbschlag catalog', () => {
|
|||||||
|
|
||||||
it('falls back to Unbekannter Farbschlag for uncatalogued genotypes', () => {
|
it('falls back to Unbekannter Farbschlag for uncatalogued genotypes', () => {
|
||||||
// E-dominant (no Fuchs/Schimmel family) + an uncatalogued cchm/dd/gg/pp combo.
|
// E-dominant (no Fuchs/Schimmel family) + an uncatalogued cchm/dd/gg/pp combo.
|
||||||
const match = farbschlagFor(fromDisplayString('aa cchmcchm dd EE gg pp spsp rere'))
|
const match = farbschlagFor(fromDisplayString('aa CC dd EE gg pp spsp rere'))
|
||||||
expect(match.unknown).toBe(true)
|
expect(match.unknown).toBe(true)
|
||||||
expect(match.name).toBe('Unbekannter Farbschlag')
|
expect(match.name).toBe('Unbekannter Farbschlag')
|
||||||
})
|
})
|
||||||
@@ -222,7 +222,7 @@ describe('Farbschlag catalog', () => {
|
|||||||
it('genotypeToFarbschlag (DATA-1 denormalization contract) returns the plain name', () => {
|
it('genotypeToFarbschlag (DATA-1 denormalization contract) returns the plain name', () => {
|
||||||
expect(genotypeToFarbschlag(wildType())).toBe('Agouti')
|
expect(genotypeToFarbschlag(wildType())).toBe('Agouti')
|
||||||
expect(genotypeToFarbschlag(fromDisplayString('aa CC DD EE GG pp spsp rere'))).toBe('Platin')
|
expect(genotypeToFarbschlag(fromDisplayString('aa CC DD EE GG pp spsp rere'))).toBe('Platin')
|
||||||
expect(genotypeToFarbschlag(fromDisplayString('aa cchmcchm dd EE gg pp spsp rere'))).toBe(
|
expect(genotypeToFarbschlag(fromDisplayString('aa CC dd EE gg pp spsp rere'))).toBe(
|
||||||
'Unbekannter Farbschlag',
|
'Unbekannter Farbschlag',
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
@@ -399,3 +399,33 @@ describe('GEN-3d: dominance tiebreak for unknown loci', () => {
|
|||||||
)
|
)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe('GEN-3e: C-locus colourpoint naming', () => {
|
||||||
|
const name = (s: string) => genotypeToFarbschlag(fromDisplayString(s))
|
||||||
|
|
||||||
|
it('A- + cchm/cchm -> CP-<base colour> (CP-Silberagouti example)', () => {
|
||||||
|
expect(name('AA cchmcchm DD EE gg PP spsp rere')).toBe('CP-Silberagouti')
|
||||||
|
expect(name('AA cchmcchm DD EE GG PP spsp rere')).toBe('CP-Agouti')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('A- + cchm/ch -> CP-<base colour>', () => {
|
||||||
|
expect(name('AA cchmch DD EE GG PP spsp rere')).toBe('CP-Agouti')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('aa fixed colourpoint names (Marder/Siam/Zobel/Zobel-Hell)', () => {
|
||||||
|
expect(name('aa cchmcchm DD EE GG PP spsp rere')).toBe('Marder')
|
||||||
|
expect(name('aa cchmch DD EE GG PP spsp rere')).toBe('Siam')
|
||||||
|
expect(name('aa cchmcchm DD EE gg PP spsp rere')).toBe('Zobel')
|
||||||
|
expect(name('aa cchmch DD EE gg PP spsp rere')).toBe('Zobel-Hell')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('chch stays Hermelin (aa) / Himalaya (A-); full C => no CP', () => {
|
||||||
|
expect(name('aa chch DD EE GG PP spsp rere')).toBe('Hermelin')
|
||||||
|
expect(name('AA chch DD EE GG PP spsp rere')).toBe('Himalaya')
|
||||||
|
expect(name('AA CC DD EE GG PP spsp rere')).toBe('Agouti') // one full C -> no colourpoint
|
||||||
|
})
|
||||||
|
|
||||||
|
it('colourpoint composes with the Schecke modifier', () => {
|
||||||
|
expect(name('aa cchmcchm DD EE GG PP Spsp rere')).toBe('Marder Schecke')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|||||||
@@ -191,25 +191,61 @@ function eFamily(g: Genotype): string | null {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** Resolve a genotype to its German Farbschlag (with Schecke/Rex modifiers). */
|
/** Resolve a genotype to its German Farbschlag (with Schecke/Rex modifiers). */
|
||||||
export function farbschlagFor(g: Genotype): FarbschlagMatch {
|
/** Resolve an allele pair to concrete alleles, defaulting unknown to wild-type. */
|
||||||
|
function resolvedPair(g: Genotype, locus: LocusKey): [string, string] {
|
||||||
|
const alleles = LOCI[locus].alleles
|
||||||
|
const isMarker = locus === 'Sp' || locus === 'Re' || locus === 'Sls'
|
||||||
|
const fallback = isMarker ? alleles[alleles.length - 1] : alleles[0]
|
||||||
|
const [x, y] = g[locus].map((a) => (a === WILDCARD ? fallback : a))
|
||||||
|
return [x, y]
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Base colour name (no modifiers, no colourpoint prefix), via E-family + matches. */
|
||||||
|
function baseColourFor(g: Genotype): string | null {
|
||||||
const family = eFamily(g)
|
const family = eFamily(g)
|
||||||
// When the animal is a Fuchs/Schimmel-family genotype, only entries that
|
|
||||||
// explicitly pin the E locus may name it (so e.g. eef can't masquerade as a
|
|
||||||
// C-series white via wildcard C); otherwise fall back to the family name.
|
|
||||||
const base = family
|
const base = family
|
||||||
? (BASE_COLORS.find((e) => e.tokens.E !== undefined && matches(g, e)) ?? null)
|
? (BASE_COLORS.find((e) => e.tokens.E !== undefined && matches(g, e)) ?? null)
|
||||||
: (BASE_COLORS.find((e) => matches(g, e)) ?? null)
|
: (BASE_COLORS.find((e) => matches(g, e)) ?? null)
|
||||||
|
return base?.name ?? family
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GEN-3e: the C-locus colourpoint NAMING transform (breeder-authoritative).
|
||||||
|
* Returns the colourpoint name, or null when it doesn't apply (full C present,
|
||||||
|
* or chch — which the base matcher names Hermelin/Himalaya, preserving both).
|
||||||
|
* aa cchm/cchm -> Marder | aa cchm/ch -> Siam
|
||||||
|
* aa cchm/cchm gg -> Zobel | aa cchm/ch gg -> Zobel-Hell
|
||||||
|
* A- cchm/cchm | cchm/ch -> CP-<base colour> (base computed as if C were full)
|
||||||
|
*/
|
||||||
|
function colourpointName(g: Genotype): string | null {
|
||||||
|
const c = resolvedPair(g, 'C')
|
||||||
|
if (c.includes('C')) return null // a full C allele => full colour, no CP
|
||||||
|
if (c[0] === 'ch' && c[1] === 'ch') return null // chch -> base matcher (Hermelin/Himalaya)
|
||||||
|
// Remaining: cchm/cchm or cchm/ch (colourpoint, no full C, not chch).
|
||||||
|
const bothCchm = c[0] === 'cchm' && c[1] === 'cchm'
|
||||||
|
const agouti = resolvedPair(g, 'A').includes('A')
|
||||||
|
if (!agouti) {
|
||||||
|
const [g1, g2] = resolvedPair(g, 'G')
|
||||||
|
const grey = g1 === 'g' && g2 === 'g'
|
||||||
|
if (grey) return bothCchm ? 'Zobel' : 'Zobel-Hell'
|
||||||
|
return bothCchm ? 'Marder' : 'Siam'
|
||||||
|
}
|
||||||
|
// A- colourpoint -> CP-<base colour>, base as if C were full.
|
||||||
|
const base = baseColourFor(makeGenotype({ ...g, C: ['C', 'C'] }))
|
||||||
|
return base ? `CP-${base}` : null
|
||||||
|
}
|
||||||
|
|
||||||
|
export function farbschlagFor(g: Genotype): FarbschlagMatch {
|
||||||
const modifiers: string[] = []
|
const modifiers: string[] = []
|
||||||
if (locusToken(g, 'Sp') === 'Sp') modifiers.push('Schecke')
|
if (locusToken(g, 'Sp') === 'Sp') modifiers.push('Schecke')
|
||||||
if (locusToken(g, 'Re') === 'Re') modifiers.push('Rex')
|
if (locusToken(g, 'Re') === 'Re') modifiers.push('Rex')
|
||||||
|
|
||||||
const baseName = base?.name ?? family
|
const baseName = colourpointName(g) ?? baseColourFor(g)
|
||||||
if (!baseName) {
|
if (!baseName) {
|
||||||
return { name: UNKNOWN_FARBSCHLAG, base: null, unknown: true }
|
return { name: UNKNOWN_FARBSCHLAG, base: null, unknown: true }
|
||||||
}
|
}
|
||||||
const name = [baseName, ...modifiers].join(' ')
|
const name = [baseName, ...modifiers].join(' ')
|
||||||
return { name, base, unknown: false }
|
return { name, base: null, unknown: false }
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
Reference in New Issue
Block a user