type MatchKey = "roleId" | "chapter" | "location" | "seniority" | "workType"; type MatchableRateCardLine = Partial>; type RateCardLineMatchCriteria = Partial>; type RateCardLineMatchWeights = Partial>; type RateCardLineMatchResult = { line: TLine; score: number; mismatch: boolean; }; function normalizeRateCardMatchValue( value: string, caseInsensitive: boolean, ): string { return caseInsensitive ? value.toLowerCase() : value; } function rateCardMatchValuesEqual(input: { lineValue: string; criteriaValue: string; caseInsensitive: boolean; }): boolean { return normalizeRateCardMatchValue(input.lineValue, input.caseInsensitive) === normalizeRateCardMatchValue(input.criteriaValue, input.caseInsensitive); } export function scoreRateCardLineMatch( line: TLine, input: { criteria: RateCardLineMatchCriteria; weights: RateCardLineMatchWeights; caseInsensitiveKeys?: readonly MatchKey[]; }, ): RateCardLineMatchResult { const caseInsensitiveKeys = new Set(input.caseInsensitiveKeys ?? []); let score = 0; let mismatch = false; for (const [key, criteriaValue] of Object.entries(input.criteria) as Array<[MatchKey, string | undefined]>) { const lineValue = line[key]; if (!criteriaValue || !lineValue) { continue; } if (rateCardMatchValuesEqual({ lineValue, criteriaValue, caseInsensitive: caseInsensitiveKeys.has(key), })) { score += input.weights[key] ?? 0; continue; } mismatch = true; } return { line, score, mismatch }; } export function pickBestRateCardLineMatch< TLine extends MatchableRateCardLine, >( lines: TLine[], input: { criteria: RateCardLineMatchCriteria; weights: RateCardLineMatchWeights; caseInsensitiveKeys?: readonly MatchKey[]; }, ): TLine | null { const candidates = lines .map((line) => scoreRateCardLineMatch(line, input)) .filter((candidate) => !candidate.mismatch) .sort((left, right) => right.score - left.score); return candidates[0]?.line ?? null; }