FEAT-7: statistics aggregation module - litters/year, avg litter size, Farbschlag distribution, population curve, losses (9 tests)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
162
gerbil-manager-web/src/statistics/__tests__/aggregate.test.ts
Normal file
162
gerbil-manager-web/src/statistics/__tests__/aggregate.test.ts
Normal file
@@ -0,0 +1,162 @@
|
||||
/** FEAT-7: Tests der Statistik-Aggregationen (reine Daten, kein DOM). */
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import type { Gerbil, GerbilStatus, Litter } from '../../api/types'
|
||||
import {
|
||||
avgLitterSizePerYear,
|
||||
farbschlagDistribution,
|
||||
littersPerYear,
|
||||
lossesPerYear,
|
||||
populationOverTime,
|
||||
} from '../aggregate'
|
||||
|
||||
function makeLitter(date: string, totalBorn: number | null = null): Litter {
|
||||
return {
|
||||
id: `l-${date}-${Math.abs(totalBorn ?? 0)}-${counter++}`,
|
||||
name: 'Wurf',
|
||||
date,
|
||||
totalBorn,
|
||||
expectedGoHomeDate: null,
|
||||
notes: null,
|
||||
fatherId: null,
|
||||
motherId: null,
|
||||
}
|
||||
}
|
||||
let counter = 0
|
||||
|
||||
function makeGerbil(over: Partial<Gerbil> & { id: string }): Gerbil {
|
||||
return {
|
||||
name: over.id,
|
||||
gender: 'unknown',
|
||||
status: 'Active' as GerbilStatus,
|
||||
dateOfBirth: null,
|
||||
dateOfDeath: null,
|
||||
causeOfDeath: null,
|
||||
goHomeDate: null,
|
||||
litterId: null,
|
||||
enclosureId: null,
|
||||
colorVarietyId: null,
|
||||
originContactId: null,
|
||||
receiverContactId: null,
|
||||
genotype: null,
|
||||
notes: null,
|
||||
...over,
|
||||
}
|
||||
}
|
||||
|
||||
describe('littersPerYear', () => {
|
||||
it('zählt pro Jahr und füllt Lücken mit 0', () => {
|
||||
const result = littersPerYear([
|
||||
makeLitter('2021-03-01'),
|
||||
makeLitter('2021-09-10'),
|
||||
makeLitter('2023-01-05'),
|
||||
])
|
||||
expect(result).toEqual([
|
||||
{ year: 2021, value: 2 },
|
||||
{ year: 2022, value: 0 },
|
||||
{ year: 2023, value: 1 },
|
||||
])
|
||||
})
|
||||
|
||||
it('leere Eingabe → leere Reihe', () => {
|
||||
expect(littersPerYear([])).toEqual([])
|
||||
})
|
||||
})
|
||||
|
||||
describe('avgLitterSizePerYear', () => {
|
||||
it('mittelt nur Würfe mit erfasster Wurfstärke und lässt Jahre ohne Daten aus', () => {
|
||||
const result = avgLitterSizePerYear([
|
||||
makeLitter('2021-01-01', 4),
|
||||
makeLitter('2021-06-01', 6),
|
||||
makeLitter('2022-01-01', null), // zählt nicht
|
||||
makeLitter('2023-01-01', 5),
|
||||
])
|
||||
expect(result).toEqual([
|
||||
{ year: 2021, value: 5 },
|
||||
{ year: 2023, value: 5 }, // 2022 fehlt bewusst (kein 0-Durchschnitt)
|
||||
])
|
||||
})
|
||||
})
|
||||
|
||||
describe('farbschlagDistribution', () => {
|
||||
it('zählt nur aktive Tiere, sortiert absteigend, null-Eimer zuletzt', () => {
|
||||
const gerbils = [
|
||||
makeGerbil({ id: 'a', colorVarietyId: 'Schwarz' }),
|
||||
makeGerbil({ id: 'b', colorVarietyId: 'Schwarz' }),
|
||||
makeGerbil({ id: 'c', colorVarietyId: 'Agouti' }),
|
||||
makeGerbil({ id: 'd', colorVarietyId: null }),
|
||||
makeGerbil({ id: 'e', colorVarietyId: 'Gold', status: 'Deceased' }), // inaktiv
|
||||
]
|
||||
const result = farbschlagDistribution(gerbils, (g) => g.colorVarietyId)
|
||||
expect(result).toEqual([
|
||||
{ name: 'Schwarz', count: 2 },
|
||||
{ name: 'Agouti', count: 1 },
|
||||
{ name: null, count: 1 },
|
||||
])
|
||||
})
|
||||
|
||||
it('sortiert bei Gleichstand alphabetisch (de)', () => {
|
||||
const gerbils = [
|
||||
makeGerbil({ id: 'a', colorVarietyId: 'Zobel' }),
|
||||
makeGerbil({ id: 'b', colorVarietyId: 'Ägypter' }),
|
||||
]
|
||||
const names = farbschlagDistribution(gerbils, (g) => g.colorVarietyId).map((e) => e.name)
|
||||
expect(names).toEqual(['Ägypter', 'Zobel'])
|
||||
})
|
||||
})
|
||||
|
||||
describe('populationOverTime', () => {
|
||||
it('bildet Geburt/Tod/Abgabe als Bestandskurve mit Monatsabtastung ab', () => {
|
||||
const gerbils = [
|
||||
makeGerbil({ id: 'a', dateOfBirth: '2024-01-15' }),
|
||||
makeGerbil({ id: 'b', dateOfBirth: '2024-02-20' }),
|
||||
makeGerbil({
|
||||
id: 'c',
|
||||
dateOfBirth: '2024-01-20',
|
||||
status: 'Deceased',
|
||||
dateOfDeath: '2024-03-10',
|
||||
}),
|
||||
makeGerbil({
|
||||
id: 'd',
|
||||
dateOfBirth: '2024-02-01',
|
||||
status: 'GivenAway',
|
||||
goHomeDate: '2024-04-05',
|
||||
}),
|
||||
]
|
||||
const points = populationOverTime(gerbils, '2024-04-15')
|
||||
expect(points).toEqual([
|
||||
{ date: '2024-01-01', value: 0 }, // vor allen Ereignissen
|
||||
{ date: '2024-02-01', value: 3 }, // a, c geboren + d (Geburt genau am Stichtag zählt mit)
|
||||
{ date: '2024-03-01', value: 4 },
|
||||
{ date: '2024-04-01', value: 3 }, // c verstorben am 10.03.
|
||||
{ date: '2024-04-15', value: 2 }, // d abgegeben am 05.04.
|
||||
])
|
||||
})
|
||||
|
||||
it('Tiere ohne Geburtsdatum werden übersprungen; leer → []', () => {
|
||||
expect(populationOverTime([makeGerbil({ id: 'x' })], '2024-01-01')).toEqual([])
|
||||
expect(populationOverTime([], '2024-01-01')).toEqual([])
|
||||
})
|
||||
|
||||
it('Endpunkt heute wird nicht doppelt erzeugt, wenn heute ein Monatsanfang ist', () => {
|
||||
const gerbils = [makeGerbil({ id: 'a', dateOfBirth: '2024-01-15' })]
|
||||
const points = populationOverTime(gerbils, '2024-03-01')
|
||||
expect(points[points.length - 1]).toEqual({ date: '2024-03-01', value: 1 })
|
||||
expect(points.filter((p) => p.date === '2024-03-01')).toHaveLength(1)
|
||||
})
|
||||
})
|
||||
|
||||
describe('lossesPerYear', () => {
|
||||
it('zählt Todesfälle pro Jahr (nur Status Verstorben, mit Todesdatum)', () => {
|
||||
const gerbils = [
|
||||
makeGerbil({ id: 'a', status: 'Deceased', dateOfDeath: '2021-05-01' }),
|
||||
makeGerbil({ id: 'b', status: 'Deceased', dateOfDeath: '2023-02-02' }),
|
||||
makeGerbil({ id: 'c', status: 'Deceased', dateOfDeath: null }), // ohne Datum: nicht zuordenbar
|
||||
makeGerbil({ id: 'd', status: 'GivenAway', goHomeDate: '2022-01-01' }), // kein Todesfall
|
||||
]
|
||||
expect(lossesPerYear(gerbils)).toEqual([
|
||||
{ year: 2021, value: 1 },
|
||||
{ year: 2022, value: 0 },
|
||||
{ year: 2023, value: 1 },
|
||||
])
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user