@nrk/valg-valgomat-algoritme
Version:
The algorithm used to calculate distance between sets of positions for NRKs Valgomat
77 lines (64 loc) • 2.57 kB
JavaScript
import { commonStatements } from './domain/positions.js'
/** @type {ValgomatAlgoritme.PositionValue} */
const MIN_POSITION = -2
/** @type {ValgomatAlgoritme.PositionValue} */
const MAX_POSITION = 2
const MAX_DIST = Math.abs(MAX_POSITION - MIN_POSITION)
/**
* @param {string[]} statements
* @param {ValgomatAlgoritme.Positions} positionsA
* @param {ValgomatAlgoritme.Positions} positionsB
* @returns {[ValgomatAlgoritme.PositionValue, ValgomatAlgoritme.PositionValue][]}
*/
function combineBy(statements, positionsA, positionsB) {
return statements.map(function (statementId) {
// NOTE: These casts are safe because we always call this with `statements`
// being the set of common statements between the two positions.
let a = /** @type {ValgomatAlgoritme.PositionValue} */ (positionsA[statementId])
let b = /** @type {ValgomatAlgoritme.PositionValue} */ (positionsB[statementId])
return [a, b]
})
}
/**
* @param {ValgomatAlgoritme.PositionValue} a
* @param {ValgomatAlgoritme.PositionValue} b
* @returns
*/
function difference(a, b) {
return Math.abs(a - b)
}
/**
* @param {string[]} statements
* @param {ValgomatAlgoritme.Positions} positionsA
* @param {ValgomatAlgoritme.Positions} positionsB
* @returns {number | null}
*/
function distanceGivenStatements(statements, positionsA, positionsB) {
if (statements.length === 0) {
// NOTE: If there are no statements, the difference would be unknowable. So
// we return null in this case. This used to return 0 for historical
// reasons, but this reason is no longer valid and we need to distinguish
// between furthest apart (0% common) and unknown.
return null
}
let distance = combineBy(statements, positionsA, positionsB)
.map(([a, b]) => difference(a, b))
.reduce((sum, value) => sum + value)
let maxPossibleDistance = statements.length * MAX_DIST
return (maxPossibleDistance - distance) / maxPossibleDistance
}
/** @type {import('../types.d.ts').proximity} */
export function proximity(positionsA, positionsB) {
let answeredStatements = commonStatements(positionsA, positionsB)
return distanceGivenStatements(answeredStatements, positionsA, positionsB)
}
/** @type {import('../types.d.ts').proximityMap} */
export function proximityMap(positionsA, positionsMap) {
let result = /** @type {Record.<string, number|null>} */ ({})
for (let id in positionsMap) {
let positionsB = positionsMap[id]
let calculatedDistance = proximity(positionsA, positionsB)
result[id] = calculatedDistance
}
return result
}