From b6ba9b03d33eeb58d35ded0e1170a0211a104de2 Mon Sep 17 00:00:00 2001 From: Svante Bengtson Date: Sun, 22 Sep 2024 20:36:30 +0200 Subject: [PATCH 1/4] separate calcualteScoresheet into calculateTally and calculateJudgeResult + separate tallyFieldDefinitions (#30) fixes #32 --- lib/helpers.test.ts | 59 +++++++++++-- lib/helpers.ts | 84 ++++++++++++------- .../ijru.freestyle@2.0.0.ts | 50 ++++++----- .../ijru.freestyle@3.0.0.test.ts | 54 +++++++----- .../ijru.freestyle@3.0.0.ts | 50 ++++++----- .../ijru.speed@1.0.0.test.ts | 62 ++++++++------ .../competition-events/ijru.speed@1.0.0.ts | 26 +++--- .../svgf-rh.freestyle@2020.ts | 38 ++++++--- .../svgf-vh.freestyle@2023.ts | 45 ++++++---- .../competition-events/svgf-vh.speed@2023.ts | 16 ++-- .../competition-events/svgf-vh.timing@2023.ts | 40 +++++++-- lib/models/types.ts | 34 ++++++-- 12 files changed, 374 insertions(+), 184 deletions(-) diff --git a/lib/helpers.test.ts b/lib/helpers.test.ts index 560f8cd..3ed3b8a 100644 --- a/lib/helpers.test.ts +++ b/lib/helpers.test.ts @@ -1,7 +1,7 @@ -import { calculateTally, clampNumber, formatFactor, isObject, parseCompetitionEventDefinition, roundTo, roundToCurry, roundToMultiple } from './helpers.js' +import { simpleCalculateTallyFactory, clampNumber, filterMarkStream, formatFactor, isObject, parseCompetitionEventDefinition, roundTo, roundToCurry, roundToMultiple } from './helpers.js' import assert from 'node:assert' import test from 'node:test' -import type { JudgeMeta, Mark } from './models/types.js' +import type { GenericMark, JudgeMeta, Mark } from './models/types.js' void test('helpers', async t => { await t.test('isObject', async t => { @@ -70,7 +70,52 @@ void test('helpers', async t => { assert.strictEqual(formatFactor(1.1), '+10 %') }) - await t.test('calculateTally', async t => { + await t.test('filterMarkStream', async t => { + const tests: Array<[name: string, input: Array>, output: Array>]> = [ + [ + 'No special marks', + [{ sequence: 1, timestamp: 1, schema: 'a' }, { sequence: 2, timestamp: 2, schema: 'a' }], + [{ sequence: 1, timestamp: 1, schema: 'a' }, { sequence: 2, timestamp: 2, schema: 'a' }], + ], + [ + 'Start at clear mark', + [{ sequence: 1, timestamp: 1, schema: 'a' }, { sequence: 2, timestamp: 2, schema: 'a' }, { sequence: 3, timestamp: 3, schema: 'clear' }, { sequence: 4, timestamp: 4, schema: 'b' }, { sequence: 5, timestamp: 5, schema: 'b' }], + [{ sequence: 4, timestamp: 4, schema: 'b' }, { sequence: 5, timestamp: 5, schema: 'b' }], + ], + [ + 'Handle single undo', + [{ sequence: 1, timestamp: 1, schema: 'a' }, { sequence: 2, timestamp: 2, schema: 'a' }, { sequence: 3, timestamp: 3, schema: 'undo', target: 2 }, { sequence: 4, timestamp: 4, schema: 'b' }, { sequence: 5, timestamp: 5, schema: 'b' }], + [{ sequence: 1, timestamp: 1, schema: 'a' }, { sequence: 4, timestamp: 4, schema: 'b' }, { sequence: 5, timestamp: 5, schema: 'b' }], + ], + [ + 'Handle two undos right after each other', + [{ sequence: 1, timestamp: 1, schema: 'a' }, { sequence: 2, timestamp: 2, schema: 'a' }, { sequence: 3, timestamp: 3, schema: 'b' }, { sequence: 4, timestamp: 4, schema: 'undo', target: 2 }, { sequence: 5, timestamp: 5, schema: 'undo', target: 3 }, { sequence: 6, timestamp: 6, schema: 'b' }], + [{ sequence: 1, timestamp: 1, schema: 'a' }, { sequence: 6, timestamp: 6, schema: 'b' }], + ], + [ + 'Cannot undo an undo - silent ignore', + [{ sequence: 1, timestamp: 1, schema: 'a' }, { sequence: 2, timestamp: 2, schema: 'a' }, { sequence: 3, timestamp: 3, schema: 'b' }, { sequence: 4, timestamp: 4, schema: 'undo', target: 2 }, { sequence: 5, timestamp: 5, schema: 'undo', target: 4 }, { sequence: 6, timestamp: 6, schema: 'b' }], + [{ sequence: 1, timestamp: 1, schema: 'a' }, { sequence: 3, timestamp: 3, schema: 'b' }, { sequence: 6, timestamp: 6, schema: 'b' }], + ], + [ + 'Cannot undo a clear mark - silent ignore', + [{ sequence: 1, timestamp: 1, schema: 'a' }, { sequence: 2, timestamp: 2, schema: 'a' }, { sequence: 3, timestamp: 3, schema: 'b' }, { sequence: 4, timestamp: 4, schema: 'clear' }, { sequence: 5, timestamp: 5, schema: 'undo', target: 4 }, { sequence: 6, timestamp: 6, schema: 'b' }], + [{ sequence: 6, timestamp: 6, schema: 'b' }], + ], + [ + 'Cannot undo before a clear mark - silent ignore', + [{ sequence: 1, timestamp: 1, schema: 'a' }, { sequence: 2, timestamp: 2, schema: 'a' }, { sequence: 3, timestamp: 3, schema: 'b' }, { sequence: 4, timestamp: 4, schema: 'clear' }, { sequence: 5, timestamp: 5, schema: 'undo', target: 2 }, { sequence: 6, timestamp: 6, schema: 'b' }], + [{ sequence: 6, timestamp: 6, schema: 'b' }], + ], + ] + for (const [title, input, output] of tests) { + await t.test(title, () => { + assert.deepStrictEqual(filterMarkStream(input), output) + }) + } + }) + + await t.test('simpleCalculateTallyFactory', async t => { const meta: JudgeMeta = { judgeId: '1', judgeTypeId: 'S', @@ -78,10 +123,6 @@ void test('helpers', async t => { participantId: '1', competitionEvent: 'e.ijru.sp.sr.srss.1.30@1.0.0', } - await t.test('Should return tally for a TallyScoresheet', () => { - const tally = { entPlus: 2, entMinus: 1 } - assert.deepStrictEqual(calculateTally({ meta, tally }), tally) - }) await t.test('Should return tally for MarkScoresheet', () => { const marks: Array> = [ @@ -93,7 +134,7 @@ void test('helpers', async t => { formPlus: 2, formCheck: 1, } - assert.deepStrictEqual(calculateTally({ meta, marks }), tally) + assert.deepStrictEqual(simpleCalculateTallyFactory(meta.judgeTypeId)({ meta, marks }), { meta, tally }) }) await t.test('Should return tally for MarkScoresheet with undo marks', () => { @@ -107,7 +148,7 @@ void test('helpers', async t => { formPlus: 1, formCheck: 1, } - assert.deepStrictEqual(calculateTally({ meta, marks }), tally) + assert.deepStrictEqual(simpleCalculateTallyFactory(meta.judgeTypeId)({ meta, marks }), { meta, tally }) }) }) diff --git a/lib/helpers.ts b/lib/helpers.ts index 3f807a6..4f4ad2d 100644 --- a/lib/helpers.ts +++ b/lib/helpers.ts @@ -1,3 +1,5 @@ +import { RSRWrongJudgeTypeError } from './errors.js' +import type { GenericMark, Mark } from './models/types.js' import { type JudgeFieldDefinition, type ScoreTally, isClearMark, isUndoMark, type Meta, type EntryResult, type TallyScoresheet, type MarkScoresheet } from './models/types.js' import { type CompetitionEventDefinition } from './preconfigured/types.js' @@ -76,6 +78,47 @@ export function formatFactor (value: number): string { else return `-${roundTo((1 - value) * 100, 0)} %` } +export function filterMarkStream (rawMarks: Readonly>>>): Array> { + const clearMarkIdx = rawMarks.findLastIndex(mark => mark.schema === 'clear') + const marks = rawMarks.slice(clearMarkIdx + 1) + for (let idx = 0; idx < marks.length; idx++) { + const mark = marks[idx] + if (isUndoMark(mark)) { + let targetIdx = -1 + // We're doing an optimised check here since the undone mark need to be + // before the undo mark, and will most likely be just before teh undo mark + // therefore findIndex or findLastIndex would be wasteful + for (let tIdx = idx - 1; tIdx >= 0; tIdx--) { + if (marks[tIdx].sequence === mark.target) { + targetIdx = tIdx + break + } + } + if (targetIdx >= 0 && !isUndoMark(marks[targetIdx]) && !isClearMark(marks[targetIdx])) { + marks.splice(targetIdx, 1) + idx-- + } + marks.splice(idx, 1) + idx-- + } + } + return marks +} + +export function filterTally (_tally: ScoreTally, fieldDefinitions?: Readonly>>): ScoreTally { + if (fieldDefinitions == null) return _tally as ScoreTally + const tally: ScoreTally = {} + + for (const field of fieldDefinitions) { + const v = _tally[field.schema] + if (typeof v !== 'number') continue + + tally[field.schema] = clampNumber(v, field) + } + + return tally +} + /** * Takes a scoresheet and returns a tally * @@ -85,41 +128,22 @@ export function formatFactor (value: number): string { * Each value of the tally will also be clamped to the specified max, min and * step size for that field schema. */ -export function calculateTally (scoresheet: Pick, 'tally'> | Pick, 'marks'>, fieldDefinitions?: Readonly>>): ScoreTally { - let tally: ScoreTally = isTallyScoresheet(scoresheet) ? { ...(scoresheet.tally ?? {}) } : {} - const allowedSchemas = fieldDefinitions?.map(f => f.schema) - - if (isMarkScoresheet(scoresheet)) { - for (const mark of scoresheet.marks) { - if (isUndoMark(mark)) { - const target = scoresheet.marks.find(m => m.sequence === mark.target) - if (target == null || isUndoMark(target) || isClearMark(target)) continue - tally[target.schema as Schema] = (tally[target.schema as Schema] ?? 0) - (target.value ?? 1) - } else if (isClearMark(mark)) { - tally = {} - } else { - tally[mark.schema as Schema] = (tally[mark.schema as Schema] ?? 0) + (mark.value ?? 1) - } - } - } +export function simpleCalculateTallyFactory (judgeTypeId: string, fieldDefinitions?: Readonly>>) { + return function simpleCalculateTally (scoresheet: MarkScoresheet) { + if (!matchMeta(scoresheet.meta, { judgeTypeId })) throw new RSRWrongJudgeTypeError(scoresheet.meta.judgeTypeId, judgeTypeId) + let tally: ScoreTally = isTallyScoresheet(scoresheet) ? { ...(scoresheet.tally ?? {}) } : {} - if (fieldDefinitions != null) { - for (const field of fieldDefinitions) { - if (typeof tally[field.schema] !== 'number') continue - // eslint-disable-next-line @typescript-eslint/non-nullable-type-assertion-style - tally[field.schema] = clampNumber(tally[field.schema] as number, field) + for (const mark of filterMarkStream(scoresheet.marks)) { + tally[mark.schema as Schema] = (tally[mark.schema as Schema] ?? 0) + (mark.value ?? 1) } - } - if (allowedSchemas != null) { - const extra = Object.keys(tally).filter(schema => !allowedSchemas.includes(schema as Schema)) + tally = filterTally(tally, fieldDefinitions) - // @ts-expect-error Yes I know schema doesn't exist in the target object, I'm deleting the schemas that shouldn't be there - // eslint-disable-next-line @typescript-eslint/no-dynamic-delete - for (const schema of extra) delete tally[schema] + return { + meta: scoresheet.meta, + tally, + } } - - return tally } /** diff --git a/lib/models/competition-events/ijru.freestyle@2.0.0.ts b/lib/models/competition-events/ijru.freestyle@2.0.0.ts index 8751f1c..ee64578 100644 --- a/lib/models/competition-events/ijru.freestyle@2.0.0.ts +++ b/lib/models/competition-events/ijru.freestyle@2.0.0.ts @@ -1,6 +1,6 @@ import { RSRWrongJudgeTypeError } from '../../errors' -import { calculateTally, formatFactor, matchMeta, roundTo, roundToCurry } from '../../helpers' -import { type ScoreTally, type JudgeTypeGetter, type JudgeFieldDefinition, type TableDefinition, type CompetitionEventModel } from '../types' +import { filterTally, formatFactor, matchMeta, roundTo, roundToCurry, simpleCalculateTallyFactory } from '../../helpers' +import { type JudgeTypeGetter, type JudgeFieldDefinition, type TableDefinition, type CompetitionEventModel } from '../types' import { ijruAverage } from './ijru.freestyle@3.0.0' type Option = 'noMusicality' | 'discipline' | 'interactions' @@ -24,10 +24,9 @@ export function L (l: number): number { // ====== // JUDGES // ====== -export const routinePresentationJudge: JudgeTypeGetter = options => { +export const routinePresentationJudge: JudgeTypeGetter