Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

separate calcualteScoresheet into calculateTally and calculateJudgeResult + separate tallyFieldDefinitions #30

Merged
merged 6 commits into from
Sep 22, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 50 additions & 9 deletions lib/helpers.test.ts
Original file line number Diff line number Diff line change
@@ -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 => {
Expand Down Expand Up @@ -70,18 +70,59 @@ 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<Mark<string>>, output: Array<GenericMark<string>>]> = [
[
'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',
entryId: '1',
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<Mark<string>> = [
Expand All @@ -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', () => {
Expand All @@ -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 })
})
})

Expand Down
84 changes: 54 additions & 30 deletions lib/helpers.ts
Original file line number Diff line number Diff line change
@@ -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'

Expand Down Expand Up @@ -76,6 +78,47 @@ export function formatFactor (value: number): string {
else return `-${roundTo((1 - value) * 100, 0)} %`
}

export function filterMarkStream <Schema extends string> (rawMarks: Readonly<Array<Readonly<Mark<Schema>>>>): Array<GenericMark<Schema>> {
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 <Schema extends string> (_tally: ScoreTally, fieldDefinitions?: Readonly<Array<JudgeFieldDefinition<Schema>>>): ScoreTally<Schema> {
if (fieldDefinitions == null) return _tally as ScoreTally<Schema>
const tally: ScoreTally<Schema> = {}

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
*
Expand All @@ -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 <Schema extends string> (scoresheet: Pick<TallyScoresheet<Schema>, 'tally'> | Pick<MarkScoresheet<Schema>, 'marks'>, fieldDefinitions?: Readonly<Array<JudgeFieldDefinition<Schema>>>): ScoreTally<Schema> {
let tally: ScoreTally<Schema> = isTallyScoresheet<Schema>(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 <Schema extends string> (judgeTypeId: string, fieldDefinitions?: Readonly<Array<JudgeFieldDefinition<Schema>>>) {
return function simpleCalculateTally (scoresheet: MarkScoresheet<Schema>) {
if (!matchMeta(scoresheet.meta, { judgeTypeId })) throw new RSRWrongJudgeTypeError(scoresheet.meta.judgeTypeId, judgeTypeId)
let tally: ScoreTally<Schema> = isTallyScoresheet<Schema>(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
}

/**
Expand Down
50 changes: 30 additions & 20 deletions lib/models/competition-events/ijru.freestyle@2.0.0.ts
Original file line number Diff line number Diff line change
@@ -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'
Expand All @@ -24,10 +24,9 @@
// ======
// JUDGES
// ======
export const routinePresentationJudge: JudgeTypeGetter<string, Option> = options => {
export const routinePresentationJudge: JudgeTypeGetter<Option> = options => {
const noMusicality = options.noMusicality === true

type Schema = `entertainment${'Plus' | 'Check' | 'Minus'}` | `musicality${'Plus' | 'Check' | 'Minus'}`
const fieldDefinitions = [
{
schema: 'entertainmentPlus',
Expand Down Expand Up @@ -75,10 +74,13 @@
return {
id,
name: 'Routine Presentation',
fieldDefinitions,
calculateScoresheet: scsh => {
markDefinitions: fieldDefinitions,
tallyDefinitions: fieldDefinitions,
calculateTally: simpleCalculateTallyFactory(id, fieldDefinitions),
calculateJudgeResult: scsh => {

Check warning on line 80 in lib/models/competition-events/ijru.freestyle@2.0.0.ts

View check run for this annotation

Codecov / codecov/patch

lib/models/competition-events/ijru.freestyle@2.0.0.ts#L77-L80

Added lines #L77 - L80 were not covered by tests
if (!matchMeta(scsh.meta, { judgeTypeId: id })) throw new RSRWrongJudgeTypeError(scsh.meta.judgeTypeId, id)
const tally: ScoreTally<Schema> = calculateTally(scsh, fieldDefinitions)
const tally = filterTally(scsh.tally, fieldDefinitions)

Check warning on line 83 in lib/models/competition-events/ijru.freestyle@2.0.0.ts

View check run for this annotation

Codecov / codecov/patch

lib/models/competition-events/ijru.freestyle@2.0.0.ts#L82-L83

Added lines #L82 - L83 were not covered by tests
const enTop = (tally.entertainmentPlus ?? 0) - (tally.entertainmentMinus ?? 0)
const enBottom = (tally.entertainmentPlus ?? 0) + (tally.entertainmentCheck ?? 0) + (tally.entertainmentMinus ?? 0)
const enAvg = enTop / (enBottom || 1)
Expand All @@ -104,7 +106,7 @@
}
}

export const athletePresentationJudge: JudgeTypeGetter<string, Option> = options => {
export const athletePresentationJudge: JudgeTypeGetter<Option> = options => {
const fieldDefinitions = [
{
name: 'Form and Execution +',
Expand Down Expand Up @@ -135,10 +137,13 @@
return {
id,
name: 'Athlete Presentation',
fieldDefinitions,
calculateScoresheet: scsh => {
markDefinitions: fieldDefinitions,
tallyDefinitions: fieldDefinitions,
calculateTally: simpleCalculateTallyFactory<string>(id, fieldDefinitions),
calculateJudgeResult: scsh => {

Check warning on line 143 in lib/models/competition-events/ijru.freestyle@2.0.0.ts

View check run for this annotation

Codecov / codecov/patch

lib/models/competition-events/ijru.freestyle@2.0.0.ts#L140-L143

Added lines #L140 - L143 were not covered by tests
if (!matchMeta(scsh.meta, { judgeTypeId: id })) throw new RSRWrongJudgeTypeError(scsh.meta.judgeTypeId, id)
const tally: ScoreTally<(typeof fieldDefinitions)[number]['schema']> = calculateTally(scsh, fieldDefinitions)
const tally = filterTally(scsh.tally, fieldDefinitions)

Check warning on line 146 in lib/models/competition-events/ijru.freestyle@2.0.0.ts

View check run for this annotation

Codecov / codecov/patch

lib/models/competition-events/ijru.freestyle@2.0.0.ts#L145-L146

Added lines #L145 - L146 were not covered by tests
const top = (tally.formExecutionPlus ?? 0) - (tally.formExecutionMinus ?? 0)
const bottom = (tally.formExecutionPlus ?? 0) + (tally.formExecutionCheck ?? 0) + (tally.formExecutionMinus ?? 0)
const avg = top / (bottom || 1)
Expand All @@ -155,7 +160,7 @@
}
}

export const requiredElementsJudge: JudgeTypeGetter<string, Option> = options => {
export const requiredElementsJudge: JudgeTypeGetter<Option> = options => {
const isDD = options.discipline === 'dd'
const hasInteractions = options.interactions === true
const fieldDefinitions = [
Expand Down Expand Up @@ -230,10 +235,12 @@
return {
id,
name: 'Required Elements',
fieldDefinitions,
calculateScoresheet: scsh => {
markDefinitions: fieldDefinitions,
tallyDefinitions: fieldDefinitions,
calculateTally: simpleCalculateTallyFactory(id, fieldDefinitions),
calculateJudgeResult: scsh => {

Check warning on line 241 in lib/models/competition-events/ijru.freestyle@2.0.0.ts

View check run for this annotation

Codecov / codecov/patch

lib/models/competition-events/ijru.freestyle@2.0.0.ts#L238-L241

Added lines #L238 - L241 were not covered by tests
if (!matchMeta(scsh.meta, { judgeTypeId: id })) throw new RSRWrongJudgeTypeError(scsh.meta.judgeTypeId, id)
const tally: ScoreTally = calculateTally(scsh, fieldDefinitions)
const tally = filterTally(scsh.tally, fieldDefinitions)

Check warning on line 243 in lib/models/competition-events/ijru.freestyle@2.0.0.ts

View check run for this annotation

Codecov / codecov/patch

lib/models/competition-events/ijru.freestyle@2.0.0.ts#L243

Added line #L243 was not covered by tests

let score = rqFields.map(f => tally[f.schema] ?? 0).reduce((a, b) => a + b)
score = score > max ? max : score
Expand All @@ -255,7 +262,7 @@
}
}

export const difficultyJudge: JudgeTypeGetter<string, Option> = options => {
export const difficultyJudge: JudgeTypeGetter<Option> = options => {
const fieldDefinitions = [
{
name: 'Level 0.5',
Expand All @@ -276,10 +283,13 @@
return {
id,
name: 'Difficulty',
fieldDefinitions,
calculateScoresheet: scsh => {
markDefinitions: fieldDefinitions,
tallyDefinitions: fieldDefinitions,
calculateTally: simpleCalculateTallyFactory<string>(id, fieldDefinitions),
calculateJudgeResult: scsh => {

Check warning on line 289 in lib/models/competition-events/ijru.freestyle@2.0.0.ts

View check run for this annotation

Codecov / codecov/patch

lib/models/competition-events/ijru.freestyle@2.0.0.ts#L286-L289

Added lines #L286 - L289 were not covered by tests
if (!matchMeta(scsh.meta, { judgeTypeId: id })) throw new RSRWrongJudgeTypeError(scsh.meta.judgeTypeId, id)
const tally: ScoreTally<(typeof fieldDefinitions)[number]['schema']> = calculateTally(scsh, fieldDefinitions)
const tally = filterTally(scsh.tally, fieldDefinitions)

Check warning on line 292 in lib/models/competition-events/ijru.freestyle@2.0.0.ts

View check run for this annotation

Codecov / codecov/patch

lib/models/competition-events/ijru.freestyle@2.0.0.ts#L291-L292

Added lines #L291 - L292 were not covered by tests
const D = fieldDefinitions.map(f => (tally[f.schema] ?? 0) * L(levels[f.schema])).reduce((a, b) => a + b)
return {
meta: scsh.meta,
Expand Down Expand Up @@ -394,4 +404,4 @@

previewTable: options => freestylePreviewTableHeaders,
resultTable: options => freestyleResultTableHeaders,
} satisfies CompetitionEventModel<string, Option>
} satisfies CompetitionEventModel<Option>
Loading
Loading