Compare commits
2 Commits
feature/ge
...
0a1e63bec6
| Author | SHA1 | Date | |
|---|---|---|---|
| 0a1e63bec6 | |||
| 5b30407257 |
@@ -507,6 +507,44 @@ namespace GerbilManager.Tests
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task UndatedLitters_counted_as_WithoutDate_not_Created()
|
||||||
|
{
|
||||||
|
// COUNTER-BUG regression: litters with no parseable date must go into WithoutDate,
|
||||||
|
// NOT Created. On re-import, Created must be 0 (not 31-phantom-phantom-phantom...).
|
||||||
|
var dir = Path.Combine(Path.GetTempPath(), "undated-" + Guid.NewGuid().ToString("N"));
|
||||||
|
Directory.CreateDirectory(dir);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// One dated litter, one undated litter (blank date field)
|
||||||
|
File.WriteAllText(Path.Combine(dir, "litters.json"), """
|
||||||
|
[
|
||||||
|
{"id":"L-dated","litterId":"A","date":"01.02.2020","damName":"Mutter","sireName":"Vater","totalBorn":3,"zuchtnummer":"","note":""},
|
||||||
|
{"id":"L-undated","litterId":"B","date":"","damName":"Mutter","sireName":"Vater","totalBorn":0,"zuchtnummer":"","note":""}
|
||||||
|
]
|
||||||
|
""");
|
||||||
|
File.WriteAllText(Path.Combine(dir, "animals.json"), "[]");
|
||||||
|
|
||||||
|
using var db = NewDb();
|
||||||
|
|
||||||
|
// First run
|
||||||
|
var r1 = await new ImportService(db, dir, dir).RunAsync(execute: true);
|
||||||
|
Assert.Equal(2, r1.Litters.InSource);
|
||||||
|
Assert.Equal(1, r1.Litters.Created); // only the dated one
|
||||||
|
Assert.Equal(0, r1.Litters.AlreadyImported);
|
||||||
|
Assert.Equal(1, r1.Litters.WithoutDate); // the undated one
|
||||||
|
Assert.Equal(1, await db.Litters.CountAsync()); // only 1 persisted
|
||||||
|
|
||||||
|
// Second run (re-import): dated litter is now existing, undated still WithoutDate
|
||||||
|
var r2 = await new ImportService(db, dir, dir).RunAsync(execute: true);
|
||||||
|
Assert.Equal(0, r2.Litters.Created); // no phantom "created"
|
||||||
|
Assert.Equal(1, r2.Litters.AlreadyImported);
|
||||||
|
Assert.Equal(1, r2.Litters.WithoutDate);
|
||||||
|
Assert.Equal(1, await db.Litters.CountAsync()); // still only 1 row
|
||||||
|
}
|
||||||
|
finally { try { Directory.Delete(dir, recursive: true); } catch { } }
|
||||||
|
}
|
||||||
|
|
||||||
[Theory]
|
[Theory]
|
||||||
[InlineData("01.02.2020", 2020, 2, 1)]
|
[InlineData("01.02.2020", 2020, 2, 1)]
|
||||||
[InlineData("5.3.21", 2021, 3, 5)]
|
[InlineData("5.3.21", 2021, 3, 5)]
|
||||||
|
|||||||
@@ -87,7 +87,7 @@ namespace GerbilManagerWebAPI.Import
|
|||||||
|
|
||||||
public sealed record LitterSummary(int InSource, int Created, int AlreadyImported,
|
public sealed record LitterSummary(int InSource, int Created, int AlreadyImported,
|
||||||
int DerivedFromChart = 0, int DerivedSkipped = 0, int ParentFksDropped = 0,
|
int DerivedFromChart = 0, int DerivedSkipped = 0, int ParentFksDropped = 0,
|
||||||
int ParentFksBackfilled = 0);
|
int ParentFksBackfilled = 0, int WithoutDate = 0);
|
||||||
|
|
||||||
public sealed record AnimalSummary(
|
public sealed record AnimalSummary(
|
||||||
int InSource,
|
int InSource,
|
||||||
|
|||||||
@@ -94,11 +94,17 @@ namespace GerbilManagerWebAPI.Import
|
|||||||
var damNames = litters.Select(l => Normalize(StripZucht(l.DamName))).Where(s => s.Length > 0).ToHashSet();
|
var damNames = litters.Select(l => Normalize(StripZucht(l.DamName))).Where(s => s.Length > 0).ToHashSet();
|
||||||
|
|
||||||
// ---- litters: create map source.id -> Litter (for high-confidence animal links) ----
|
// ---- litters: create map source.id -> Litter (for high-confidence animal links) ----
|
||||||
int littersCreated = 0, littersExisting = 0;
|
// COUNTER-BUG FIX: undated litters (31 in the Wurfchronik) have no parseable date,
|
||||||
|
// so their existingLitterKeySet key was always "" → they were always counted as
|
||||||
|
// "created" even though the execute block skipped them (date is DateOnly d = false).
|
||||||
|
// Fix: skip undated litters early — they can never be created or linked to animals.
|
||||||
|
int littersCreated = 0, littersExisting = 0, littersWithoutDate = 0;
|
||||||
var litterIdMap = new Dictionary<string, Guid>(); // source litter id -> Litter.Id
|
var litterIdMap = new Dictionary<string, Guid>(); // source litter id -> Litter.Id
|
||||||
foreach (var sl in litters)
|
foreach (var sl in litters)
|
||||||
{
|
{
|
||||||
var date = ParseDate(sl.Date);
|
var date = ParseDate(sl.Date);
|
||||||
|
if (date is null) { littersWithoutDate++; continue; } // undated: skip entirely
|
||||||
|
|
||||||
var name = $"Wurf {sl.LitterId}".Trim();
|
var name = $"Wurf {sl.LitterId}".Trim();
|
||||||
var key = $"{name}|{date:yyyy-MM-dd}";
|
var key = $"{name}|{date:yyyy-MM-dd}";
|
||||||
if (existingLitterKeySet.Contains(key)) { littersExisting++; continue; }
|
if (existingLitterKeySet.Contains(key)) { littersExisting++; continue; }
|
||||||
@@ -106,19 +112,19 @@ namespace GerbilManagerWebAPI.Import
|
|||||||
var id = Guid.NewGuid();
|
var id = Guid.NewGuid();
|
||||||
litterIdMap[sl.Id] = id;
|
litterIdMap[sl.Id] = id;
|
||||||
littersCreated++;
|
littersCreated++;
|
||||||
if (execute && date is DateOnly d)
|
if (execute)
|
||||||
{
|
{
|
||||||
_db.Litters.Add(new Litter
|
_db.Litters.Add(new Litter
|
||||||
{
|
{
|
||||||
Id = id,
|
Id = id,
|
||||||
Name = name,
|
Name = name,
|
||||||
Date = d,
|
Date = date.Value,
|
||||||
TotalBorn = sl.TotalBorn,
|
TotalBorn = sl.TotalBorn,
|
||||||
Notes = string.IsNullOrWhiteSpace(sl.Note) ? null : sl.Note,
|
Notes = string.IsNullOrWhiteSpace(sl.Note) ? null : sl.Note,
|
||||||
PairingCode = string.IsNullOrWhiteSpace(sl.Zuchtnummer) ? null : sl.Zuchtnummer,
|
PairingCode = string.IsNullOrWhiteSpace(sl.Zuchtnummer) ? null : sl.Zuchtnummer,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (samples.Count < 8 && date is not null)
|
if (samples.Count < 8)
|
||||||
samples.Add($"Wurf: {name} ({sl.Date}) — {sl.DamName} × {sl.SireName}");
|
samples.Add($"Wurf: {name} ({sl.Date}) — {sl.DamName} × {sl.SireName}");
|
||||||
}
|
}
|
||||||
if (execute) await _db.SaveChangesAsync();
|
if (execute) await _db.SaveChangesAsync();
|
||||||
@@ -454,6 +460,8 @@ namespace GerbilManagerWebAPI.Import
|
|||||||
if (execute && parentFksBackfilled > 0) await _db.SaveChangesAsync();
|
if (execute && parentFksBackfilled > 0) await _db.SaveChangesAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (littersWithoutDate > 0)
|
||||||
|
notes.Add($"Würfe ohne Datum: {littersWithoutDate} Wurfchronik-Einträge ohne parsbares Geburtsdatum übersprungen (weder erstellt noch verknüpft).");
|
||||||
notes.Add("Quarantäne (kein Import): Konflikte + Stubs ohne Geburtsdatum + unsichere Wurf-Zuordnungen — warten auf die Prüfung durch die Züchterin.");
|
notes.Add("Quarantäne (kein Import): Konflikte + Stubs ohne Geburtsdatum + unsichere Wurf-Zuordnungen — warten auf die Prüfung durch die Züchterin.");
|
||||||
if (parentLinksAdded > 0)
|
if (parentLinksAdded > 0)
|
||||||
notes.Add($"Stammbaum-Diagramm: {parentLinksAdded} Tiere über Eltern-Verknüpfung einem (abgeleiteten) Wurf zugeordnet ({derivedLitters} abgeleitete Würfe).");
|
notes.Add($"Stammbaum-Diagramm: {parentLinksAdded} Tiere über Eltern-Verknüpfung einem (abgeleiteten) Wurf zugeordnet ({derivedLitters} abgeleitete Würfe).");
|
||||||
@@ -468,7 +476,7 @@ namespace GerbilManagerWebAPI.Import
|
|||||||
|
|
||||||
return new ImportReport(
|
return new ImportReport(
|
||||||
Executed: execute,
|
Executed: execute,
|
||||||
Litters: new LitterSummary(litters.Count, littersCreated, littersExisting, derivedLitters, derivedLittersSkipped, litterParentFksDropped, parentFksBackfilled),
|
Litters: new LitterSummary(litters.Count, littersCreated, littersExisting, derivedLitters, derivedLittersSkipped, litterParentFksDropped, parentFksBackfilled, littersWithoutDate),
|
||||||
Animals: new AnimalSummary(
|
Animals: new AnimalSummary(
|
||||||
animals.Count, animalsCreated, linked, fbMatched, fbUnmatched, animalsExisting,
|
animals.Count, animalsCreated, linked, fbMatched, fbUnmatched, animalsExisting,
|
||||||
new QuarantineSummary(conflicts, stubs, dateOnly, ambiguous, conflicts + stubs),
|
new QuarantineSummary(conflicts, stubs, dateOnly, ambiguous, conflicts + stubs),
|
||||||
|
|||||||
@@ -179,11 +179,10 @@ describe('Farbschlag catalog', () => {
|
|||||||
expect(match.name).toBe('Unbekannter Farbschlag')
|
expect(match.name).toBe('Unbekannter Farbschlag')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('has the expected catalogue coverage (GEN-3g: 66 after adding CP-*-Hell het variants)', () => {
|
it('has the expected catalogue coverage (GEN-3f: 61 after cchm CP reconciliation)', () => {
|
||||||
// GEN-3f: 73 -> 61 (cchm CP reconciliation).
|
// GEN-3f collapsed the 24 portal cchm colourpoint rows to 12 breeder-named
|
||||||
// GEN-3g: +5 het variants (CP-Agouti/Silberagouti/Algierfuchs/Polarfuchs/Orangeschimmel -Hell),
|
// varieties (Marder/Siam/Zobel/Zobel-Hell + CP-<base>), so 73 -> 61.
|
||||||
// giving 61 + 5 = 66. CP-Fuchs-Hell was already counted.
|
expect(CATALOG_SIZE).toBe(61)
|
||||||
expect(CATALOG_SIZE).toBe(66)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
it('frozen contract names round-trip to themselves (DB-key guard)', () => {
|
it('frozen contract names round-trip to themselves (DB-key guard)', () => {
|
||||||
@@ -415,12 +414,8 @@ describe('GEN-3e: C-locus colourpoint naming', () => {
|
|||||||
expect(name('AA cchmcchm DD EE GG PP spsp rere')).toBe('CP-Agouti')
|
expect(name('AA cchmcchm DD EE GG PP spsp rere')).toBe('CP-Agouti')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('A- + cchm/ch -> CP-<base colour>-Hell (GEN-3g: het gets -Hell suffix)', () => {
|
it('A- + cchm/ch -> CP-<base colour>', () => {
|
||||||
expect(name('AA cchmch DD EE GG PP spsp rere')).toBe('CP-Agouti-Hell')
|
expect(name('AA cchmch DD EE GG PP spsp rere')).toBe('CP-Agouti')
|
||||||
expect(name('AA cchmch DD EE gg PP spsp rere')).toBe('CP-Silberagouti-Hell')
|
|
||||||
expect(name('AA cchmch DD ee GG PP spsp rere')).toBe('CP-Algierfuchs-Hell')
|
|
||||||
expect(name('AA cchmch DD ee gg PP spsp rere')).toBe('CP-Polarfuchs-Hell')
|
|
||||||
expect(name('AA cchmch dd ee GG PP spsp rere')).toBe('CP-Fuchs-Hell')
|
|
||||||
})
|
})
|
||||||
|
|
||||||
it('aa fixed colourpoint names (Marder/Siam/Zobel/Zobel-Hell)', () => {
|
it('aa fixed colourpoint names (Marder/Siam/Zobel/Zobel-Hell)', () => {
|
||||||
@@ -476,50 +471,10 @@ describe('GEN-3f: CP catalog reconciled to the breeder CP- naming (matches her l
|
|||||||
expect(name('AA cchmcchm DD efef GG PP spsp rere')).toBe('CP-Orangeschimmel')
|
expect(name('AA cchmcchm DD efef GG PP spsp rere')).toBe('CP-Orangeschimmel')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('het cchm/ch colourpoints (Siam, Zobel-Hell, CP-*-Hell) resolve to their own names', () => {
|
it('het cchm/ch colourpoints (Siam, Zobel-Hell) resolve to their own names', () => {
|
||||||
const siam = BASE_COLORS.find((e) => e.name === 'Siam')!
|
const siam = BASE_COLORS.find((e) => e.name === 'Siam')!
|
||||||
const zh = BASE_COLORS.find((e) => e.name === 'Zobel-Hell')!
|
const zh = BASE_COLORS.find((e) => e.name === 'Zobel-Hell')!
|
||||||
const cpah = BASE_COLORS.find((e) => e.name === 'CP-Agouti-Hell')!
|
|
||||||
const cpfh = BASE_COLORS.find((e) => e.name === 'CP-Fuchs-Hell')!
|
|
||||||
expect(genotypeToFarbschlag(representativeGenotype(siam))).toBe('Siam')
|
expect(genotypeToFarbschlag(representativeGenotype(siam))).toBe('Siam')
|
||||||
expect(genotypeToFarbschlag(representativeGenotype(zh))).toBe('Zobel-Hell')
|
expect(genotypeToFarbschlag(representativeGenotype(zh))).toBe('Zobel-Hell')
|
||||||
expect(genotypeToFarbschlag(representativeGenotype(cpah))).toBe('CP-Agouti-Hell')
|
|
||||||
expect(genotypeToFarbschlag(representativeGenotype(cpfh))).toBe('CP-Fuchs-Hell')
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('GEN-3g: "-Hell" in variety name == cchm/ch het; hom == cchm/cchm', () => {
|
|
||||||
const name = (s: string) => genotypeToFarbschlag(fromDisplayString(s))
|
|
||||||
const has = (n: string) => BASE_COLORS.some((e) => e.name === n)
|
|
||||||
|
|
||||||
it('all new -Hell het entries exist in catalog', () => {
|
|
||||||
for (const n of [
|
|
||||||
'CP-Agouti-Hell', 'CP-Silberagouti-Hell', 'CP-Algierfuchs-Hell',
|
|
||||||
'CP-Polarfuchs-Hell', 'CP-Orangeschimmel-Hell',
|
|
||||||
]) {
|
|
||||||
expect(has(n)).toBe(true)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
it('engine correctly maps het (cchm/ch) to -Hell suffix for all A- bases', () => {
|
|
||||||
expect(name('AA cchmch DD EE GG PP spsp rere')).toBe('CP-Agouti-Hell')
|
|
||||||
expect(name('AA cchmch DD EE gg PP spsp rere')).toBe('CP-Silberagouti-Hell')
|
|
||||||
expect(name('AA cchmch DD ee GG PP spsp rere')).toBe('CP-Algierfuchs-Hell')
|
|
||||||
expect(name('AA cchmch DD ee gg PP spsp rere')).toBe('CP-Polarfuchs-Hell')
|
|
||||||
expect(name('AA cchmch dd ee GG PP spsp rere')).toBe('CP-Fuchs-Hell')
|
|
||||||
expect(name('AA cchmch DD efef GG PP spsp rere')).toBe('CP-Orangeschimmel-Hell')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('hom (cchm/cchm) still maps without -Hell suffix', () => {
|
|
||||||
expect(name('AA cchmcchm DD EE GG PP spsp rere')).toBe('CP-Agouti')
|
|
||||||
expect(name('AA cchmcchm DD EE gg PP spsp rere')).toBe('CP-Silberagouti')
|
|
||||||
expect(name('AA cchmcchm DD efef GG PP spsp rere')).toBe('CP-Orangeschimmel')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('aa non-agouti branch is unchanged (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')
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -107,29 +107,27 @@ export const BASE_COLORS: readonly FarbschlagEntry[] = [
|
|||||||
{ name: 'Topas dd', tokens: { A: 'A', C: 'C', D: 'd', E: 'E', G: 'G', P: 'p' }, image: 'topas-dd.jpg' },
|
{ name: 'Topas dd', tokens: { A: 'A', C: 'C', D: 'd', E: 'E', G: 'G', P: 'p' }, image: 'topas-dd.jpg' },
|
||||||
{ name: 'Blaufuchs dd', tokens: { A: 'a', C: 'C', D: 'd', E: 'e', G: 'g', P: 'p' }, image: 'blaufuchs-dd.jpg' },
|
{ name: 'Blaufuchs dd', tokens: { A: 'a', C: 'C', D: 'd', E: 'e', G: 'g', P: 'p' }, image: 'blaufuchs-dd.jpg' },
|
||||||
|
|
||||||
// ── GEN-3f/3g: c^chm colourpoint varieties ──
|
// ── GEN-3f: c^chm colourpoint varieties, reconciled to the breeder's CP- naming ──
|
||||||
// GEN-3f: aa points = marten/sable group (Marder/Siam, +gg Zobel/Zobel-Hell).
|
// The merged GEN-3e colourpointName() rule is authoritative: aa points are the
|
||||||
// GEN-3g (breeder rule): '-Hell' == cchm/ch het; no '-Hell' == cchm/cchm hom.
|
// marten/sable group (Marder/Siam, +gg Zobel/Zobel-Hell — E and D irrelevant);
|
||||||
// A- points: hom -> 'CP-<base>', het -> 'CP-<base>-Hell' (colourpointName()).
|
// A- points take the 'CP-<base colour>' prefix and the '-Hell' shade variants
|
||||||
// CP-Fuchs is a Sammelbegriff (unknown loci); its -Hell het = CP-Fuchs-Hell.
|
// collapse (other loci irrelevant for the CP prefix). These names == the strings
|
||||||
// CP-Blaufuchs (D:d, G:g) still resolves engine-side to 'CP-Fuchs' (dd/gg
|
// in her live data (god: extract animals.json) so the re-import name-matches and
|
||||||
// fox CP has no dedicated base entry); kept for import name-match + hand-pick.
|
// the Farbschlag mismatch hint stops. The het cchm/ch points (Siam, Zobel-Hell,
|
||||||
|
// CP-Fuchs-Hell) use the 'cchm/ch' pair token. The agouti fox/dilute points
|
||||||
|
// (CP-Fuchs/CP-Blaufuchs) resolve through the engine's E-family fallback to
|
||||||
|
// 'CP-Fuchs'; their distinct dropdown names remain for hand-pick + import match.
|
||||||
{ name: 'Marder', tokens: { A: 'a', C: 'cchm', D: 'D', E: 'E', G: 'G', P: 'P' }, image: 'marder.JPG' },
|
{ name: 'Marder', tokens: { A: 'a', C: 'cchm', D: 'D', E: 'E', G: 'G', P: 'P' }, image: 'marder.JPG' },
|
||||||
{ name: 'Siam', tokens: { A: 'a', C: 'cchm/ch', D: 'D', E: 'E', G: 'G', P: 'P' }, image: 'siam-marder-hell.JPG' },
|
{ name: 'Siam', tokens: { A: 'a', C: 'cchm/ch', D: 'D', E: 'E', G: 'G', P: 'P' }, image: 'siam-marder-hell.JPG' },
|
||||||
{ name: 'Zobel-Hell', tokens: { A: 'a', C: 'cchm/ch', D: 'D', E: 'E', G: 'g', P: 'P' }, image: 'zobel-hell.jpg' },
|
{ name: 'Zobel-Hell', tokens: { A: 'a', C: 'cchm/ch', D: 'D', E: 'E', G: 'g', P: 'P' }, image: 'zobel-hell.jpg' },
|
||||||
{ name: 'CP-Agouti', tokens: { A: 'A', C: 'cchm', D: 'D', E: 'E', G: 'G', P: 'P' }, image: 'agouti-cp.jpg' },
|
{ name: 'CP-Agouti', tokens: { A: 'A', C: 'cchm', D: 'D', E: 'E', G: 'G', P: 'P' }, image: 'agouti-cp.jpg' },
|
||||||
{ name: 'CP-Agouti-Hell', tokens: { A: 'A', C: 'cchm/ch', D: 'D', E: 'E', G: 'G', P: 'P' } },
|
|
||||||
{ name: 'CP-Silberagouti', tokens: { A: 'A', C: 'cchm', D: 'D', E: 'E', G: 'g', P: 'P' }, image: 'silberagouti-cp.JPG' },
|
{ name: 'CP-Silberagouti', tokens: { A: 'A', C: 'cchm', D: 'D', E: 'E', G: 'g', P: 'P' }, image: 'silberagouti-cp.JPG' },
|
||||||
{ name: 'CP-Silberagouti-Hell', tokens: { A: 'A', C: 'cchm/ch', D: 'D', E: 'E', G: 'g', P: 'P' } },
|
|
||||||
{ name: 'CP-Algierfuchs', tokens: { A: 'A', C: 'cchm', D: 'D', E: 'e', G: 'G', P: 'P' }, image: 'algierfuchs-cp.jpg' },
|
{ name: 'CP-Algierfuchs', tokens: { A: 'A', C: 'cchm', D: 'D', E: 'e', G: 'G', P: 'P' }, image: 'algierfuchs-cp.jpg' },
|
||||||
{ name: 'CP-Algierfuchs-Hell', tokens: { A: 'A', C: 'cchm/ch', D: 'D', E: 'e', G: 'G', P: 'P' } },
|
|
||||||
{ name: 'CP-Polarfuchs', tokens: { A: 'A', C: 'cchm', D: 'D', E: 'e', G: 'g', P: 'P' }, image: 'polarfuchs-cp.jpg' },
|
{ name: 'CP-Polarfuchs', tokens: { A: 'A', C: 'cchm', D: 'D', E: 'e', G: 'g', P: 'P' }, image: 'polarfuchs-cp.jpg' },
|
||||||
{ name: 'CP-Polarfuchs-Hell', tokens: { A: 'A', C: 'cchm/ch', D: 'D', E: 'e', G: 'g', P: 'P' } },
|
|
||||||
{ name: 'CP-Fuchs', tokens: { A: 'A', C: 'cchm', D: 'd', E: 'e', G: 'G', P: 'P' } },
|
{ name: 'CP-Fuchs', tokens: { A: 'A', C: 'cchm', D: 'd', E: 'e', G: 'G', P: 'P' } },
|
||||||
{ name: 'CP-Fuchs-Hell', tokens: { A: 'A', C: 'cchm/ch', D: 'd', E: 'e', G: 'G', P: 'P' } },
|
{ name: 'CP-Fuchs-Hell', tokens: { A: 'A', C: 'cchm/ch', D: 'd', E: 'e', G: 'G', P: 'P' } },
|
||||||
{ name: 'CP-Blaufuchs', tokens: { A: 'A', C: 'cchm', D: 'd', E: 'e', G: 'g', P: 'P' } },
|
{ name: 'CP-Blaufuchs', tokens: { A: 'A', C: 'cchm', D: 'd', E: 'e', G: 'g', P: 'P' } },
|
||||||
{ name: 'CP-Orangeschimmel', tokens: { C: 'cchm', D: 'D', E: 'ef', G: 'G', P: 'P' } },
|
{ name: 'CP-Orangeschimmel', tokens: { C: 'cchm', D: 'D', E: 'ef', G: 'G', P: 'P' } },
|
||||||
{ name: 'CP-Orangeschimmel-Hell', tokens: { C: 'cchm/ch', D: 'D', E: 'ef', G: 'G', P: 'P' } },
|
|
||||||
]
|
]
|
||||||
|
|
||||||
export const UNKNOWN_FARBSCHLAG = 'Unbekannter Farbschlag'
|
export const UNKNOWN_FARBSCHLAG = 'Unbekannter Farbschlag'
|
||||||
@@ -209,14 +207,12 @@ function baseColourFor(g: Genotype): string | null {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* GEN-3e/3g: the C-locus colourpoint NAMING transform (breeder-authoritative).
|
* GEN-3e: the C-locus colourpoint NAMING transform (breeder-authoritative).
|
||||||
* Returns the colourpoint name, or null when it doesn't apply (full C present,
|
* 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).
|
* or chch — which the base matcher names Hermelin/Himalaya, preserving both).
|
||||||
* aa cchm/cchm -> Marder | aa cchm/ch -> Siam
|
* aa cchm/cchm -> Marder | aa cchm/ch -> Siam
|
||||||
* aa cchm/cchm gg -> Zobel | aa cchm/ch gg -> Zobel-Hell
|
* aa cchm/cchm gg -> Zobel | aa cchm/ch gg -> Zobel-Hell
|
||||||
* A- cchm/cchm -> CP-<base> | A- cchm/ch -> CP-<base>-Hell
|
* A- cchm/cchm | cchm/ch -> CP-<base colour> (base computed as if C were full)
|
||||||
* GEN-3g (breeder rule): "-Hell" in variety name == c[h]-Allel (cchm/ch het);
|
|
||||||
* no "-Hell" == cchm/cchm hom. CP-Fuchs is a Sammelbegriff (unknown loci).
|
|
||||||
*/
|
*/
|
||||||
function colourpointName(g: Genotype): string | null {
|
function colourpointName(g: Genotype): string | null {
|
||||||
const c = resolvedPair(g, 'C')
|
const c = resolvedPair(g, 'C')
|
||||||
@@ -231,9 +227,9 @@ function colourpointName(g: Genotype): string | null {
|
|||||||
if (grey) return bothCchm ? 'Zobel' : 'Zobel-Hell'
|
if (grey) return bothCchm ? 'Zobel' : 'Zobel-Hell'
|
||||||
return bothCchm ? 'Marder' : 'Siam'
|
return bothCchm ? 'Marder' : 'Siam'
|
||||||
}
|
}
|
||||||
// A- colourpoint: base as if C were full; het (cchm/ch) -> '-Hell' suffix.
|
// A- colourpoint -> CP-<base colour>, base as if C were full.
|
||||||
const base = baseColourFor(makeGenotype({ ...g, C: ['C', 'C'] }))
|
const base = baseColourFor(makeGenotype({ ...g, C: ['C', 'C'] }))
|
||||||
return base ? `CP-${base}${bothCchm ? '' : '-Hell'}` : null
|
return base ? `CP-${base}` : null
|
||||||
}
|
}
|
||||||
|
|
||||||
export function farbschlagFor(g: Genotype): FarbschlagMatch {
|
export function farbschlagFor(g: Genotype): FarbschlagMatch {
|
||||||
|
|||||||
@@ -340,67 +340,42 @@
|
|||||||
"sortOrder": 53,
|
"sortOrder": 53,
|
||||||
"image": "agouti-cp.jpg"
|
"image": "agouti-cp.jpg"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"name": "CP-Agouti-Hell",
|
|
||||||
"canonicalGenotype": "AA cchmch DD EE GG PP spsp rere",
|
|
||||||
"sortOrder": 54
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"name": "CP-Silberagouti",
|
"name": "CP-Silberagouti",
|
||||||
"canonicalGenotype": "AA cchmcchm DD EE gg PP spsp rere",
|
"canonicalGenotype": "AA cchmcchm DD EE gg PP spsp rere",
|
||||||
"sortOrder": 55,
|
"sortOrder": 54,
|
||||||
"image": "silberagouti-cp.JPG"
|
"image": "silberagouti-cp.JPG"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"name": "CP-Silberagouti-Hell",
|
|
||||||
"canonicalGenotype": "AA cchmch DD EE gg PP spsp rere",
|
|
||||||
"sortOrder": 56
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"name": "CP-Algierfuchs",
|
"name": "CP-Algierfuchs",
|
||||||
"canonicalGenotype": "AA cchmcchm DD ee GG PP spsp rere",
|
"canonicalGenotype": "AA cchmcchm DD ee GG PP spsp rere",
|
||||||
"sortOrder": 57,
|
"sortOrder": 55,
|
||||||
"image": "algierfuchs-cp.jpg"
|
"image": "algierfuchs-cp.jpg"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"name": "CP-Algierfuchs-Hell",
|
|
||||||
"canonicalGenotype": "AA cchmch DD ee GG PP spsp rere",
|
|
||||||
"sortOrder": 58
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"name": "CP-Polarfuchs",
|
"name": "CP-Polarfuchs",
|
||||||
"canonicalGenotype": "AA cchmcchm DD ee gg PP spsp rere",
|
"canonicalGenotype": "AA cchmcchm DD ee gg PP spsp rere",
|
||||||
"sortOrder": 59,
|
"sortOrder": 56,
|
||||||
"image": "polarfuchs-cp.jpg"
|
"image": "polarfuchs-cp.jpg"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"name": "CP-Polarfuchs-Hell",
|
|
||||||
"canonicalGenotype": "AA cchmch DD ee gg PP spsp rere",
|
|
||||||
"sortOrder": 60
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"name": "CP-Fuchs",
|
"name": "CP-Fuchs",
|
||||||
"canonicalGenotype": "AA cchmcchm dd ee GG PP spsp rere",
|
"canonicalGenotype": "AA cchmcchm dd ee GG PP spsp rere",
|
||||||
"sortOrder": 61
|
"sortOrder": 57
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "CP-Fuchs-Hell",
|
"name": "CP-Fuchs-Hell",
|
||||||
"canonicalGenotype": "AA cchmch dd ee GG PP spsp rere",
|
"canonicalGenotype": "AA cchmch dd ee GG PP spsp rere",
|
||||||
"sortOrder": 62
|
"sortOrder": 58
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "CP-Blaufuchs",
|
"name": "CP-Blaufuchs",
|
||||||
"canonicalGenotype": "AA cchmcchm dd ee gg PP spsp rere",
|
"canonicalGenotype": "AA cchmcchm dd ee gg PP spsp rere",
|
||||||
"sortOrder": 63
|
"sortOrder": 59
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "CP-Orangeschimmel",
|
"name": "CP-Orangeschimmel",
|
||||||
"canonicalGenotype": "AA cchmcchm DD efef GG PP spsp rere",
|
"canonicalGenotype": "AA cchmcchm DD efef GG PP spsp rere",
|
||||||
"sortOrder": 64
|
"sortOrder": 60
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "CP-Orangeschimmel-Hell",
|
|
||||||
"canonicalGenotype": "AA cchmch DD efef GG PP spsp rere",
|
|
||||||
"sortOrder": 65
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
Reference in New Issue
Block a user