German schools grade on a 1–6 scale (1 = sehr gut, 6 = ungenügend), but tests are scored in points. The mapping between the two is called a Notenschlüssel (grading key), and teachers build one for almost every exam — often by hand in a spreadsheet.
Notenschlüsselrechner is a free, no-login tool that does this: enter the maximum points, choose a linear key for school or the IHK scale for vocational training, and get the full table, a grade distribution (Notenspiegel) and export. There's a teacher mode and a student mode. The logic is small, but the details matter — here's how it works.
Keys are just thresholds
Every grading key is a list of minimum percentages per grade. Model it as data:
type Grade = 1 | 2 | 3 | 4 | 5 | 6;
type Key = { grade: Grade; minPct: number }[]; // sorted from best to worst
// IHK 100-point scale, common in German vocational training
export const IHK: Key = [
{ grade: 1, minPct: 92 },
{ grade: 2, minPct: 81 },
{ grade: 3, minPct: 67 },
{ grade: 4, minPct: 50 },
{ grade: 5, minPct: 30 },
{ grade: 6, minPct: 0 },
];
A linear key from one decision
For a linear school key, the teacher usually decides one thing: the pass mark (the percentage needed for a 4). The remaining grades are spaced evenly between that and 100%:
export function linearKey(passPct = 50): Key {
const step = (100 - passPct) / 4; // grades 1–4 share the top band
const top: Key = [1, 2, 3, 4].map((g, i) => ({
grade: g as Grade,
minPct: +(100 - step * (i + 1)).toFixed(2),
}));
return [...top, { grade: 5, minPct: passPct / 2 }, { grade: 6, minPct: 0 }];
}
With a 50% pass mark that gives 87.5 / 75 / 62.5 / 50 / 25 / 0. Schools differ, which is exactly why the thresholds are data rather than hardcoded logic.
Half points and rounding
Tests are often scored in half points, and the boundary rule matters: a teacher expects "you need 34.5 of 37 points for a 1", not "34.04". Convert percentages to point thresholds and round up to the next half point, so nobody gets a better grade than the key allows:
const ceilHalf = (x: number) => Math.ceil(x * 2) / 2;
export function pointTable(maxPoints: number, key: Key) {
return key.map(({ grade, minPct }, i) => {
const from = ceilHalf((minPct / 100) * maxPoints);
const to = i === 0 ? maxPoints : ceilHalf((key[i - 1].minPct / 100) * maxPoints) - 0.5;
return { grade, from, to };
});
}
For 50 points on the IHK scale, a 1 starts at 46 points (92% of 50).
Looking up a grade
export function gradeFor(points: number, maxPoints: number, key: Key): Grade {
const pct = (points / maxPoints) * 100;
return key.find((k) => pct >= k.minPct)!.grade;
}
Test the boundaries explicitly — exactly on the threshold, half a point below, zero, and full marks.
The Notenspiegel
Once you have grades, the class distribution and average are one reduce away:
export function notenspiegel(grades: Grade[]) {
const counts = [1, 2, 3, 4, 5, 6].map((g) => grades.filter((x) => x === g).length);
const avg = grades.reduce((a, b) => a + b, 0) / grades.length;
return { counts, average: +avg.toFixed(2) };
}
Formatting for Germany
Decimal commas matter: a class average is "2,43", not "2.43". Use Intl.NumberFormat("de-DE") everywhere numbers are displayed or exported.
Takeaways
- Represent grading keys as threshold data.
- Derive linear keys from the one decision teachers actually make.
- Round point thresholds up to the next half point.
- Test boundaries, and format numbers for the locale.
Try it at notenschluesselrechner.de (in German). Have you built grading tools for other school systems?
Top comments (0)