{
  "schemaVersion": 1,
  "slug": "grade-calculator",
  "category": "education",
  "kind": "implementation",
  "independentValidation": false,
  "implementationHash": "8bfd95394d19017f46bbb0499c07d36ebbf77ff6e16358055b54ad79b8a3eb86",
  "provenance": "Generated from the calculation contract used by the published page. Publication is not external validation.",
  "sourceFiles": [
    "data/calculators/grade-calculator.json",
    "src/engine.js"
  ],
  "runtimeSource": "/sources/engine.txt",
  "runtimeHash": "5c888d8898afbdee36267bb9977829e1e2ac568f6677c8ee87469c139864645c",
  "scope": "The compute function records implemented equations, constants, lookup tables and custom rules. Inputs record defaults and allowed values; outputs record precision and units. Defaults are examples, not recommendations. General background references are not evidence for this specific algorithm.",
  "publishedFormula": {
    "formula": "G = Σ(s_j w_j)/Σw_j + E",
    "vars": [
      [
        "s_j",
        "Scores of each graded category"
      ],
      [
        "w_j",
        "Weights of each category"
      ],
      [
        "E",
        "Extra credit"
      ],
      [
        "G",
        "Final weighted grade, %"
      ],
      [
        "C",
        "Cutoff for the next higher letter"
      ],
      [
        "N",
        "Score needed on the final"
      ]
    ],
    "note": "G is clamped to 0–120; letters use the selected cutoff scale; N = (C×W − B)/w_f − E×W/w_f, where W = Σw_j and B = points earned before the final."
  },
  "externalEvidence": null,
  "contract": {
    "compute": "const tw = assignmentWeight + quizWeight + midtermWeight + finalWeight;\nconst base = assignmentScore*assignmentWeight + quizScore*quizWeight + midtermScore*midtermWeight;\nconst grade = tw > 0 ? (base + finalScore*finalWeight)/tw + extraCredit : 0;\nconst g = Math.max(0, Math.min(120, grade));\nconst scaleMap = scale === 'plusminus'\n  ? [[93,'A'],[90,'A-'],[87,'B+'],[83,'B'],[80,'B-'],[77,'C+'],[73,'C'],[70,'C-'],[67,'D+'],[63,'D'],[60,'D-'],[0,'F']]\n  : scale === 'strict'\n  ? [[93,'A'],[85,'B'],[77,'C'],[70,'D'],[0,'F']]\n  : [[90,'A'],[80,'B'],[70,'C'],[60,'D'],[0,'F']];\nconst hit = scaleMap.find ? scaleMap.find(function(p){ return g >= p[0]; }) : null;\nconst letter = (hit || [0,'F'])[1];\nconst gpaMap = {A:4,'A-':3.7,'B+':3.3,B:3,'B-':2.7,'C+':2.3,C:2,'C-':1.7,'D+':1.3,D:1,'D-':0.7,F:0};\nconst gpa = gpaMap[letter] === undefined ? 0 : gpaMap[letter];\nconst higher = scaleMap.filter(function(p){ return p[0] > g; });\nconst nextCut = higher.length ? Math.min.apply(null, higher.map(function(p){ return p[0]; })) : 100;\nconst nextLetter = higher.length ? higher[higher.length - 1][1] : 'the top letter grade';\nconst needed = higher.length && finalWeight > 0 ? (nextCut*tw - base)/finalWeight - (extraCredit*tw/finalWeight) : 0;\nconst best = tw > 0 ? (base + 100*finalWeight)/tw + extraCredit : 0;\nconst worst = tw > 0 ? base/tw + extraCredit : 0;\nconst advice = tw === 0 ? 'Enter the weights for your graded categories to see a result.'\n  : tw !== 100 ? 'Your weights add up to ' + tw + ' per cent, not 100 — check the syllabus before trusting the total.'\n  : !higher.length ? 'You have already reached the top letter grade on the selected scale.'\n  : needed <= 0 ? 'You have already secured the next grade up, even if you score zero on the final.'\n  : needed > 100 ? 'The next grade up is out of reach on this exam alone.'\n  : 'You need ' + needed.toFixed(1) + ' per cent on the final to reach ' + nextLetter + ' (score ' + nextCut + ' overall).';\nreturn { weightedGrade: g, letterGrade: letter, gpaPoints: gpa, totalWeight: tw, bestCase: Math.max(0, Math.min(120, best)), worstCase: Math.max(0, Math.min(120, worst)), pointsNeededOnFinal: needed, nextLetter: nextLetter, advice: advice };",
    "inputs": [
      {
        "name": "assignmentScore",
        "type": "number",
        "default": 88,
        "min": 0,
        "max": 120,
        "step": 0.5,
        "slider": true
      },
      {
        "name": "assignmentWeight",
        "type": "number",
        "default": 30,
        "min": 0,
        "max": 100,
        "step": 1,
        "slider": true
      },
      {
        "name": "quizScore",
        "type": "number",
        "default": 92,
        "min": 0,
        "max": 120,
        "step": 0.5,
        "slider": true
      },
      {
        "name": "quizWeight",
        "type": "number",
        "default": 10,
        "min": 0,
        "max": 100,
        "step": 1,
        "slider": true
      },
      {
        "name": "midtermScore",
        "type": "number",
        "default": 78,
        "min": 0,
        "max": 120,
        "step": 0.5,
        "slider": true
      },
      {
        "name": "midtermWeight",
        "type": "number",
        "default": 25,
        "min": 0,
        "max": 100,
        "step": 1,
        "slider": true
      },
      {
        "name": "finalScore",
        "type": "number",
        "default": 85,
        "min": 0,
        "max": 120,
        "step": 0.5,
        "slider": true
      },
      {
        "name": "finalWeight",
        "type": "number",
        "default": 35,
        "min": 0,
        "max": 100,
        "step": 1,
        "slider": true
      },
      {
        "name": "extraCredit",
        "type": "number",
        "default": 0,
        "min": 0,
        "max": 30,
        "step": 0.5,
        "slider": true,
        "advanced": true
      },
      {
        "name": "scale",
        "type": "select",
        "default": "standard",
        "options": [
          {
            "value": "standard"
          },
          {
            "value": "plusminus"
          },
          {
            "value": "strict"
          }
        ],
        "advanced": true
      }
    ],
    "outputs": [
      {
        "name": "weightedGrade",
        "precision": 2,
        "unit": "%"
      },
      {
        "name": "letterGrade",
        "precision": 0,
        "unit": "",
        "type": "text"
      },
      {
        "name": "gpaPoints",
        "precision": 2,
        "unit": ""
      },
      {
        "name": "totalWeight",
        "precision": 0,
        "unit": "%"
      },
      {
        "name": "bestCase",
        "precision": 2,
        "unit": "%"
      },
      {
        "name": "worstCase",
        "precision": 2,
        "unit": "%"
      },
      {
        "name": "pointsNeededOnFinal",
        "precision": 1,
        "unit": "%"
      },
      {
        "name": "nextLetter",
        "precision": 0,
        "unit": "",
        "type": "text"
      },
      {
        "name": "advice",
        "precision": 0,
        "unit": "",
        "type": "text"
      }
    ],
    "interpret": null
  }
}
