Skip to content

Commit

Permalink
WIP for #32
Browse files Browse the repository at this point in the history
  • Loading branch information
swantzter committed Sep 21, 2024
1 parent 79fd37b commit 1b89437
Show file tree
Hide file tree
Showing 4 changed files with 251 additions and 127 deletions.
49 changes: 47 additions & 2 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 { calculateTally, 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,6 +70,51 @@ void test('helpers', async t => {
assert.strictEqual(formatFactor(1.1), '+10 %')
})

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('calculateTally', async t => {
const meta: JudgeMeta = {
judgeId: '1',
Expand Down
84 changes: 55 additions & 29 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,33 @@ 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
}

/**
* Takes a scoresheet and returns a tally
*
Expand All @@ -85,41 +114,38 @@ 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)
const tally: ScoreTally<Schema> = isTallyScoresheet<Schema>(scoresheet) ? { ...(scoresheet.tally ?? {}) } : {}
const allowedSchemas = fieldDefinitions?.map(f => f.schema)

for (const mark of filterMarkStream(scoresheet.marks)) {
tally[mark.schema as Schema] = (tally[mark.schema as Schema] ?? 0) + (mark.value ?? 1)
}
}

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)
if (fieldDefinitions != null) {
for (const field of fieldDefinitions) {
const v = tally[field.schema]
if (typeof v !== 'number') continue

tally[field.schema] = clampNumber(v, field)
}
}
}

if (allowedSchemas != null) {
const extra = Object.keys(tally).filter(schema => !allowedSchemas.includes(schema as Schema))
if (allowedSchemas != null) {
const extra = Object.keys(tally).filter(schema => !allowedSchemas.includes(schema as Schema))

// @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]
}
// @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 tally
return {
meta: scoresheet.meta,
tally,
}
}
}

/**
Expand Down
Loading

0 comments on commit 1b89437

Please sign in to comment.