{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "quiz-lib",
  "title": "Quiz helpers",
  "description": "Pure helpers for a tap-to-answer quiz: grade a pick, tally a set of picks, build an empty pick list, and check that your questions are well formed.",
  "files": [
    {
      "path": "registry/crisp/lib/quiz.ts",
      "content": "/**\n * Pure helpers for the quiz pattern: grade one pick, tally a set of picks, and\n * check that a question set is well formed. No React and no network, so they\n * are safe to unit test and to run on a server.\n *\n * A pick is the index of the chosen option, or `null` while unanswered. Every\n * helper returns a new value and never changes what it is given.\n */\n\n/** The part of a question the helpers need. The component's `QuizQuestion` fits it. */\nexport interface GradableQuestion {\n  options: readonly unknown[]\n  /** Index into `options` of the one right answer. */\n  answer: number\n}\n\nexport interface QuizTally {\n  total: number\n  /** Questions with a pick. */\n  answered: number\n  /** Picks that matched the answer. */\n  correct: number\n  /** Every question has a pick. An empty quiz is never complete. */\n  complete: boolean\n}\n\n/** \"A\" for 0, \"B\" for 1, and so on. Wraps to \"AA\" after \"Z\". */\nexport function optionLetter(index: number): string {\n  if (!Number.isInteger(index) || index < 0) return \"\"\n  let n = index\n  let letter = \"\"\n  do {\n    letter = String.fromCharCode(65 + (n % 26)) + letter\n    n = Math.floor(n / 26) - 1\n  } while (n >= 0)\n  return letter\n}\n\n/** True when the pick is the answer. An unanswered pick is never correct. */\nexport function isCorrect(\n  question: Pick<GradableQuestion, \"answer\">,\n  pick: number | null\n): boolean {\n  return pick !== null && pick === question.answer\n}\n\n/**\n * Count answered and correct picks. `picks` is read by position; a missing\n * entry counts as unanswered, and extra entries are ignored.\n */\nexport function tally(\n  questions: readonly GradableQuestion[],\n  picks: readonly (number | null)[]\n): QuizTally {\n  let answered = 0\n  let correct = 0\n  questions.forEach((question, i) => {\n    const pick = picks[i] ?? null\n    if (pick === null) return\n    answered += 1\n    if (isCorrect(question, pick)) correct += 1\n  })\n  return {\n    total: questions.length,\n    answered,\n    correct,\n    complete: questions.length > 0 && answered === questions.length,\n  }\n}\n\n/** A fresh, all-unanswered pick list for `questions`. */\nexport function emptyPicks(questions: readonly unknown[]): null[] {\n  // Annotated so a project without `strictNullChecks` still gets `null[]`, not `any[]`.\n  return questions.map((): null => null)\n}\n\n/**\n * Problems that would make a question unanswerable or ambiguous, one message\n * per problem, each naming the question (1-based). An empty list means the set\n * is fine. Run it in a test or at build time against your own data.\n */\nexport function checkQuestions(\n  questions: readonly GradableQuestion[]\n): string[] {\n  const problems: string[] = []\n  questions.forEach((question, i) => {\n    const name = `Question ${i + 1}`\n    if (question.options.length < 2) {\n      problems.push(`${name}: needs at least two options`)\n    }\n    if (\n      !Number.isInteger(question.answer) ||\n      question.answer < 0 ||\n      question.answer >= question.options.length\n    ) {\n      problems.push(`${name}: answer ${question.answer} is not an option`)\n    }\n    const labels = question.options.filter(\n      (option): option is string => typeof option === \"string\"\n    )\n    const seen = new Set<string>()\n    for (const label of labels) {\n      const key = label.trim().toLowerCase()\n      if (seen.has(key)) {\n        problems.push(\n          `${name}: \"${label}\" appears twice, so two options are right`\n        )\n      }\n      seen.add(key)\n    }\n  })\n  return problems\n}\n",
      "type": "registry:lib"
    }
  ],
  "meta": {
    "stage": "confirm",
    "recipe": "https://realgood.site/docs/components/quiz.md"
  },
  "docs": "Pure helpers for a quiz: grade a pick, tally a set of picks, build an empty pick list, and check that your questions are well formed (checkQuestions).\nRead the installed file before using it; use only the props it exports.\nDocs: https://realgood.site/docs/components/quiz.md\nVerify: run your typechecker, and run checkQuestions in a test against your own questions.",
  "categories": [
    "education",
    "utilities"
  ],
  "type": "registry:lib"
}