Fix calculation of scores
With this commit two calculation errors are fixed * the score in the case partyPosition = 'negative' while userPosition = 'neutral' shouldn't be MIN_POINTS but rather BASE_POINTS. * the totalScoredPoints (now "totalMaxPoints") should be independent of any party-positions which it wasn't. To minimize errors in this area in the future tests are added which are based on the official Rechenmodel of the bpb. To that end the score calculation logic was refactored and moved from results.vue to scoring.js, too. (cherry picked from commit 2d246fefbc4730ca5f7a4224325084a98f1c41f0)
This commit is contained in:
parent
66019ecda9
commit
f01d492d95
|
@ -1,7 +1,8 @@
|
|||
module.exports = {
|
||||
root: true,
|
||||
env: {
|
||||
node: true
|
||||
node: true,
|
||||
jest: true
|
||||
},
|
||||
'extends': [
|
||||
'plugin:vue/recommended',
|
||||
|
|
|
@ -20,7 +20,7 @@ module.exports = {
|
|||
'jest-serializer-vue'
|
||||
],
|
||||
testMatch: [
|
||||
'**/tests/unit/**/*.spec.(js|jsx|ts|tsx)|**/__tests__/*.(js|jsx|ts|tsx)'
|
||||
'**/*.test.(js|jsx|ts|tsx)|**/__tests__/*.(js|jsx|ts|tsx)'
|
||||
],
|
||||
testURL: 'http://localhost/'
|
||||
}
|
||||
|
|
|
@ -32,7 +32,7 @@
|
|||
<v-progress
|
||||
class="result-percentage"
|
||||
:value="party.score"
|
||||
:max="totalScoredPoints"
|
||||
:max="totalMaxPoints"
|
||||
/>
|
||||
</router-link>
|
||||
|
||||
|
@ -99,17 +99,9 @@
|
|||
<script>
|
||||
import { IPDATA_URL } from '@/config/api'
|
||||
import { getTranslatedUrl, getUserLanguage } from '@/i18n/helper'
|
||||
import {
|
||||
MAX_POINTS,
|
||||
BASE_POINTS,
|
||||
MIN_POINTS,
|
||||
EMPHASIS_POINTS,
|
||||
getScoringGrid
|
||||
} from '@/app/euromat/scoring'
|
||||
import { getPartiesWithScores, getTotalMaxPoints } from '@/app/euromat/scoring'
|
||||
import { parties } from '@/data'
|
||||
|
||||
const addUp = (a, b) => a + b
|
||||
|
||||
export default {
|
||||
name: 'Results',
|
||||
|
||||
|
@ -126,11 +118,8 @@
|
|||
return {
|
||||
userCountry: getUserLanguage().country,
|
||||
scoringGrid: [],
|
||||
answers: [],
|
||||
emphasized: [],
|
||||
scores: [],
|
||||
parties,
|
||||
totalScoredPoints: 0
|
||||
parties: [],
|
||||
totalMaxPoints: 0
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -180,18 +169,15 @@
|
|||
console.warn('Unable to fetch geo location:', error)
|
||||
}
|
||||
|
||||
this.emphasized = emphasized
|
||||
this.answers = answers
|
||||
|
||||
this.scoringGrid = getScoringGrid(this.answers, this.emphasized)
|
||||
this.scores = this.getScorePoints(this.scoringGrid)
|
||||
this.parties = this.parties
|
||||
.map(this.getScorePerParty)
|
||||
const partiesWithScores = getPartiesWithScores(answers, emphasized, parties)
|
||||
this.parties = partiesWithScores.map(party => ({
|
||||
token: party.token,
|
||||
score: party.score,
|
||||
nationalParty: party['national_parties'][this.userCountry]
|
||||
}))
|
||||
.sort((a, b) => a.score - b.score)
|
||||
.reverse()
|
||||
this.totalScoredPoints = this.scores
|
||||
.map(s => s.highestScore)
|
||||
.reduce(addUp, 0)
|
||||
this.totalMaxPoints = getTotalMaxPoints(answers, emphasized)
|
||||
},
|
||||
|
||||
methods: {
|
||||
|
@ -224,69 +210,7 @@
|
|||
}
|
||||
},
|
||||
getScorePercentage (score) {
|
||||
return (score / this.totalScoredPoints * 100).toFixed(2)
|
||||
},
|
||||
evalPoints (party, user, emphasis) {
|
||||
let score = 0
|
||||
|
||||
if (user.position === party.position) {
|
||||
score = MAX_POINTS
|
||||
} else if (
|
||||
(user.position === 'positive' && party.position === 'neutral') ||
|
||||
(user.position === 'neutral' && party.position === 'positive') ||
|
||||
(user.position === 'negative' && party.position === 'neutral')
|
||||
) {
|
||||
score = BASE_POINTS
|
||||
} else if (
|
||||
(user.position === 'positive' && party.position === 'negative') ||
|
||||
(user.position === 'neutral' && party.position === 'negative') ||
|
||||
(user.position === 'negative' && party.position === 'positive')
|
||||
) {
|
||||
score = MIN_POINTS
|
||||
}
|
||||
|
||||
return {
|
||||
party: party.party,
|
||||
score: emphasis ? score * EMPHASIS_POINTS : score
|
||||
}
|
||||
},
|
||||
getHighestScore (scores) {
|
||||
const highestScore = Math.max(...scores.map(s => s.score))
|
||||
|
||||
if (!highestScore) {
|
||||
return MIN_POINTS
|
||||
}
|
||||
|
||||
return highestScore === 1
|
||||
? MAX_POINTS
|
||||
: highestScore
|
||||
},
|
||||
getScorePoints (grid) {
|
||||
// 1. Iterate over scoringGrid
|
||||
// 2. Get user and party positions of each thesis
|
||||
// 3. Evaluate points based on calculation model for each party
|
||||
// 4. Count the highest score per thesis
|
||||
// 5. Return a new object for each thesis row with results
|
||||
return grid.map(row => {
|
||||
const partiesFromRow = row.positions.filter(p => p.type === 'party')
|
||||
const user = row.positions[row.positions.length - 1]
|
||||
const scores = partiesFromRow.map(party => this.evalPoints(party, user, row.emphasis))
|
||||
const highestScore = this.getHighestScore(scores)
|
||||
return {
|
||||
thesis: row.thesis,
|
||||
highestScore,
|
||||
scores
|
||||
}
|
||||
})
|
||||
},
|
||||
getScorePerParty (party) {
|
||||
return {
|
||||
token: party.token,
|
||||
score: this.scores
|
||||
.map(t => t.scores.find(s => s.party === party.id).score)
|
||||
.reduce(addUp, 0),
|
||||
nationalParty: party['national_parties'][this.userCountry]
|
||||
}
|
||||
return (score / this.totalMaxPoints * 100).toFixed(2)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,21 +1,60 @@
|
|||
import { parties } from '@/data'
|
||||
|
||||
export const MAX_POINTS = 2
|
||||
export const BASE_POINTS = 1
|
||||
export const MIN_POINTS = 0
|
||||
export const EMPHASIS_POINTS = 2
|
||||
|
||||
export function getPartyPositions (thesis) {
|
||||
return parties.map(party => {
|
||||
const position = party.positions.find(p => p.thesis === thesis)
|
||||
export function getPartiesWithScores (answers, emphasized, partiesPositions) {
|
||||
const scorePointsGrid = getScorePointsGrid(answers, emphasized, partiesPositions)
|
||||
|
||||
return partiesPositions.map(party => ({
|
||||
...party,
|
||||
score: getTotalScorePerParty(party, scorePointsGrid)
|
||||
}))
|
||||
}
|
||||
|
||||
export function getTotalMaxPoints (userAnswers, userEmphasized) {
|
||||
return userAnswers.map(answer => {
|
||||
const emphasis = userEmphasized.filter(e => e.thesis === answer.thesis).length >= 1
|
||||
return getMaxScorePerThesis(answer.position, emphasis)
|
||||
}).reduce((total, maxScorePerRow) => total + maxScorePerRow)
|
||||
}
|
||||
|
||||
function getScorePointsGrid (userAnswers, userEmphasized, partiesPositions) {
|
||||
// 1. Iterate over scoringGrid
|
||||
// 2. Get user and party positions of each thesis
|
||||
// 3. Evaluate points based on calculation model for each party
|
||||
// 4. Get the maximum score per thesis
|
||||
// 5. Return a new object for each thesis row with results
|
||||
const scoringGrid = getScoringGrid(userAnswers, userEmphasized, partiesPositions)
|
||||
|
||||
return scoringGrid.map(row => {
|
||||
const partiesFromRow = row.positions.filter(p => p.type === 'party')
|
||||
const userPosition = getUserPosition(row)
|
||||
const scores = partiesFromRow.map(party => ({
|
||||
party: party.party,
|
||||
score: evalPointsPerThesisPerParty(party.position, userPosition, row.emphasis)
|
||||
}))
|
||||
return {
|
||||
type: 'party',
|
||||
party: party.id,
|
||||
position: (position && position.position) || {}
|
||||
thesis: row.thesis,
|
||||
scores
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function getTotalScorePerParty (party, scorePointsGrid) {
|
||||
return scorePointsGrid
|
||||
.map(thesis => thesis.scores.find(scores => scores.party === party.id).score)
|
||||
.reduce((total, score) => total + score, 0)
|
||||
}
|
||||
|
||||
function getMaxScorePerThesis (userPosition, emphasis) {
|
||||
return userPosition === 'skipped' ? MIN_POINTS : emphasis ? MAX_POINTS * EMPHASIS_POINTS : MAX_POINTS
|
||||
}
|
||||
|
||||
function getUserPosition (row) {
|
||||
return row.positions.find(p => p.type === 'user').position
|
||||
}
|
||||
|
||||
// Grid example:
|
||||
// [
|
||||
// {
|
||||
|
@ -33,15 +72,48 @@ export function getPartyPositions (thesis) {
|
|||
// },
|
||||
// ...
|
||||
// ]
|
||||
export function getScoringGrid (userAnswers, emphasizedTheses) {
|
||||
function getScoringGrid (userAnswers, emphasizedTheses, parties) {
|
||||
return userAnswers.map(answer => (
|
||||
{
|
||||
thesis: answer.thesis,
|
||||
emphasis: emphasizedTheses.filter(e => e.thesis === answer.thesis).length >= 1,
|
||||
positions: [
|
||||
...getPartyPositions(answer.thesis),
|
||||
...getPartyPositions(answer.thesis, parties),
|
||||
...[{ type: 'user', position: answer.position }]
|
||||
]
|
||||
}
|
||||
))
|
||||
}
|
||||
|
||||
function getPartyPositions (thesis, parties) {
|
||||
return parties.map(party => {
|
||||
const position = party.positions.find(p => p.thesis === thesis)
|
||||
return {
|
||||
type: 'party',
|
||||
party: party.id,
|
||||
position: (position && position.position) || {}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export function evalPointsPerThesisPerParty (partyPosition, userPosition, emphasis) {
|
||||
let score = 0
|
||||
|
||||
if (userPosition === partyPosition) {
|
||||
score = MAX_POINTS
|
||||
} else if (
|
||||
(userPosition === 'positive' && partyPosition === 'neutral') ||
|
||||
(userPosition === 'neutral' && partyPosition === 'positive') ||
|
||||
(userPosition === 'neutral' && partyPosition === 'negative') ||
|
||||
(userPosition === 'negative' && partyPosition === 'neutral')
|
||||
) {
|
||||
score = BASE_POINTS
|
||||
} else if (
|
||||
(userPosition === 'positive' && partyPosition === 'negative') ||
|
||||
(userPosition === 'negative' && partyPosition === 'positive')
|
||||
) {
|
||||
score = MIN_POINTS
|
||||
}
|
||||
|
||||
return emphasis ? score * EMPHASIS_POINTS : score
|
||||
}
|
||||
|
|
|
@ -0,0 +1,109 @@
|
|||
import { evalPointsPerThesisPerParty, getPartiesWithScores, getTotalMaxPoints } from '@/app/euromat/scoring'
|
||||
|
||||
const positionDef = ['positive', 'neutral', 'negative', 'skipped']
|
||||
// See offficial Rechenmodell of bpb from 2019
|
||||
const rechenmodellGrid = [
|
||||
[0, 0, 0, 0, 0, false],
|
||||
[0, 0, 2, 0, 1, false],
|
||||
[0, 2, 2, 0, 0, false],
|
||||
[0, 0, 0, 0, 2, false],
|
||||
[1, 1, 0, 2, 2, true],
|
||||
[0, 0, 2, 0, 0, false],
|
||||
[0, 2, 2, 1, 0, false],
|
||||
[0, 0, 0, 2, 2, false],
|
||||
[2, 0, 2, 2, 1, false],
|
||||
[1, 2, 2, 2, 1, false],
|
||||
[2, 2, 0, 0, 2, false],
|
||||
[0, 2, 2, 2, 2, false],
|
||||
[0, 2, 0, 0, 3, false],
|
||||
[0, 2, 1, 2, 2, false],
|
||||
[2, 0, 0, 1, 2, false],
|
||||
[2, 0, 0, 0, 2, true],
|
||||
[2, 0, 0, 2, 3, false],
|
||||
[0, 0, 0, 0, 2, false],
|
||||
[2, 0, 0, 0, 1, false],
|
||||
[2, 0, 0, 0, 1, false],
|
||||
[0, 0, 2, 2, 2, true],
|
||||
[2, 2, 1, 2, 2, false],
|
||||
[2, 2, 0, 2, 2, false],
|
||||
[2, 2, 2, 0, 2, false],
|
||||
[0, 2, 0, 2, 1, false],
|
||||
[1, 2, 2, 0, 0, false],
|
||||
[2, 0, 2, 0, 0, false],
|
||||
[2, 0, 0, 2, 2, false],
|
||||
[0, 2, 0, 2, 1, false],
|
||||
[1, 2, 0, 2, 2, false],
|
||||
[0, 1, 2, 0, 0, false],
|
||||
[1, 2, 2, 0, 1, false],
|
||||
[2, 0, 2, 2, 1, false],
|
||||
[0, 2, 2, 2, 2, false],
|
||||
[0, 2, 0, 0, 0, false],
|
||||
[0, 2, 0, 0, 1, false],
|
||||
[2, 2, 2, 2, 0, false],
|
||||
[2, 0, 0, 2, 0, false]
|
||||
]
|
||||
|
||||
const rechenmodellExpectedTotalScorePerParty = [44, 37, 28, 50]
|
||||
const rechenmodellExpectedMaxScore = 78
|
||||
|
||||
const testParties = [1, 2, 3, 4].map(partyId => ({
|
||||
id: partyId,
|
||||
token: 'idontcare',
|
||||
positions: rechenmodellGrid.map((thesis, index) => ({
|
||||
thesis: index,
|
||||
position: positionDef[thesis[partyId - 1]]
|
||||
}))
|
||||
}))
|
||||
|
||||
const testAnswers = rechenmodellGrid.map((thesis, index) => ({
|
||||
position: positionDef[thesis[4]],
|
||||
thesis: index
|
||||
}))
|
||||
|
||||
const testEmphasis = rechenmodellGrid
|
||||
.map((thesis, index) => ({
|
||||
...thesis,
|
||||
thesis: index
|
||||
}))
|
||||
.filter((thesis, index) => thesis[5])
|
||||
.map(thesis => ({ thesis: thesis.thesis }))
|
||||
|
||||
describe('The getPartiesWithScores function', () => {
|
||||
it('returns the correct total scores according to the Rechenmodell example of bpb', () => {
|
||||
const resultPartiesWithScores = getPartiesWithScores(testAnswers, testEmphasis, testParties)
|
||||
|
||||
expect(resultPartiesWithScores.map(party => party.score)).toEqual(rechenmodellExpectedTotalScorePerParty)
|
||||
})
|
||||
})
|
||||
|
||||
describe('The getTotalMaxPoints function', () => {
|
||||
it('returns the correct maximum score points according to the Rechenmodell example of bpb', () => {
|
||||
const resultTotalMaxPoints = getTotalMaxPoints(testAnswers, testEmphasis)
|
||||
|
||||
expect(resultTotalMaxPoints).toEqual(rechenmodellExpectedMaxScore)
|
||||
})
|
||||
})
|
||||
|
||||
describe('The evalPointsPerThesisPerParty fucntion', () => {
|
||||
test.each`
|
||||
partyPosition | userPosition | expectedScore | expectedScoreWithEmphasis
|
||||
${'negative'} | ${'negative'} | ${2} | ${4}
|
||||
${'negative'} | ${'neutral'} | ${1} | ${2}
|
||||
${'negative'} | ${'positive'} | ${0} | ${0}
|
||||
${'neutral'} | ${'negative'} | ${1} | ${2}
|
||||
${'neutral'} | ${'neutral'} | ${2} | ${4}
|
||||
${'neutral'} | ${'positive'} | ${1} | ${2}
|
||||
${'positive'} | ${'negative'} | ${0} | ${0}
|
||||
${'positive'} | ${'neutral'} | ${1} | ${2}
|
||||
${'positive'} | ${'positive'} | ${2} | ${4}
|
||||
${'positive'} | ${'skipped'} | ${0} | ${0}
|
||||
`('returns the correct score (according to the Rechenmodell of the bpb' +
|
||||
' if the party position is $partyPosition and the user\'s position is $userPosition',
|
||||
({ partyPosition, userPosition, expectedScore, expectedScoreWithEmphasis }) => {
|
||||
const resultScore = evalPointsPerThesisPerParty(partyPosition, userPosition, false)
|
||||
const resultScoreWithEmphasis = evalPointsPerThesisPerParty(partyPosition, userPosition, true)
|
||||
|
||||
expect(resultScore).toEqual(expectedScore)
|
||||
expect(resultScoreWithEmphasis).toEqual(expectedScoreWithEmphasis)
|
||||
})
|
||||
})
|
Loading…
Reference in New Issue