develop-candymat #7

Merged
christoph.lienhard merged 10 commits from develop-candymat into main 2022-02-02 15:03:48 +01:00
5 changed files with 206 additions and 100 deletions
Showing only changes of commit f01d492d95 - Show all commits

View file

@ -1,7 +1,8 @@
module.exports = {
root: true,
env: {
node: true
node: true,
jest: true
},
'extends': [
'plugin:vue/recommended',

View file

@ -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/'
}

View file

@ -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)
}
}
}

View file

@ -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
}

View file

@ -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)
})
})