Skip to content

Commit

Permalink
Handle missing numeric input answer values
Browse files Browse the repository at this point in the history
  • Loading branch information
benchristel committed Nov 27, 2024
1 parent 4172a32 commit 2916acb
Show file tree
Hide file tree
Showing 5 changed files with 233 additions and 3 deletions.
2 changes: 1 addition & 1 deletion packages/perseus/src/perseus-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1116,7 +1116,7 @@ export type PerseusNumericInputAnswer = {
// Translatable Display; A description for why this answer is correct, wrong, or ungraded
message: string;
// The expected answer
value: number;
value?: number;
// Whether this answer is "correct", "wrong", or "ungraded"
status: string;
// The forms available for this answer. Options: "integer, ""decimal", "proper", "improper", "mixed", or "pi"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ export const parseNumericInputWidget: Parser<NumericInputWidget> = parseWidget(
answers: array(
object({
message: string,
value: number,
value: optional(number),
status: string,
answerForms: optional(array(parseMathFormat)),
strict: boolean,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
{
"question": {
"content": "$\\bigg(\\dfrac{2}{3} \\bigg)^3 =$ [[☃ expression 1]]",
"images": {},
"widgets": {
"numeric-input 1": {
"type": "numeric-input",
"alignment": "default",
"static": false,
"graded": true,
"options": {
"answers": [
{
"status": "correct",
"message": "",
"simplify": "required",
"strict": false,
"maxError": null,
"answerForms": [
"proper",
"improper"
]
}
],
"size": "normal",
"coefficient": false,
"labelText": "",
"rightAlign": false,
"static": false,
"multipleNumberInput": false
},
"version": {
"major": 0,
"minor": 0
}
},
"expression 1": {
"options": {
"answerForms": [
{
"considered": "correct",
"form": true,
"key": 0,
"simplify": true,
"value": "\\frac{8}{27}",
"times": false,
"functions": [
"f",
"g",
"h"
],
"buttonSets": [
"basic"
],
"buttonsVisible": "focused",
"linterContext": {
"contentType": "",
"highlightLint": false,
"paths": [],
"stack": []
}
}
],
"times": false,
"buttonSets": [
"basic"
],
"functions": [
"f",
"g",
"h"
],
"static": false
},
"type": "expression",
"version": {
"major": 1,
"minor": 0
},
"graded": true,
"alignment": "default",
"static": false
}
}
},
"answerArea": {
"calculator": false,
"chi2Table": false,
"financialCalculatorMonthlyPayment": false,
"financialCalculatorTotalAmount": false,
"financialCalculatorTimeToPayOff": false,
"periodicTable": false,
"periodicTableWithKey": false,
"tTable": false,
"zTable": false
},
"itemDataVersion": {
"major": 0,
"minor": 1
},
"hints": [
{
"replace": false,
"content": "$\\bigg(\\dfrac{2}{3} \\bigg)^3 =\\dfrac{2}{3}\\times \\dfrac{2}{3}\\times\\dfrac{2}{3}$\n\nLet's perform this multiplication to find the answer.",
"images": {},
"widgets": {}
},
{
"replace": false,
"content": "On multiplying, we see that\n\n$\\dfrac{2}{3}\\times \\dfrac{2}{3}\\times\\dfrac{2}{3}=\\boxed{\\dfrac{8}{27}}$.",
"images": {},
"widgets": {}
}
]
}
115 changes: 115 additions & 0 deletions packages/perseus/src/widgets/numeric-input/score-numeric-input.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -287,6 +287,121 @@ describe("static function validate", () => {
// Assert - "incomplete"
expect(score).toHaveBeenAnsweredCorrectly();
});

it("ignores missing values when determining if the answer can be formatted as a percentage", () => {
const rubric: PerseusNumericInputRubric = {
answers: [
{
// This answer is missing its value field.
status: "correct",
maxError: 0,
simplify: "",
strict: false,
message: "",
},
{
// This is the actual correct answer
value: 0.5,
status: "correct",
maxError: 0,
simplify: "",
strict: false,
message: "",
},
],
coefficient: true,
};

const score = scoreNumericInput({currentValue: "50%"}, rubric, mockStrings);

expect(score).toHaveBeenAnsweredCorrectly();
})

it("converts a percentage input value to a decimal", () => {
const rubric: PerseusNumericInputRubric = {
answers: [
{
value: 0.2,
status: "correct",
maxError: 0,
simplify: "",
strict: false,
message: "",
},
],
coefficient: true,
};

const score = scoreNumericInput({currentValue: "20%"}, rubric, mockStrings);

expect(score).toHaveBeenAnsweredCorrectly();
});

it("rejects percentages greater than 100%", () => {
// TODO(benchristel): This seems like incorrect behavior. I've added
// this test to characterize the current behavior. Feel free to
// delete/change it if it's in your way.
const rubric: PerseusNumericInputRubric = {
answers: [
{
value: 1.2,
status: "correct",
maxError: 0,
simplify: "",
strict: false,
message: "",
},
],
coefficient: true,
};

const score = scoreNumericInput({currentValue: "120%"}, rubric, mockStrings);

expect(score).toHaveBeenAnsweredIncorrectly();
});

it("accepts answers with an extra, incorrect percent sign if > 1", () => {
// TODO(benchristel): This seems like incorrect behavior. I've added
// this test to characterize the current behavior. Feel free to
// delete/change it if it's in your way.
const rubric: PerseusNumericInputRubric = {
answers: [
{
value: 1.1,
status: "correct",
maxError: 0,
simplify: "",
strict: false,
message: "",
},
],
coefficient: true,
};

const score = scoreNumericInput({currentValue: "1.1%"}, rubric, mockStrings);

expect(score).toHaveBeenAnsweredCorrectly();
});

it("rejects answers with an extra, incorrect percent sign if < 1", () => {
const rubric: PerseusNumericInputRubric = {
answers: [
{
value: 0.9,
status: "correct",
maxError: 0,
simplify: "",
strict: false,
message: "",
},
],
coefficient: true,
};

const score = scoreNumericInput({currentValue: "0.9%"}, rubric, mockStrings);

expect(score).toHaveBeenAnsweredIncorrectly();
});
});

describe("maybeParsePercentInput utility function", () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ function scoreNumericInput(

const normalizedAnswerExpected = rubric.answers
.filter((answer) => answer.status === "correct")
.every((answer) => Math.abs(answer.value) <= 1);
.every((answer) => answer.value == null || Math.abs(answer.value) <= 1);

// The coefficient is an attribute of the widget
let localValue: string | number = currentValue;
Expand Down

0 comments on commit 2916acb

Please sign in to comment.