Last active
April 21, 2023 15:58
-
-
Save chrcit/8c7b2f81e8f6dc608fd97c7b43e96066 to your computer and use it in GitHub Desktop.
Code for calculating the score for the "SPÖ Vorsitzbefragungs-Kabine" on mitentscheiden.at
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { QuestionType } from "@prisma/client"; | |
export type ScaleOptionValueType = typeof options[number]["value"]; | |
export const getScaleOptionTendency = (option: ScaleOptionValueType) => { | |
return option > 0 ? "positive" : "negative"; | |
}; | |
export const options = [ | |
{ | |
label: "voll zu", | |
value: 2, | |
}, | |
{ | |
label: "eher zu", | |
value: 1, | |
}, | |
{ | |
label: "eher nicht zu", | |
value: -1, | |
}, | |
{ | |
label: "gar nicht zu", | |
value: -2, | |
}, | |
] as const; | |
export const yesNoOptions = [ | |
{ | |
label: "Ja", | |
value: 3, // Settings a different value to be able to differnentiate. Can normalise afterwards. | |
}, | |
{ | |
label: "Nein", | |
value: -3, // Settings a different value to be able to differnentiate. Can normalise afterwards. | |
}, | |
] as const; | |
export const getOptionsBasedOnType = (type: QuestionType) => { | |
switch (type) { | |
case "YesNo": | |
return yesNoOptions; | |
case "Range": | |
return options; | |
default: | |
return options; | |
} | |
}; | |
export const optionLabelForValue = (value: number) => { | |
const option = options.find((option) => option.value === value); | |
return option ? option.label : ""; | |
}; | |
export const optionLabelForYesNoValue = (value: number) => { | |
const option = yesNoOptions.find((option) => option.value === value); | |
return option ? option.label : ""; | |
}; | |
export const getWeightingTendency = (weighting: WeightingValueType) => { | |
if (weighting === 0 || weighting === 1) return "negative"; | |
if (weighting === 2 || weighting === 3) return "positive"; | |
}; | |
export type WeightingValueType = typeof weightings[number]["value"]; | |
export const weightings = [ | |
{ | |
label: "sehr wichtig", | |
value: 3, | |
}, | |
{ | |
label: "eher wichtig", | |
value: 2, | |
}, | |
{ | |
label: "eher nicht wichtig", | |
value: 1, | |
}, | |
{ | |
label: "gar nicht wichtig", | |
value: 0, | |
}, | |
] as const; | |
export const weightingLabelForValue = (value: number) => { | |
const weighting = weightings.find((weighting) => weighting.value === value); | |
return weighting ? weighting.label : ""; | |
}; | |
export const CATEGORIES = [ | |
{ | |
label: "Klima & Umwelt", | |
hex: "#38be23", | |
}, | |
{ | |
label: "Frauen", | |
hex: "#2766d4", | |
}, | |
{ | |
label: "Demokratie & Mitbestimmung", | |
hex: "#EB5757", | |
}, | |
{ | |
label: "Arbeit & Soziales", | |
hex: "#e8ce26", | |
}, | |
{ | |
label: "Internes", | |
hex: "#6FCF97", | |
}, | |
{ | |
label: "Europa & Internationales", | |
hex: "#BB6BD9", | |
}, | |
{ | |
label: "Bildung", | |
hex: "#c16926", | |
}, | |
{ | |
label: "Zusatz", | |
hex: "#485d7c", | |
}, | |
]; | |
export const categoryHexForLabel = (label: string) => { | |
const category = CATEGORIES.find((category) => category.label === label); | |
return category ? category.hex : ""; | |
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { | |
ScaleOptionValueType, | |
WeightingValueType, | |
getScaleOptionTendency, | |
getWeightingTendency, | |
} from "~/data/answers"; | |
import { | |
CandidateQuestionAnswer, | |
Question, | |
VoterQuestionAnswer, | |
} from "@prisma/client"; | |
const calculateWeightingRelevancy = ( | |
voterWeighting: WeightingValueType, | |
candidateWeighting: WeightingValueType | |
) => { | |
if (voterWeighting === candidateWeighting) return 0.15; | |
if ( | |
getWeightingTendency(voterWeighting) === | |
getWeightingTendency(candidateWeighting) | |
) { | |
return 0.075; | |
} | |
return 0; | |
}; | |
const calculateMatchForScaleOption = ( | |
voterAnswer: ScaleOptionValueType, | |
candidateAnswer: ScaleOptionValueType | |
) => { | |
if (voterAnswer === candidateAnswer) return 1; | |
if ( | |
getScaleOptionTendency(voterAnswer) === | |
getScaleOptionTendency(candidateAnswer) | |
) { | |
return 0.7; | |
} | |
return 0; | |
}; | |
export const calculateScore = ( | |
voterAnswers: (VoterQuestionAnswer & { | |
question: Question; | |
})[], | |
candidateAnswers: (CandidateQuestionAnswer & { | |
question: Question; | |
})[] | |
) => { | |
let score = 0; | |
voterAnswers.forEach((voterAnswer) => { | |
const candidateAnswer = candidateAnswers.find( | |
(candidateAnswer) => candidateAnswer.questionId === voterAnswer.questionId | |
); | |
if ( | |
voterAnswer.skipped || | |
(candidateAnswer?.option === null && candidateAnswer.weighting === null) | |
) { | |
return; | |
} | |
if (voterAnswer.question.type === "YesNo") { | |
if (voterAnswer.option === candidateAnswer?.option) { | |
score += 1; | |
} | |
} | |
if (voterAnswer.question.type === "Range") { | |
const matchScore = calculateMatchForScaleOption( | |
voterAnswer.option! as ScaleOptionValueType, | |
candidateAnswer!.option! as ScaleOptionValueType | |
); | |
score += matchScore; | |
} | |
const weightingRelevancy = calculateWeightingRelevancy( | |
voterAnswer.weighting! as WeightingValueType, | |
candidateAnswer!.weighting! as WeightingValueType | |
); | |
score += weightingRelevancy; | |
}); | |
return score; | |
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { | |
Candidate, | |
CandidateQuestionAnswer, | |
Question, | |
VoterQuestionAnswer, | |
} from "@prisma/client"; | |
import { calculateScore } from "~/data/calucate-score"; | |
type VoterAnswer = VoterQuestionAnswer & { | |
question: Question; | |
}; | |
type CandidateWithAnswers = Candidate & { | |
answers: (CandidateQuestionAnswer & { | |
question: Question; | |
})[]; | |
}; | |
const getAnsweredQuestionsLength = ( | |
voterAnswers: VoterAnswer[], | |
candidateAnswers: CandidateWithAnswers["answers"] | |
) => { | |
return candidateAnswers.filter((answer) => { | |
const voterAnswer = voterAnswers.find( | |
(voterAnswer) => voterAnswer.questionId === answer.questionId | |
); | |
return ( | |
!voterAnswer?.skipped && | |
answer.option !== null && | |
answer.weighting !== null | |
); | |
}).length; | |
}; | |
export const rateCandidate = ( | |
voterAnswers: VoterAnswer[], | |
candidate: CandidateWithAnswers | |
) => { | |
const maxScore = | |
getAnsweredQuestionsLength(voterAnswers, candidate.answers) * 1.15; | |
const score = calculateScore(voterAnswers, candidate.answers); | |
console.log({ score, maxScore }); | |
const scorePercentageRaw = maxScore !== 0 ? (score / maxScore) * 100 : 0; | |
const scorePercentage = Math.round(scorePercentageRaw); | |
return { | |
...candidate, | |
score, | |
scorePercentage, | |
scorePercentageRaw, | |
}; | |
}; | |
export const rateCandidates = ( | |
voterAnswers: VoterAnswer[], | |
candidates: CandidateWithAnswers[] | |
) => { | |
return candidates | |
.map((candidate) => rateCandidate(voterAnswers, candidate)) | |
.sort((a, b) => b.score - a.score); | |
}; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment