UNPKG

@nrk/valg-valgomat-algoritme

Version:

The algorithm used to calculate distance between sets of positions for NRKs Valgomat

383 lines (309 loc) 7.61 kB
import tap from 'tap' import jsc from 'jsverify' import { proximity, proximityMap } from './algorithm.js' import { toPositions } from './domain/positions.js' import { positions as positionsMock } from './__helpers/mocks.js' /** * @callback checkFunction * @param {ValgomatAlgoritme.Positions[]} positions */ /** * * @param {checkFunction} check * @returns {(vectors: (ValgomatAlgoritme.PositionValue | null)[][]) => boolean} */ function arrayToPositionsHelper(check) { // The order of keys is not really interesing here, // but needed for the algorithm to do its thing. return function (vectors) { let args = vectors .map((vector) => vector.map((v, i) => /** @type {ValgomatAlgoritme.PositionVector} */ ([`${i}`, v])), ) .map((vector) => toPositions(vector)) return check(args) } } let arbitraryPositions = jsc.array(jsc.integer(-2, 2)) // Property based tests tap.test('algorithm is symmetrical', function (t) { let arbitraryPositionsPair = jsc.suchthat( jsc.pair(arbitraryPositions, arbitraryPositions), ([a, b]) => a.length > 10 && b.length > 10, ) /** @type {checkFunction} */ function check([a, b]) { let distanceA = proximity(a, b) let distanceB = proximity(b, a) return distanceA === distanceB } let wrappedCheck = arrayToPositionsHelper(check) jsc.assert(jsc.forall(arbitraryPositionsPair, wrappedCheck)) t.end() }) tap.test('not answered and missing are identical', function (t) { let arbitraryPositionsPair = jsc.pair(jsc.array(jsc.constant(null)), jsc.constant([])) /** @type {checkFunction} */ function check([a, b]) { return proximity(a, b) === null } let wrappedCheck = arrayToPositionsHelper(check) jsc.assert(jsc.forall(arbitraryPositionsPair, wrappedCheck)) t.end() }) tap.test('handles identical set of answered statements', function (t) { // Ensure equal length, to have identical sets of answered statements let arbitraryPositionsPair = jsc.suchthat( jsc.pair(arbitraryPositions, arbitraryPositions), ([a, b]) => a.length === b.length, ) /** @type {checkFunction} */ function check([a, b]) { let d = proximity(a, b) return d == null || (d <= 1 && d >= 0) } let wrappedCheck = arrayToPositionsHelper(check) jsc.assert(jsc.forall(arbitraryPositionsPair, wrappedCheck)) t.end() }) tap.test('handles uneven number of answered statements', function (t) { // Ensure un-even length. let arbitraryPositionsPair = jsc.suchthat( jsc.pair(arbitraryPositions, arbitraryPositions), ([a, b]) => a.length !== b.length, ) /** @type {checkFunction} */ function check([a, b]) { let d = proximity(a, b) return d == null || (d <= 1 && d >= 0) } let wrappedCheck = arrayToPositionsHelper(check) jsc.assert(jsc.forall(arbitraryPositionsPair, wrappedCheck)) t.end() }) // Example based tests tap.test('both empty', function (t) { let a = /** @type {ValgomatAlgoritme.Positions} */ ({}) let b = /** @type {ValgomatAlgoritme.Positions} */ ({}) t.ok(proximity(a, b) === null) t.end() }) tap.test('left-empty', function (t) { let a = positionsMock(2) let b = /** @type {ValgomatAlgoritme.Positions} */ ({}) t.ok(proximity(a, b) === null) t.end() }) tap.test('right-empty', function (t) { let a = /** @type {ValgomatAlgoritme.Positions} */ ({}) let b = positionsMock(2) t.ok(proximity(a, b) === null) t.end() }) tap.test('left just-0s', function (t) { let n = 2 let a = positionsMock(n, () => null) let b = positionsMock(n) t.ok(proximity(a, b) === null) t.end() }) tap.test('right just-0s', function (t) { let n = 2 let a = positionsMock(n) let b = positionsMock(n, () => null) t.ok(proximity(a, b) === null) t.end() }) tap.test('both just-0s', function (t) { let n = 2 let a = positionsMock(n, () => null) let b = positionsMock(n, () => null) t.ok(proximity(a, b) === null) t.end() }) tap.test('left not answered', function (t) { let a = toPositions([ ['0', -2], ['1', 1], ['2', 2], ]) let b = toPositions([ ['0', 1], ['1', 1], ['2', null], ]) t.ok(proximity(a, b) === (8 - 3) / 8) t.end() }) tap.test('left missing', function (t) { let a = toPositions([ ['0', -2], ['1', 1], ['2', 2], ]) let b = toPositions([ ['0', 1], ['1', 1], ]) t.ok(proximity(a, b) === (8 - 3) / 8) t.end() }) tap.test('right not answered', function (t) { let a = toPositions([ ['0', 1], ['1', 1], ['2', null], ]) let b = toPositions([ ['0', -2], ['1', 1], ['2', 2], ]) t.ok(proximity(a, b) === (8 - 3) / 8) t.end() }) tap.test('right missing', function (t) { let a = toPositions([ ['0', 1], ['1', 1], ]) let b = toPositions([ ['0', -2], ['1', 1], ['2', 2], ]) t.ok(proximity(a, b) === (8 - 3) / 8) t.end() }) tap.test('both not answered', function (t) { let a = toPositions([ ['0', 1], ['1', 1], ['2', null], ['3', -1], ]) let b = toPositions([ ['0', -2], ['1', 1], ['2', 2], ['3', null], ]) t.ok(proximity(a, b) === (8 - 3) / 8) t.end() }) tap.test('both missing', function (t) { let a = toPositions([ ['0', 1], ['1', 1], ['3', -1], ]) let b = toPositions([ ['0', -2], ['1', 1], ['2', 2], ]) t.ok(proximity(a, b) === (8 - 3) / 8) t.end() }) tap.test('readme example', function (t) { let a = toPositions([ ['0', 1], ['1', -1], ]) let b = toPositions([ ['0', -2], ['1', 2], ]) t.ok(proximity(a, b) === 0.25) t.end() }) tap.test('proximityMap', function (t) { tap.test('result includes all ids passed', function (t) { let map = { 1: toPositions([ ['0', 1], ['1', -1], ]), 2: toPositions([ ['0', 2], ['1', -2], ]), } let a = toPositions([ ['0', 1], ['1', -2], ]) let distances = proximityMap(a, map) t.same(Object.keys(map), Object.keys(distances)) t.end() }) tap.test('result include actual distances', function (t) { let b1 = toPositions([ ['0', 1], ['1', -1], ]) let b2 = toPositions([ ['0', 2], ['1', -2], ]) let map = { 1: b1, 2: b2 } let a = toPositions([ ['0', 1], ['1', -2], ]) let distances = proximityMap(a, map) t.ok(distances[1] === proximity(a, b1)) t.ok(distances[2] === proximity(a, b2)) t.end() }) tap.test('cannot make proximity larger than 1', function (t) { let b1 = toPositions([ ['0', 1], ['1', -1], ]) let b2 = toPositions([ ['0', 2], ['1', -2], ]) let map = { 1: b1, 2: b2 } let a = b1 let distances = proximityMap(a, map) t.ok(distances[1] === 1.0) t.end() }) tap.test('preserves null logic for unknowable distances', function (t) { let b1 = toPositions([ ['0', 1], ['1', -1], ]) let b2 = toPositions([ ['2', 2], ['3', -2], ]) let map = { 1: b1, 2: b2 } let a = b1 let distances = proximityMap(a, map) t.ok(distances[1] === 1.0) t.ok(distances[2] === null) t.end() }) tap.test('readme example', function (t) { let a = toPositions([ ['0', 1], ['1', -1], ]) let b = toPositions([ ['0', 2], ['1', -2], ]) let voter = toPositions([ ['0', null], ['1', -2], ]) let distances = proximityMap(voter, { a, b }) t.ok(distances['a'] === 0.75) t.ok(distances['b'] === 1.0) t.end() }) t.end() })