import type { ColonyType, VarroaMeasurement, VarroaMethod, } from '../../generated/prisma'; import { addDays, berlinDayOf } from '../common/dates'; import type { TreatmentWindowLite } from '../treatments/withdrawal-window'; import type { VarroaThresholdConfig } from './rule-config/rule-config.types'; export type VarroaLightKey = 'green' | 'yellow' | 'red'; export type VarroaBandReason = | 'NATURAL_FALL_SEASON' | 'NATURAL_FALL_WINTER' | 'INFESTATION_SUMMER' | 'INFESTATION_AUTUMN'; export interface VarroaBand { method: VarroaMethod; unit: 'MITES_PER_DAY' | 'PERCENT'; /** null: no yellow stage (winter). */ yellowFrom: number | null; redAbove: number; reason: VarroaBandReason; youngColony: boolean; } export interface VarroaBands { day: string; bands: Partial>; /** Recheck day while a treatment effect is in force, otherwise null. */ postTreatmentUntil: string | null; } export interface VarroaRating { light: VarroaLightKey | 'POST_TREATMENT' | null; reasonCode: VarroaBandReason | 'POST_TREATMENT' | null; band: VarroaBand | null; postTreatmentUntil: string | null; } const PERCENT_METHODS: readonly VarroaMethod[] = [ 'POWDERED_SUGAR', 'WASHING_METHOD', ]; function naturalFallBand( month: number, factor: number, young: boolean, config: VarroaThresholdConfig['naturalFall'], ): VarroaBand { const inSeason = config.seasonMonths.includes(month); return { method: 'NATURAL_MITE_FALL', unit: 'MITES_PER_DAY', yellowFrom: inSeason ? config.yellowFrom * factor : null, redAbove: (inSeason ? config.redAbove : config.winterRedAbove) * factor, reason: inSeason ? 'NATURAL_FALL_SEASON' : 'NATURAL_FALL_WINTER', youngColony: young, }; } /** The percentage band of the month, absent outside summer and autumn. */ function percentBand( method: VarroaMethod, month: number, config: VarroaThresholdConfig['infestationPercent'], ): VarroaBand | undefined { if (config.summerMonths.includes(month)) { return { method, unit: 'PERCENT', ...config.summer, reason: 'INFESTATION_SUMMER', youngColony: false, }; } if (config.autumnMonths.includes(month)) { return { method, unit: 'PERCENT', ...config.autumn, reason: 'INFESTATION_AUTUMN', youngColony: false, }; } return undefined; } /** Latest recheck day of the treatments whose effect covers `day`. */ function postTreatmentUntil( day: string, recheckAfterDays: number, treatments: readonly TreatmentWindowLite[], ): string | null { let until: string | null = null; for (const { startDay, endDay } of treatments) { const recheckDay = addDays(endDay, recheckAfterDays); const covers = startDay <= day && day < recheckDay; if (covers && (until === null || recheckDay > until)) { until = recheckDay; } } return until; } /** * The bands in force on `day` for a colony of `colonyType`: natural mite fall * by season (young colonies at the reduced factor), the percentage methods in * summer and autumn only, no band for the drone brood check. */ export function bandsFor( day: string, colonyType: ColonyType | null, config: VarroaThresholdConfig, treatments: readonly TreatmentWindowLite[], ): VarroaBands { const month = Number(day.slice(5, 7)); const young = colonyType !== null && config.youngColonyTypes.includes(colonyType); const factor = young ? config.youngColonyFactor : 1; const bands: VarroaBands['bands'] = { NATURAL_MITE_FALL: naturalFallBand( month, factor, young, config.naturalFall, ), }; for (const method of PERCENT_METHODS) { const band = percentBand(method, month, config.infestationPercent); if (band) { bands[method] = band; } } return { day, bands, postTreatmentUntil: postTreatmentUntil( day, config.recheckAfterDays, treatments, ), }; } /** Red above redAbove, yellow from yellowFrom, otherwise green. */ export function classify( value: number | null, band: VarroaBand | null | undefined, ): VarroaLightKey | null { if (value === null || !band) { return null; } if (value > band.redAbove) { return 'red'; } if (band.yellowFrom !== null && value >= band.yellowFrom) { return 'yellow'; } return 'green'; } /** * The light of one measurement. A measurement taken from the start of a * treatment until the recheck day has no light (treatment effect), except the * drone brood check, which never has one. */ export function rateMeasurement( m: Pick, colonyType: ColonyType | null, config: VarroaThresholdConfig, treatments: readonly TreatmentWindowLite[], ): VarroaRating { const day = berlinDayOf(m.date); const { bands, postTreatmentUntil: recheckDay } = bandsFor( day, colonyType, config, treatments, ); const band = bands[m.method] ?? null; if (recheckDay !== null && m.method !== 'DRONE_BROOD_CONTROL') { return { light: 'POST_TREATMENT', reasonCode: 'POST_TREATMENT', band: null, postTreatmentUntil: recheckDay, }; } return { light: classify(m.calculatedValue, band), reasonCode: band?.reason ?? null, band, postTreatmentUntil: null, }; }