@khanacademy/perseus-score 7.4.0 → 7.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/es/index.js +29 -29
- package/dist/es/index.js.map +1 -1
- package/dist/index.js +29 -29
- package/dist/index.js.map +1 -1
- package/dist/widgets/categorizer/score-categorizer.d.ts +1 -1
- package/dist/widgets/categorizer/validate-categorizer.d.ts +1 -1
- package/dist/widgets/cs-program/score-cs-program.d.ts +1 -1
- package/dist/widgets/dropdown/score-dropdown.d.ts +1 -1
- package/dist/widgets/dropdown/validate-dropdown.d.ts +1 -1
- package/dist/widgets/expression/score-expression.d.ts +1 -1
- package/dist/widgets/expression/validate-expression.d.ts +1 -1
- package/dist/widgets/free-response/score-free-response.d.ts +1 -1
- package/dist/widgets/free-response/validate-free-response.d.ts +1 -1
- package/dist/widgets/grapher/score-grapher.d.ts +1 -1
- package/dist/widgets/group/score-group.d.ts +1 -1
- package/dist/widgets/group/validate-group.d.ts +1 -1
- package/dist/widgets/iframe/score-iframe.d.ts +1 -1
- package/dist/widgets/input-number/score-input-number.d.ts +1 -1
- package/dist/widgets/interactive-graph/score-interactive-graph.d.ts +1 -1
- package/dist/widgets/label-image/score-label-image.d.ts +1 -1
- package/dist/widgets/label-image/validate-label-image.d.ts +1 -1
- package/dist/widgets/matcher/score-matcher.d.ts +1 -1
- package/dist/widgets/matrix/score-matrix.d.ts +1 -1
- package/dist/widgets/matrix/validate-matrix.d.ts +1 -1
- package/dist/widgets/mock-widget/score-mock-widget.d.ts +1 -1
- package/dist/widgets/mock-widget/validate-mock-widget.d.ts +1 -1
- package/dist/widgets/number-line/score-number-line.d.ts +1 -1
- package/dist/widgets/numeric-input/score-numeric-input.d.ts +1 -1
- package/dist/widgets/orderer/score-orderer.d.ts +1 -1
- package/dist/widgets/orderer/validate-orderer.d.ts +1 -1
- package/dist/widgets/plotter/score-plotter.d.ts +1 -1
- package/dist/widgets/plotter/validate-plotter.d.ts +1 -1
- package/dist/widgets/radio/score-radio.d.ts +1 -1
- package/dist/widgets/radio/validate-radio.d.ts +1 -1
- package/package.json +3 -3
package/dist/es/index.js
CHANGED
|
@@ -7,51 +7,51 @@ const APPROXIMATED_PI_ERROR="APPROXIMATED_PI_ERROR";const CHOOSE_CORRECT_NUM_ERR
|
|
|
7
7
|
|
|
8
8
|
const MAXERROR_EPSILON=Math.pow(2,-42);const KhanAnswerTypes={predicate:{defaultForms:"integer, proper, improper, mixed, decimal",createValidatorFunctional:function(predicate,options){options=_.extend({simplify:"required",ratio:false,forms:KhanAnswerTypes.predicate.defaultForms},options);let acceptableForms;if(!_.isArray(options.forms)){acceptableForms=options.forms.split(/\s*,\s*/);}else {acceptableForms=options.forms;}if(options.inexact===undefined){options.maxError=0;}options.maxError=+options.maxError+MAXERROR_EPSILON;if(_.contains(acceptableForms,"percent")){acceptableForms=_.without(acceptableForms,"percent");acceptableForms.push("percent");}const fractionTransformer=function(text){text=text.replace(/\u2212/,"-").replace(/([+-])\s+/g,"$1").replace(/(^\s*)|(\s*$)/gi,"");const match=text.match(/^([+-]?\d+)\s*\/\s*([+-]?\d+)$/);const mobileDeviceMatch=text.match(/^([+-]?)\\frac\{([+-]?\d+)\}\{([+-]?\d+)\}$/);const parsedInt=parseInt(text,10);if(match||mobileDeviceMatch){let num;let denom;let simplified=true;if(match){num=parseFloat(match[1]);denom=parseFloat(match[2]);}else {num=parseFloat(mobileDeviceMatch[2]);if(mobileDeviceMatch[1]==="-"){if(num<0){simplified=false;}num=-num;}denom=parseFloat(mobileDeviceMatch[3]);}simplified=simplified&&denom>0&&(options.ratio||denom!==1)&&KhanMath.getGCD(num,denom)===1;return [{value:num/denom,exact:simplified}]}if(!isNaN(parsedInt)&&""+parsedInt===text){return [{value:parsedInt,exact:true}]}return []};const forms={integer:function(text){const decimal=forms.decimal(text);const rounded=forms.decimal(text,1);if(decimal[0]?.value!=null&&decimal[0].value===rounded[0]?.value||decimal[1]?.value!=null&&decimal[1].value===rounded[1]?.value){return decimal}return []},proper:function(text){const transformed=fractionTransformer(text);return transformed.flatMap(o=>{if(Math.abs(o.value)<1){return [o]}return []})},improper:function(text){const fractionExists=text.includes("/")||text.match(/\\(d?frac)/);if(!fractionExists){return []}const transformed=fractionTransformer(text);return transformed.flatMap(o=>{if(Math.abs(o.value)>=1){return [o]}return []})},pi:function(text){let match;let possibilities=[];text=text.replace(/\u2212/,"-");if(match=text.match(/^([+-]?)\s*(\\?pi|p|\u03c0|\\?tau|t|\u03c4|pau)$/i)){possibilities=[{value:parseFloat(match[1]+"1"),exact:true}];}else if(match=text.match(/^([+-]?\s*\d+\s*(?:\/\s*[+-]?\s*\d+)?)\s*\*?\s*(\\?pi|p|\u03c0|\\?tau|t|\u03c4|pau)$/i)){possibilities=fractionTransformer(match[1]);}else if(match=text.match(/^([+-]?)\s*(\d+)\s*([+-]?\d+)\s*\/\s*([+-]?\d+)\s*\*?\s*(\\?pi|p|\u03c0|\\?tau|t|\u03c4|pau)$/i)){const sign=parseFloat(match[1]+"1");const integ=parseFloat(match[2]);const num=parseFloat(match[3]);const denom=parseFloat(match[4]);const simplified=num<denom&&KhanMath.getGCD(num,denom)===1;possibilities=[{value:sign*(integ+num/denom),exact:simplified}];}else if(match=text.match(/^([+-]?\s*\d+)\s*\*?\s*(\\?pi|p|\u03c0|\\?tau|t|\u03c4|pau)\s*(?:\/\s*([+-]?\s*\d+))?$/i)){possibilities=fractionTransformer(match[1]+"/"+match[3]);}else if(match=text.match(/^([+-]?)\s*\*?\s*(\\?pi|p|\u03c0|\\?tau|t|\u03c4|pau)\s*(?:\/\s*([+-]?\d+))?$/i)){possibilities=fractionTransformer(match[1]+"1/"+match[3]);}else if(text==="0"){possibilities=[{value:0,exact:true}];}else if(match=text.match(/^(.+)\s*\*?\s*(\\?pi|p|\u03c0|\\?tau|t|\u03c4|pau)$/i)){possibilities=forms.decimal(match[1]);}else {possibilities=_.reduce(KhanAnswerTypes.predicate.defaultForms.split(/\s*,\s*/),function(memo,form){return memo.concat(forms[form](text))},[]);let approximatesPi=false;const number=parseFloat(text);if(!isNaN(number)&&number!==parseInt(text)){const piMult=Math.PI/12;const roundedNumber=piMult*Math.round(number/piMult);if(Math.abs(number-roundedNumber)<.01){approximatesPi=true;}}else if(text.match(/\/\s*7/)){approximatesPi=true;}if(approximatesPi){_.each(possibilities,function(possibility){possibility.piApprox=true;});}return possibilities}let multiplier=Math.PI;if(text.match(/\\?tau|t|\u03c4/)){multiplier=Math.PI*2;}if(text.match(/pau/)){multiplier=Math.PI*1.5;}possibilities.forEach(possibility=>{possibility.value*=multiplier;});return possibilities},coefficient:function(text){let possibilities=[];text=text.replace(/\u2212/,"-");if(text===""){possibilities=[{value:1,exact:true}];}else if(text==="-"){possibilities=[{value:-1,exact:true}];}return possibilities},log:function(text){let match;let possibilities=[];text=text.replace(/\u2212/,"-");text=text.replace(/[ \(\)]/g,"");if(match=text.match(/^log\s*(\S+)\s*$/i)){possibilities=forms.decimal(match[1]);}else if(text==="0"){possibilities=[{value:0,exact:true}];}return possibilities},percent:function(text){text=String(text).trim();let hasPercentSign=false;if(text.indexOf("%")===text.length-1){text=text.substring(0,text.length-1).trim();hasPercentSign=true;}const transformed=forms.decimal(text);transformed.forEach(t=>{t.exact=hasPercentSign;t.value=t.value/100;});return transformed},mixed:function(text){const match=text.replace(/\u2212/,"-").replace(/([+-])\s+/g,"$1").match(/^([+-]?)(\d+)\s+(\d+)\s*\/\s*(\d+)$/);if(match){const sign=parseFloat(match[1]+"1");const integ=parseFloat(match[2]);const num=parseFloat(match[3]);const denom=parseFloat(match[4]);const simplified=num<denom&&KhanMath.getGCD(num,denom)===1;return [{value:sign*(integ+num/denom),exact:simplified}]}return []},decimal:function(text,precision=1e10){const normal=function(text){text=String(text).trim();const match=text.replace(/\u2212/,"-").replace(/([+-])\s+/g,"$1").match(/^([+-]?(?:\d{1,3}(?:[, ]?\d{3})*\.?|\d{0,3}(?:[, ]?\d{3})*\.(?:\d{3}[, ]?)*\d{1,3}))$/);const badLeadingZero=text.match(/^0[0,]*,/);if(match&&!badLeadingZero){let x=parseFloat(match[1].replace(/[, ]/g,""));if(options.inexact===undefined){x=Math.round(x*precision)/precision;}return x}};const commas=function(text){text=text.replace(/([\.,])/g,function(_,c){return c==="."?",":"."});return normal(text)};const results=[{value:normal(text),exact:true}];if(options.decimal_separator===","){results.push({value:commas(text),exact:true});}return results}};return function(guess){const fallback=options.fallback!=null?""+options.fallback:"";guess=String(guess).trim()||fallback;const score={empty:guess==="",correct:false,message:null,guess:guess};const findCorrectAnswer=()=>{for(const form of acceptableForms){const transformed=forms[form](guess);for(let j=0,l=transformed.length;j<l;j++){const val=transformed[j].value;const exact=transformed[j].exact;const piApprox=transformed[j].piApprox;if(predicate(val,options.maxError)){if(exact||options.simplify==="optional"){score.correct=true;score.message=options.message||null;score.empty=false;}else if(form==="percent"){score.empty=true;score.message=ErrorCodes.MISSING_PERCENT_ERROR;}else {if(options.simplify!=="enforced"){score.empty=true;}score.message=ErrorCodes.NEEDS_TO_BE_SIMPLIFIED_ERROR;}return false}if(piApprox&&predicate(val,Math.abs(val*.001))){score.empty=true;score.message=ErrorCodes.APPROXIMATED_PI_ERROR;}}}};findCorrectAnswer();if(score.correct===false){let interpretedGuess=false;_.each(forms,function(form){const anyAreNaN=_.any(form(guess),function(t){return t.value!=null&&!_.isNaN(t.value)});if(anyAreNaN){interpretedGuess=true;}});if(!interpretedGuess){score.empty=true;score.message=ErrorCodes.EXTRA_SYMBOLS_ERROR;return score}}return score}}},number:{convertToPredicate:function(correctAnswer,options){const correctFloat=parseFloat(correctAnswer);return [function(guess,maxError){return Math.abs(guess-correctFloat)<maxError},{...options,type:"predicate"}]},createValidatorFunctional:function(correctAnswer,options){return KhanAnswerTypes.predicate.createValidatorFunctional(...KhanAnswerTypes.number.convertToPredicate(correctAnswer,options))}},expression:{parseSolution:function(solutionString,options){let solution=KAS.parse(solutionString,options);if(!solution.parsed){throw new PerseusError("The provided solution ("+solutionString+") didn't parse.",Errors.InvalidInput)}else if(options.simplified&&!solution.expr.isSimplified()){throw new PerseusError("The provided solution ("+solutionString+") isn't fully expanded and simplified.",Errors.InvalidInput)}else {solution=solution.expr;}return solution},createValidatorFunctional:function(solution,options){return function(guess){const score={empty:false,correct:false,message:null,guess:guess,ungraded:false};if(!guess){score.empty=true;return score}const answer=KAS.parse(guess,options);if(!answer.parsed){score.empty=true;return score}if(typeof solution==="string"){solution=KhanAnswerTypes.expression.parseSolution(solution,options);}const result=KAS.compare(answer.expr,solution,options);if(result.equal){score.correct=true;}else if(result.wrongVariableNames||result.wrongVariableCase){score.ungraded=true;score.message=result.wrongVariableCase?ErrorCodes.WRONG_CASE_ERROR:ErrorCodes.WRONG_LETTER_ERROR;score.suppressAlmostThere=true;}else if(result.message){score.message=result.message;}else {const answerX=KAS.parse(guess.replace(/[xX]/g,"*"),options);if(answerX.parsed){const resultX=KAS.compare(answerX.expr,solution,options);if(resultX.equal){score.ungraded=true;score.message=ErrorCodes.MULTIPLICATION_SIGN_ERROR;}else if(resultX.message){score.message=resultX.message+" Also, I'm a computer. I only understand "+"multiplication if you use an "+"asterisk (*) as the multiplication "+"sign.";}}}return score}}}};
|
|
9
9
|
|
|
10
|
-
function scoreCategorizer(userInput,rubric){let allCorrect=true;rubric.values.forEach((value,i)=>{if(userInput.values[i]!==value){allCorrect=false;}});return {type:"points",earned:allCorrect?1:0,total:1,message:null}}
|
|
10
|
+
function scoreCategorizer(userInput,rubric){if(userInput==null){return {type:"invalid",message:null}}let allCorrect=true;rubric.values.forEach((value,i)=>{if(userInput.values[i]!==value){allCorrect=false;}});return {type:"points",earned:allCorrect?1:0,total:1,message:null}}
|
|
11
11
|
|
|
12
|
-
function validateCategorizer(userInput,validationData){const incomplete=validationData.items.some((_,i)=>userInput.values[i]==null);if(incomplete){return {type:"invalid",message:ErrorCodes.INVALID_SELECTION_ERROR}}return null}
|
|
12
|
+
function validateCategorizer(userInput,validationData){if(userInput==null){return {type:"invalid",message:null}}const incomplete=validationData.items.some((_,i)=>userInput.values[i]==null);if(incomplete){return {type:"invalid",message:ErrorCodes.INVALID_SELECTION_ERROR}}return null}
|
|
13
13
|
|
|
14
|
-
function scoreCSProgram(userInput){if(userInput.status==="correct"){return {type:"points",earned:1,total:1,message:userInput.message||null}}if(userInput.status==="incorrect"){return {type:"points",earned:0,total:1,message:userInput.message||null}}return {type:"invalid",message:"Keep going, you're not there yet!"}}
|
|
14
|
+
function scoreCSProgram(userInput){if(userInput==null){return {type:"invalid",message:null}}if(userInput.status==="correct"){return {type:"points",earned:1,total:1,message:userInput.message||null}}if(userInput.status==="incorrect"){return {type:"points",earned:0,total:1,message:userInput.message||null}}return {type:"invalid",message:"Keep going, you're not there yet!"}}
|
|
15
15
|
|
|
16
|
-
function scoreDropdown(userInput,rubric){const correct=rubric.choices[userInput.value-1].correct;return {type:"points",earned:correct?1:0,total:1,message:null}}
|
|
16
|
+
function scoreDropdown(userInput,rubric){if(userInput==null){return {type:"invalid",message:null}}const correct=rubric.choices[userInput.value-1].correct;return {type:"points",earned:correct?1:0,total:1,message:null}}
|
|
17
17
|
|
|
18
|
-
function validateDropdown(userInput){if(userInput.value===0){return {type:"invalid",message:null}}return null}
|
|
18
|
+
function validateDropdown(userInput){if(userInput==null){return {type:"invalid",message:null}}if(userInput.value===0){return {type:"invalid",message:null}}return null}
|
|
19
19
|
|
|
20
|
-
function scoreExpression(userInput,rubric,locale){const options=_.clone(rubric);_.extend(options,{decimal_separator:getDecimalSeparator(locale)});if(!KAS.parse(userInput,options).parsed){return {type:"invalid",message:ErrorCodes.EXTRA_SYMBOLS_ERROR}}const createValidator=answer=>{const expression=KAS.parse(answer.value,rubric);if(!expression.parsed){throw new PerseusError("Unable to parse solution answer for expression",Errors.InvalidInput,{metadata:{rubric:JSON.stringify(rubric)}})}return KhanAnswerTypes.expression.createValidatorFunctional(expression.expr,_({}).extend(options,{simplify:answer.simplify,form:answer.form}))};let matchingAnswerForm;let matchMessage;let allEmpty=true;let firstUngradedResult;for(const answerForm of rubric.answerForms||[]){const validator=createValidator(answerForm);if(!validator){continue}const result=validator(userInput);if(result.correct){matchingAnswerForm=answerForm;matchMessage=result.message||"";break}allEmpty=allEmpty&&result.empty;if(answerForm.considered==="correct"&&result.ungraded&&!firstUngradedResult){firstUngradedResult=result;}}if(!matchingAnswerForm){if(firstUngradedResult){return {type:"invalid",message:firstUngradedResult.message,suppressAlmostThere:firstUngradedResult.suppressAlmostThere}}if(allEmpty){return {type:"invalid",message:null}}return {type:"points",earned:0,total:1}}if(matchingAnswerForm.considered==="ungraded"){return {type:"invalid",message:matchMessage}}return {type:"points",earned:matchingAnswerForm.considered==="correct"?1:0,total:1,message:matchMessage}}
|
|
20
|
+
function scoreExpression(userInput,rubric,locale){if(userInput==null){return {type:"invalid",message:null}}const options=_.clone(rubric);_.extend(options,{decimal_separator:getDecimalSeparator(locale)});if(!KAS.parse(userInput,options).parsed){return {type:"invalid",message:ErrorCodes.EXTRA_SYMBOLS_ERROR}}const createValidator=answer=>{const expression=KAS.parse(answer.value,rubric);if(!expression.parsed){throw new PerseusError("Unable to parse solution answer for expression",Errors.InvalidInput,{metadata:{rubric:JSON.stringify(rubric)}})}return KhanAnswerTypes.expression.createValidatorFunctional(expression.expr,_({}).extend(options,{simplify:answer.simplify,form:answer.form}))};let matchingAnswerForm;let matchMessage;let allEmpty=true;let firstUngradedResult;for(const answerForm of rubric.answerForms||[]){const validator=createValidator(answerForm);if(!validator){continue}const result=validator(userInput);if(result.correct){matchingAnswerForm=answerForm;matchMessage=result.message||"";break}allEmpty=allEmpty&&result.empty;if(answerForm.considered==="correct"&&result.ungraded&&!firstUngradedResult){firstUngradedResult=result;}}if(!matchingAnswerForm){if(firstUngradedResult){return {type:"invalid",message:firstUngradedResult.message,suppressAlmostThere:firstUngradedResult.suppressAlmostThere}}if(allEmpty){return {type:"invalid",message:null}}return {type:"points",earned:0,total:1}}if(matchingAnswerForm.considered==="ungraded"){return {type:"invalid",message:matchMessage}}return {type:"points",earned:matchingAnswerForm.considered==="correct"?1:0,total:1,message:matchMessage}}
|
|
21
21
|
|
|
22
|
-
function validateExpression(userInput){if(userInput===""){return {type:"invalid",message:null}}return null}
|
|
22
|
+
function validateExpression(userInput){if(userInput===""||userInput==null){return {type:"invalid",message:null}}return null}
|
|
23
23
|
|
|
24
|
-
function getCoefficientsByType(data){if(data.coords==null){return undefined}if(data.type==="exponential"||data.type==="logarithm"){const grader=GrapherUtil.functionForType(data.type);return grader.getCoefficients(data.coords,data.asymptote)}else if(data.type==="linear"||data.type==="quadratic"||data.type==="absolute_value"||data.type==="sinusoid"||data.type==="tangent"){const grader=GrapherUtil.functionForType(data.type);return grader.getCoefficients(data.coords)}else {throw new PerseusError("Invalid grapher type",Errors.InvalidInput)}}function scoreGrapher(userInput,rubric){if(userInput.type!==rubric.correct.type){return {type:"points",earned:0,total:1,message:null}}if(userInput.coords==null){return {type:"invalid",message:null}}const grader=GrapherUtil.functionForType(userInput.type);const guessCoeffs=getCoefficientsByType(userInput);const correctCoeffs=getCoefficientsByType(rubric.correct);if(guessCoeffs==null||correctCoeffs==null){return {type:"invalid",message:null}}if(grader.areEqual(guessCoeffs,correctCoeffs)){return {type:"points",earned:1,total:1,message:null}}return {type:"points",earned:0,total:1,message:null}}
|
|
24
|
+
function getCoefficientsByType(data){if(data.coords==null){return undefined}if(data.type==="exponential"||data.type==="logarithm"){const grader=GrapherUtil.functionForType(data.type);return grader.getCoefficients(data.coords,data.asymptote)}else if(data.type==="linear"||data.type==="quadratic"||data.type==="absolute_value"||data.type==="sinusoid"||data.type==="tangent"){const grader=GrapherUtil.functionForType(data.type);return grader.getCoefficients(data.coords)}else {throw new PerseusError("Invalid grapher type",Errors.InvalidInput)}}function scoreGrapher(userInput,rubric){if(userInput==null){return {type:"invalid",message:null}}if(userInput.type!==rubric.correct.type){return {type:"points",earned:0,total:1,message:null}}if(userInput.coords==null){return {type:"invalid",message:null}}const grader=GrapherUtil.functionForType(userInput.type);const guessCoeffs=getCoefficientsByType(userInput);const correctCoeffs=getCoefficientsByType(rubric.correct);if(guessCoeffs==null||correctCoeffs==null){return {type:"invalid",message:null}}if(grader.areEqual(guessCoeffs,correctCoeffs)){return {type:"points",earned:1,total:1,message:null}}return {type:"points",earned:0,total:1,message:null}}
|
|
25
25
|
|
|
26
|
-
function scoreIframe(userInput){if(userInput.status==="correct"){return {type:"points",earned:1,total:1,message:userInput.message||null}}if(userInput.status==="incorrect"){return {type:"points",earned:0,total:1,message:userInput.message||null}}return {type:"invalid",message:"Keep going, you're not there yet!"}}
|
|
26
|
+
function scoreIframe(userInput){if(userInput==null){return {type:"invalid",message:null}}if(userInput.status==="correct"){return {type:"points",earned:1,total:1,message:userInput.message||null}}if(userInput.status==="incorrect"){return {type:"points",earned:0,total:1,message:userInput.message||null}}return {type:"invalid",message:"Keep going, you're not there yet!"}}
|
|
27
27
|
|
|
28
|
-
const{collinear,canonicalSineCoefficients,similar,clockwise}=geometry;const{getClockwiseAngle}=angles;const{getSinusoidCoefficients,getQuadraticCoefficients}=coefficients;function scoreInteractiveGraph(userInput,rubric){if(userInput.type==="none"&&rubric.correct.type==="none"){return {type:"points",earned:0,total:0,message:null}}const hasValue=Boolean(userInput.coords||userInput.center&&userInput.radius);if(userInput.type===rubric.correct.type&&hasValue){if(userInput.type==="linear"&&rubric.correct.type==="linear"&&userInput.coords!=null){const guess=userInput.coords;const correct=rubric.correct.coords;if(collinear(correct[0],correct[1],guess[0])&&collinear(correct[0],correct[1],guess[1])){return {type:"points",earned:1,total:1,message:null}}}else if(userInput.type==="linear-system"&&rubric.correct.type==="linear-system"&&userInput.coords!=null){const guess=userInput.coords;const correct=rubric.correct.coords;if(collinear(correct[0][0],correct[0][1],guess[0][0])&&collinear(correct[0][0],correct[0][1],guess[0][1])&&collinear(correct[1][0],correct[1][1],guess[1][0])&&collinear(correct[1][0],correct[1][1],guess[1][1])||collinear(correct[0][0],correct[0][1],guess[1][0])&&collinear(correct[0][0],correct[0][1],guess[1][1])&&collinear(correct[1][0],correct[1][1],guess[0][0])&&collinear(correct[1][0],correct[1][1],guess[0][1])){return {type:"points",earned:1,total:1,message:null}}}else if(userInput.type==="quadratic"&&rubric.correct.type==="quadratic"&&userInput.coords!=null){const guessCoeffs=getQuadraticCoefficients(userInput.coords);const correctCoeffs=getQuadraticCoefficients(rubric.correct.coords);if(approximateDeepEqual(guessCoeffs,correctCoeffs)){return {type:"points",earned:1,total:1,message:null}}}else if(userInput.type==="sinusoid"&&rubric.correct.type==="sinusoid"&&userInput.coords!=null){const guessCoeffs=getSinusoidCoefficients(userInput.coords);const correctCoeffs=getSinusoidCoefficients(rubric.correct.coords);const canonicalGuessCoeffs=canonicalSineCoefficients(guessCoeffs);const canonicalCorrectCoeffs=canonicalSineCoefficients(correctCoeffs);if(approximateDeepEqual(canonicalGuessCoeffs,canonicalCorrectCoeffs)){return {type:"points",earned:1,total:1,message:null}}}else if(userInput.type==="circle"&&rubric.correct.type==="circle"){if(approximateDeepEqual(userInput.center,rubric.correct.center)&&approximateEqual(userInput.radius,rubric.correct.radius)){return {type:"points",earned:1,total:1,message:null}}}else if(userInput.type==="point"&&rubric.correct.type==="point"&&userInput.coords!=null){let correct=rubric.correct.coords;if(correct==null){throw new Error("Point graph rubric has null coords")}const guess=userInput.coords.slice();correct=correct.slice();guess?.sort();correct.sort();if(approximateDeepEqual(guess,correct)){return {type:"points",earned:1,total:1,message:null}}}else if(userInput.type==="polygon"&&rubric.correct.type==="polygon"&&userInput.coords!=null){const guess=userInput.coords.slice();const correct=rubric.correct.coords.slice();let match;if(rubric.correct.match==="similar"){match=similar(guess,correct,Number.POSITIVE_INFINITY);}else if(rubric.correct.match==="congruent"){match=similar(guess,correct,number.DEFAULT_TOLERANCE);}else if(rubric.correct.match==="approx"){match=similar(guess,correct,.1);}else {guess.sort();correct.sort();match=approximateDeepEqual(guess,correct);}if(match){return {type:"points",earned:1,total:1,message:null}}}else if(userInput.type==="segment"&&rubric.correct.type==="segment"&&userInput.coords!=null){let guess=deepClone(userInput.coords);let correct=deepClone(rubric.correct.coords);guess=_.invoke(guess,"sort").sort();correct=_.invoke(correct,"sort").sort();if(approximateDeepEqual(guess,correct)){return {type:"points",earned:1,total:1,message:null}}}else if(userInput.type==="ray"&&rubric.correct.type==="ray"&&userInput.coords!=null){const guess=userInput.coords;const correct=rubric.correct.coords;if(approximateDeepEqual(guess[0],correct[0])&&collinear(correct[0],correct[1],guess[1])){return {type:"points",earned:1,total:1,message:null}}}else if(userInput.type==="angle"&&rubric.correct.type==="angle"){const coords=userInput.coords;const correct=rubric.correct.coords;const allowReflexAngles=rubric.correct.allowReflexAngles;if(!coords){return {type:"invalid",message:null}}const areClockwise=clockwise([coords[0],coords[2],coords[1]]);const shouldReverseCoords=areClockwise&&!allowReflexAngles;const guess=shouldReverseCoords?coords.slice().reverse():coords;let match;if(rubric.correct.match==="congruent"){const angles=_.map([guess,correct],function(coords){if(!coords){return false}const angle=getClockwiseAngle(coords,allowReflexAngles);return angle});match=approximateEqual(...angles);}else {match=approximateDeepEqual(guess[1],correct[1])&&collinear(correct[1],correct[0],guess[0])&&collinear(correct[1],correct[2],guess[2]);}if(match){return {type:"points",earned:1,total:1,message:null}}}}if(!hasValue||_.isEqual(userInput,rubric.graph)){return {type:"invalid",message:null}}return {type:"points",earned:0,total:1,message:null}}
|
|
28
|
+
const{collinear,canonicalSineCoefficients,similar,clockwise}=geometry;const{getClockwiseAngle}=angles;const{getSinusoidCoefficients,getQuadraticCoefficients}=coefficients;function scoreInteractiveGraph(userInput,rubric){if(userInput==null){return {type:"invalid",message:null}}if(userInput.type==="none"&&rubric.correct.type==="none"){return {type:"points",earned:0,total:0,message:null}}const hasValue=Boolean(userInput.coords||userInput.center&&userInput.radius);if(userInput.type===rubric.correct.type&&hasValue){if(userInput.type==="linear"&&rubric.correct.type==="linear"&&userInput.coords!=null){const guess=userInput.coords;const correct=rubric.correct.coords;if(collinear(correct[0],correct[1],guess[0])&&collinear(correct[0],correct[1],guess[1])){return {type:"points",earned:1,total:1,message:null}}}else if(userInput.type==="linear-system"&&rubric.correct.type==="linear-system"&&userInput.coords!=null){const guess=userInput.coords;const correct=rubric.correct.coords;if(collinear(correct[0][0],correct[0][1],guess[0][0])&&collinear(correct[0][0],correct[0][1],guess[0][1])&&collinear(correct[1][0],correct[1][1],guess[1][0])&&collinear(correct[1][0],correct[1][1],guess[1][1])||collinear(correct[0][0],correct[0][1],guess[1][0])&&collinear(correct[0][0],correct[0][1],guess[1][1])&&collinear(correct[1][0],correct[1][1],guess[0][0])&&collinear(correct[1][0],correct[1][1],guess[0][1])){return {type:"points",earned:1,total:1,message:null}}}else if(userInput.type==="quadratic"&&rubric.correct.type==="quadratic"&&userInput.coords!=null){const guessCoeffs=getQuadraticCoefficients(userInput.coords);const correctCoeffs=getQuadraticCoefficients(rubric.correct.coords);if(approximateDeepEqual(guessCoeffs,correctCoeffs)){return {type:"points",earned:1,total:1,message:null}}}else if(userInput.type==="sinusoid"&&rubric.correct.type==="sinusoid"&&userInput.coords!=null){const guessCoeffs=getSinusoidCoefficients(userInput.coords);const correctCoeffs=getSinusoidCoefficients(rubric.correct.coords);const canonicalGuessCoeffs=canonicalSineCoefficients(guessCoeffs);const canonicalCorrectCoeffs=canonicalSineCoefficients(correctCoeffs);if(approximateDeepEqual(canonicalGuessCoeffs,canonicalCorrectCoeffs)){return {type:"points",earned:1,total:1,message:null}}}else if(userInput.type==="circle"&&rubric.correct.type==="circle"){if(approximateDeepEqual(userInput.center,rubric.correct.center)&&approximateEqual(userInput.radius,rubric.correct.radius)){return {type:"points",earned:1,total:1,message:null}}}else if(userInput.type==="point"&&rubric.correct.type==="point"&&userInput.coords!=null){let correct=rubric.correct.coords;if(correct==null){throw new Error("Point graph rubric has null coords")}const guess=userInput.coords.slice();correct=correct.slice();guess?.sort();correct.sort();if(approximateDeepEqual(guess,correct)){return {type:"points",earned:1,total:1,message:null}}}else if(userInput.type==="polygon"&&rubric.correct.type==="polygon"&&userInput.coords!=null){const guess=userInput.coords.slice();const correct=rubric.correct.coords.slice();let match;if(rubric.correct.match==="similar"){match=similar(guess,correct,Number.POSITIVE_INFINITY);}else if(rubric.correct.match==="congruent"){match=similar(guess,correct,number.DEFAULT_TOLERANCE);}else if(rubric.correct.match==="approx"){match=similar(guess,correct,.1);}else {guess.sort();correct.sort();match=approximateDeepEqual(guess,correct);}if(match){return {type:"points",earned:1,total:1,message:null}}}else if(userInput.type==="segment"&&rubric.correct.type==="segment"&&userInput.coords!=null){let guess=deepClone(userInput.coords);let correct=deepClone(rubric.correct.coords);guess=_.invoke(guess,"sort").sort();correct=_.invoke(correct,"sort").sort();if(approximateDeepEqual(guess,correct)){return {type:"points",earned:1,total:1,message:null}}}else if(userInput.type==="ray"&&rubric.correct.type==="ray"&&userInput.coords!=null){const guess=userInput.coords;const correct=rubric.correct.coords;if(approximateDeepEqual(guess[0],correct[0])&&collinear(correct[0],correct[1],guess[1])){return {type:"points",earned:1,total:1,message:null}}}else if(userInput.type==="angle"&&rubric.correct.type==="angle"){const coords=userInput.coords;const correct=rubric.correct.coords;const allowReflexAngles=rubric.correct.allowReflexAngles;if(!coords){return {type:"invalid",message:null}}const areClockwise=clockwise([coords[0],coords[2],coords[1]]);const shouldReverseCoords=areClockwise&&!allowReflexAngles;const guess=shouldReverseCoords?coords.slice().reverse():coords;let match;if(rubric.correct.match==="congruent"){const angles=_.map([guess,correct],function(coords){if(!coords){return false}const angle=getClockwiseAngle(coords,allowReflexAngles);return angle});match=approximateEqual(...angles);}else {match=approximateDeepEqual(guess[1],correct[1])&&collinear(correct[1],correct[0],guess[0])&&collinear(correct[1],correct[2],guess[2]);}if(match){return {type:"points",earned:1,total:1,message:null}}}}if(!hasValue||_.isEqual(userInput,rubric.graph)){return {type:"invalid",message:null}}return {type:"points",earned:0,total:1,message:null}}
|
|
29
29
|
|
|
30
|
-
function scoreLabelImageMarker(userInput,rubric){const score={hasAnswers:false,isCorrect:false};if(userInput&&userInput.length>0){score.hasAnswers=true;}if(rubric.length>0){if(userInput&&userInput.length===rubric.length){score.isCorrect=userInput.every(choice=>rubric.includes(choice));}}else if(!userInput||userInput.length===0){score.isCorrect=true;}return score}function scoreLabelImage(userInput,rubric){let numCorrect=0;for(let i=0;i<userInput.markers.length;i++){const score=scoreLabelImageMarker(userInput.markers[i].selected,rubric.markers[i].answers);if(score.isCorrect){numCorrect++;}}return {type:"points",earned:numCorrect===userInput.markers.length?1:0,total:1,message:null}}
|
|
30
|
+
function scoreLabelImageMarker(userInput,rubric){const score={hasAnswers:false,isCorrect:false};if(userInput&&userInput.length>0){score.hasAnswers=true;}if(rubric.length>0){if(userInput&&userInput.length===rubric.length){score.isCorrect=userInput.every(choice=>rubric.includes(choice));}}else if(!userInput||userInput.length===0){score.isCorrect=true;}return score}function scoreLabelImage(userInput,rubric){if(userInput==null){return {type:"invalid",message:null}}let numCorrect=0;for(let i=0;i<userInput.markers.length;i++){const score=scoreLabelImageMarker(userInput.markers[i].selected,rubric.markers[i].answers);if(score.isCorrect){numCorrect++;}}return {type:"points",earned:numCorrect===userInput.markers.length?1:0,total:1,message:null}}
|
|
31
31
|
|
|
32
|
-
function scoreMatcher(userInput,rubric){const correct=_.isEqual(userInput.left,rubric.left)&&_.isEqual(userInput.right,rubric.right);return {type:"points",earned:correct?1:0,total:1,message:null}}
|
|
32
|
+
function scoreMatcher(userInput,rubric){if(userInput==null){return {type:"invalid",message:null}}const correct=_.isEqual(userInput.left,rubric.left)&&_.isEqual(userInput.right,rubric.right);return {type:"points",earned:correct?1:0,total:1,message:null}}
|
|
33
33
|
|
|
34
|
-
function scoreMatrix(userInput,rubric){const solution=rubric.answers;const supplied=userInput.answers;const solutionSize=getMatrixSize(solution);const suppliedSize=getMatrixSize(supplied);const incorrectSize=solutionSize[0]!==suppliedSize[0]||solutionSize[1]!==suppliedSize[1];const createValidator=KhanAnswerTypes.number.createValidatorFunctional;let message=null;let incorrect=false;_(suppliedSize[0]).times(row=>{_(suppliedSize[1]).times(col=>{if(!incorrectSize){const validator=createValidator(solution[row][col],{simplify:true});const result=validator(supplied[row][col]);if(result.message){message=result.message;}if(!result.correct){incorrect=true;}}});});if(incorrectSize){return {type:"points",earned:0,total:1,message:null}}return {type:"points",earned:incorrect?0:1,total:1,message:message}}
|
|
34
|
+
function scoreMatrix(userInput,rubric){if(userInput==null){return {type:"invalid",message:null}}const solution=rubric.answers;const supplied=userInput.answers;const solutionSize=getMatrixSize(solution);const suppliedSize=getMatrixSize(supplied);const incorrectSize=solutionSize[0]!==suppliedSize[0]||solutionSize[1]!==suppliedSize[1];const createValidator=KhanAnswerTypes.number.createValidatorFunctional;let message=null;let incorrect=false;_(suppliedSize[0]).times(row=>{_(suppliedSize[1]).times(col=>{if(!incorrectSize){const validator=createValidator(solution[row][col],{simplify:true});const result=validator(supplied[row][col]);if(result.message){message=result.message;}if(!result.correct){incorrect=true;}}});});if(incorrectSize){return {type:"points",earned:0,total:1,message:null}}return {type:"points",earned:incorrect?0:1,total:1,message:message}}
|
|
35
35
|
|
|
36
|
-
function validateMatrix(userInput){const supplied=userInput.answers;const suppliedSize=getMatrixSize(supplied);for(let row=0;row<suppliedSize[0];row++){for(let col=0;col<suppliedSize[1];col++){const rowData=supplied[row];const cellValue=rowData?.[col];if(cellValue==null||cellValue.toString().length===0){return {type:"invalid",message:ErrorCodes.FILL_ALL_CELLS_ERROR}}}}return null}
|
|
36
|
+
function validateMatrix(userInput){if(userInput==null){return {type:"invalid",message:null}}const supplied=userInput.answers;const suppliedSize=getMatrixSize(supplied);for(let row=0;row<suppliedSize[0];row++){for(let col=0;col<suppliedSize[1];col++){const rowData=supplied[row];const cellValue=rowData?.[col];if(cellValue==null||cellValue.toString().length===0){return {type:"invalid",message:ErrorCodes.FILL_ALL_CELLS_ERROR}}}}return null}
|
|
37
37
|
|
|
38
|
-
function scoreNumberLine(userInput,rubric){const divisionRange=rubric.divisionRange;const outsideAllowedRange=userInput.numDivisions>divisionRange[1]||userInput.numDivisions<divisionRange[0];if(rubric.isTickCtrl&&outsideAllowedRange){return {type:"invalid",message:"Number of divisions is outside the allowed range."}}const range=rubric.range;const start=rubric.initialX!=null?rubric.initialX:range[0];const startRel=rubric.isInequality?"ge":"eq";const correctRel=rubric.correctRel||"eq";const correctPos=number.equal(userInput.numLinePosition,rubric.correctX||0);if(correctPos&&correctRel===userInput.rel){return {type:"points",earned:1,total:1,message:null}}if(userInput.numLinePosition===start&&userInput.rel===startRel){return {type:"invalid",message:null}}return {type:"points",earned:0,total:1,message:null}}
|
|
38
|
+
function scoreNumberLine(userInput,rubric){if(userInput==null){return {type:"invalid",message:null}}const divisionRange=rubric.divisionRange;const outsideAllowedRange=userInput.numDivisions>divisionRange[1]||userInput.numDivisions<divisionRange[0];if(rubric.isTickCtrl&&outsideAllowedRange){return {type:"invalid",message:"Number of divisions is outside the allowed range."}}const range=rubric.range;const start=rubric.initialX!=null?rubric.initialX:range[0];const startRel=rubric.isInequality?"ge":"eq";const correctRel=rubric.correctRel||"eq";const correctPos=number.equal(userInput.numLinePosition,rubric.correctX||0);if(correctPos&&correctRel===userInput.rel){return {type:"points",earned:1,total:1,message:null}}if(userInput.numLinePosition===start&&userInput.rel===startRel){return {type:"invalid",message:null}}return {type:"points",earned:0,total:1,message:null}}
|
|
39
39
|
|
|
40
40
|
function findEndpoint(tex,currentIndex){let bracketDepth=0;for(let i=currentIndex,len=tex.length;i<len;i++){const c=tex[i];if(c==="{"){bracketDepth++;}else if(c==="}"){bracketDepth--;}if(bracketDepth<0){return i}}return tex.length}function parseNextExpression(tex,currentIndex,handler){const openBracketIndex=tex.indexOf("{",currentIndex);if(openBracketIndex===-1){return {endpoint:tex.length,expression:""}}const nextExpIndex=openBracketIndex+1;const endpoint=findEndpoint(tex,nextExpIndex);const expressionTeX=tex.substring(nextExpIndex,endpoint);const parsedExp=walkTex(expressionTeX,handler);return {endpoint:endpoint,expression:parsedExp}}function getNextFracIndex(tex,currentIndex){const dfrac="\\dfrac";const frac="\\frac";const nextFrac=tex.indexOf(frac,currentIndex);const nextDFrac=tex.indexOf(dfrac,currentIndex);if(nextFrac>-1&&nextDFrac>-1){return Math.min(nextFrac,nextDFrac)}if(nextFrac>-1){return nextFrac}if(nextDFrac>-1){return nextDFrac}return -1}function walkTex(tex,handler){if(!tex){return ""}let parsedString="";let currentIndex=0;let nextFrac=getNextFracIndex(tex,currentIndex);while(nextFrac>-1){parsedString+=tex.substring(currentIndex,nextFrac);currentIndex=nextFrac;const firstParsedExpression=parseNextExpression(tex,currentIndex,handler);currentIndex=firstParsedExpression.endpoint+1;const secondParsedExpression=parseNextExpression(tex,currentIndex,handler);currentIndex=secondParsedExpression.endpoint+1;if(parsedString.length){parsedString+=" ";}parsedString+=handler(firstParsedExpression.expression,secondParsedExpression.expression);nextFrac=getNextFracIndex(tex,currentIndex);}parsedString+=tex.slice(currentIndex);return parsedString}function parseTex(tex){const handler=function(exp1,exp2){return exp1+"/"+exp2};const texWithoutFracs=walkTex(tex,handler);return texWithoutFracs.replace("\\%","%")}
|
|
41
41
|
|
|
42
|
-
const answerFormButtons=[{title:"Integers",value:"integer",content:"6"},{title:"Decimals",value:"decimal",content:"0.75"},{title:"Proper fractions",value:"proper",content:"⅗"},{title:"Improper fractions",value:"improper",content:"⁷⁄₄"},{title:"Mixed numbers",value:"mixed",content:"1¾"},{title:"Numbers with π",value:"pi",content:"π"}];function maybeParsePercentInput(inputValue,normalizedAnswerExpected){if(!(typeof inputValue==="string"&&inputValue.endsWith("%"))){return inputValue}const value=parseFloat(inputValue.slice(0,-1));if(isNaN(value)){return inputValue}if(normalizedAnswerExpected){return value/100}return value}function scoreNumericInput(userInput,rubric,locale){const defaultAnswerForms=answerFormButtons.map(e=>e["value"]).filter(e=>e!=="pi");const createValidator=answer=>{const stringAnswer=`${answer.value}`;const validatorForms=[...answer.answerForms??[]];if(!answer.strict||validatorForms.length===0){validatorForms.push(...defaultAnswerForms);}return KhanAnswerTypes.number.createValidatorFunctional(stringAnswer,{message:answer.message,simplify:answer.status==="correct"?answer.simplify:"optional",inexact:true,maxError:answer.maxError,forms:validatorForms,...locale&&{decimal_separator:getDecimalSeparator(locale)}})};const currentValue=parseTex(userInput.currentValue);const normalizedAnswerExpected=rubric.answers.filter(answer=>answer.status==="correct").every(answer=>answer.value!=null&&Math.abs(answer.value)<=1);let localValue=currentValue;if(rubric.coefficient){if(!localValue){localValue=1;}else if(localValue==="-"){localValue=-1;}}const matchedAnswer=rubric.answers.map(answer=>{const validateFn=createValidator(answer);const score=validateFn(maybeParsePercentInput(localValue,normalizedAnswerExpected));return {...answer,score}}).find(answer=>{return answer.score.correct||answer.status==="correct"&&answer.score.empty});const result=matchedAnswer?.status==="correct"?matchedAnswer.score:{empty:matchedAnswer?.status==="ungraded",correct:matchedAnswer?.status==="correct",message:matchedAnswer?.message??null};if(result.empty){return {type:"invalid",message:result.message}}return {type:"points",earned:result.correct?1:0,total:1,message:result.message}}
|
|
42
|
+
const answerFormButtons=[{title:"Integers",value:"integer",content:"6"},{title:"Decimals",value:"decimal",content:"0.75"},{title:"Proper fractions",value:"proper",content:"⅗"},{title:"Improper fractions",value:"improper",content:"⁷⁄₄"},{title:"Mixed numbers",value:"mixed",content:"1¾"},{title:"Numbers with π",value:"pi",content:"π"}];function maybeParsePercentInput(inputValue,normalizedAnswerExpected){if(!(typeof inputValue==="string"&&inputValue.endsWith("%"))){return inputValue}const value=parseFloat(inputValue.slice(0,-1));if(isNaN(value)){return inputValue}if(normalizedAnswerExpected){return value/100}return value}function scoreNumericInput(userInput,rubric,locale){if(userInput==null){return {type:"invalid",message:null}}const defaultAnswerForms=answerFormButtons.map(e=>e["value"]).filter(e=>e!=="pi");const createValidator=answer=>{const stringAnswer=`${answer.value}`;const validatorForms=[...answer.answerForms??[]];if(!answer.strict||validatorForms.length===0){validatorForms.push(...defaultAnswerForms);}return KhanAnswerTypes.number.createValidatorFunctional(stringAnswer,{message:answer.message,simplify:answer.status==="correct"?answer.simplify:"optional",inexact:true,maxError:answer.maxError,forms:validatorForms,...locale&&{decimal_separator:getDecimalSeparator(locale)}})};const currentValue=parseTex(userInput.currentValue);const normalizedAnswerExpected=rubric.answers.filter(answer=>answer.status==="correct").every(answer=>answer.value!=null&&Math.abs(answer.value)<=1);let localValue=currentValue;if(rubric.coefficient){if(!localValue){localValue=1;}else if(localValue==="-"){localValue=-1;}}const matchedAnswer=rubric.answers.map(answer=>{const validateFn=createValidator(answer);const score=validateFn(maybeParsePercentInput(localValue,normalizedAnswerExpected));return {...answer,score}}).find(answer=>{return answer.score.correct||answer.status==="correct"&&answer.score.empty});const result=matchedAnswer?.status==="correct"?matchedAnswer.score:{empty:matchedAnswer?.status==="ungraded",correct:matchedAnswer?.status==="correct",message:matchedAnswer?.message??null};if(result.empty){return {type:"invalid",message:result.message}}return {type:"points",earned:result.correct?1:0,total:1,message:result.message}}
|
|
43
43
|
|
|
44
|
-
function scoreOrderer(userInput,rubric){const correct=_.isEqual(userInput.current,rubric.correctOptions.map(option=>option.content));return {type:"points",earned:correct?1:0,total:1,message:null}}
|
|
44
|
+
function scoreOrderer(userInput,rubric){if(userInput==null){return {type:"invalid",message:null}}const correct=_.isEqual(userInput.current,rubric.correctOptions.map(option=>option.content));return {type:"points",earned:correct?1:0,total:1,message:null}}
|
|
45
45
|
|
|
46
|
-
function validateOrderer(userInput){if(userInput.current.length===0){return {type:"invalid",message:null}}return null}
|
|
46
|
+
function validateOrderer(userInput){if(userInput==null||userInput.current.length===0){return {type:"invalid",message:null}}return null}
|
|
47
47
|
|
|
48
48
|
function scorePlotter(userInput,rubric){return {type:"points",earned:approximateDeepEqual(userInput,rubric.correct)?1:0,total:1,message:null}}
|
|
49
49
|
|
|
50
|
-
function validatePlotter(userInput,validationData){if(approximateDeepEqual(userInput,validationData.starting)){return {type:"invalid",message:null}}return null}
|
|
50
|
+
function validatePlotter(userInput,validationData){if(userInput==null||approximateDeepEqual(userInput,validationData.starting)){return {type:"invalid",message:null}}return null}
|
|
51
51
|
|
|
52
|
-
function scoreRadio(userInput,rubric){const numSelected=userInput.choicesSelected.reduce((sum,selected)=>{return sum+(selected?1:0)},0);const numCorrect=rubric.choices.reduce((sum,currentChoice)=>{return currentChoice.correct?sum+1:sum},0);if(numCorrect>1&&numSelected!==numCorrect&&rubric.countChoices){return {type:"invalid",message:ErrorCodes.CHOOSE_CORRECT_NUM_ERROR}}const noneOfTheAboveSelected=rubric.choices.some((choice,index)=>choice.isNoneOfTheAbove&&userInput.choicesSelected[index]);if(noneOfTheAboveSelected&&numSelected>1){return {type:"invalid",message:ErrorCodes.NOT_NONE_ABOVE_ERROR}}const correct=userInput.choicesSelected.every((selected,i)=>{let isCorrect;if(rubric.choices[i].isNoneOfTheAbove){isCorrect=rubric.choices.every((choice,j)=>{return i===j||!choice.correct});}else {isCorrect=!!rubric.choices[i].correct;}return isCorrect===selected});return {type:"points",earned:correct?1:0,total:1,message:null}}
|
|
52
|
+
function scoreRadio(userInput,rubric){if(userInput==null){return {type:"invalid",message:null}}const numSelected=userInput.choicesSelected.reduce((sum,selected)=>{return sum+(selected?1:0)},0);const numCorrect=rubric.choices.reduce((sum,currentChoice)=>{return currentChoice.correct?sum+1:sum},0);if(numCorrect>1&&numSelected!==numCorrect&&rubric.countChoices){return {type:"invalid",message:ErrorCodes.CHOOSE_CORRECT_NUM_ERROR}}const noneOfTheAboveSelected=rubric.choices.some((choice,index)=>choice.isNoneOfTheAbove&&userInput.choicesSelected[index]);if(noneOfTheAboveSelected&&numSelected>1){return {type:"invalid",message:ErrorCodes.NOT_NONE_ABOVE_ERROR}}const correct=userInput.choicesSelected.every((selected,i)=>{let isCorrect;if(rubric.choices[i].isNoneOfTheAbove){isCorrect=rubric.choices.every((choice,j)=>{return i===j||!choice.correct});}else {isCorrect=!!rubric.choices[i].correct;}return isCorrect===selected});return {type:"points",earned:correct?1:0,total:1,message:null}}
|
|
53
53
|
|
|
54
|
-
function validateRadio(userInput){if(
|
|
54
|
+
function validateRadio(userInput){if(userInput==null||!userInput.choicesSelected.includes(true)){return {type:"invalid",message:null}}return null}
|
|
55
55
|
|
|
56
56
|
function scoreSorter(userInput,rubric){const correct=approximateDeepEqual(userInput.options,rubric.correct);return {type:"points",earned:correct?1:0,total:1,message:null}}
|
|
57
57
|
|
|
@@ -63,25 +63,25 @@ function validateTable(userInput){const supplied=filterNonEmpty(userInput);const
|
|
|
63
63
|
|
|
64
64
|
function scoreTable(userInput,rubric){const validationResult=validateTable(userInput);if(validationResult!=null){return validationResult}const supplied=filterNonEmpty(userInput);const solution=filterNonEmpty(rubric.answers);if(supplied.length!==solution.length){return {type:"points",earned:0,total:1,message:null}}const createValidator=KhanAnswerTypes.number.createValidatorFunctional;let message=null;const allCorrect=solution.every(function(rowSolution){for(let i=0;i<supplied.length;i++){const rowSupplied=supplied[i];const correct=rowSupplied.every(function(cellSupplied,i){const cellSolution=rowSolution[i];const validator=createValidator(cellSolution,{simplify:true});const result=validator(cellSupplied);if(result.message){message=result.message;}return result.correct});if(correct){supplied.splice(i,1);return true}}return false});return {type:"points",earned:allCorrect?1:0,total:1,message}}
|
|
65
65
|
|
|
66
|
-
const inputNumberAnswerTypes={number:{name:"Numbers",forms:"integer, decimal, proper, improper, mixed"},decimal:{name:"Decimals",forms:"decimal"},integer:{name:"Integers",forms:"integer"},rational:{name:"Fractions and mixed numbers",forms:"integer, proper, improper, mixed"},improper:{name:"Improper numbers (no mixed)",forms:"integer, proper, improper"},mixed:{name:"Mixed numbers (no improper)",forms:"integer, proper, mixed"},percent:{name:"Numbers or percents",forms:"integer, decimal, proper, improper, mixed, percent"},pi:{name:"Numbers with pi",forms:"pi"}};function scoreInputNumber(userInput,rubric,locale){if(rubric.answerType==null){rubric.answerType="number";}const stringValue=`${rubric.value}`;const val=KhanAnswerTypes.number.createValidatorFunctional(stringValue,{simplify:rubric.simplify,inexact:rubric.inexact||undefined,maxError:rubric.maxError,forms:inputNumberAnswerTypes[rubric.answerType].forms,...locale&&{decimal_separator:getDecimalSeparator(locale)}});const currentValue=parseTex(userInput.currentValue);const result=val(currentValue);if(result.empty){return {type:"invalid",message:result.message}}return {type:"points",earned:result.correct?1:0,total:1,message:result.message}}
|
|
66
|
+
const inputNumberAnswerTypes={number:{name:"Numbers",forms:"integer, decimal, proper, improper, mixed"},decimal:{name:"Decimals",forms:"decimal"},integer:{name:"Integers",forms:"integer"},rational:{name:"Fractions and mixed numbers",forms:"integer, proper, improper, mixed"},improper:{name:"Improper numbers (no mixed)",forms:"integer, proper, improper"},mixed:{name:"Mixed numbers (no improper)",forms:"integer, proper, mixed"},percent:{name:"Numbers or percents",forms:"integer, decimal, proper, improper, mixed, percent"},pi:{name:"Numbers with pi",forms:"pi"}};function scoreInputNumber(userInput,rubric,locale){if(userInput==null){return {type:"invalid",message:null}}if(rubric.answerType==null){rubric.answerType="number";}const stringValue=`${rubric.value}`;const val=KhanAnswerTypes.number.createValidatorFunctional(stringValue,{simplify:rubric.simplify,inexact:rubric.inexact||undefined,maxError:rubric.maxError,forms:inputNumberAnswerTypes[rubric.answerType].forms,...locale&&{decimal_separator:getDecimalSeparator(locale)}});const currentValue=parseTex(userInput.currentValue);const result=val(currentValue);if(result.empty){return {type:"invalid",message:result.message}}return {type:"points",earned:result.correct?1:0,total:1,message:result.message}}
|
|
67
67
|
|
|
68
68
|
function scoreNoop(points=0){return {type:"points",earned:points,total:points,message:null}}
|
|
69
69
|
|
|
70
70
|
function scoreFreeResponse(userInput,rubric,locale){return scoreNoop()}
|
|
71
71
|
|
|
72
|
-
function validateFreeResponse(userInput,widgetOptions){const userInputLength=userInput
|
|
72
|
+
function validateFreeResponse(userInput,widgetOptions){const userInputLength=userInput?.currentValue.trim().length??0;if(userInputLength===0){return {type:"invalid",message:ErrorCodes.USER_INPUT_EMPTY}}if(!widgetOptions.allowUnlimitedCharacters&&userInputLength>widgetOptions.characterLimit){return {type:"invalid",message:ErrorCodes.USER_INPUT_TOO_LONG}}return null}
|
|
73
73
|
|
|
74
|
-
function scoreGroup(userInput,rubric,locale){const scores=scoreWidgetsFunctional(rubric.widgets,Object.keys(rubric.widgets),userInput,locale);return flattenScores(scores)}
|
|
74
|
+
function scoreGroup(userInput,rubric,locale){if(userInput==null){return {type:"invalid",message:null}}const scores=scoreWidgetsFunctional(rubric.widgets,Object.keys(rubric.widgets),userInput,locale);return flattenScores(scores)}
|
|
75
75
|
|
|
76
|
-
function emptyWidgetsFunctional(widgets,widgetIds,userInputMap,locale){return widgetIds.filter(id=>{const widget=widgets[id];if(!widget||widget.static===true){return false}const validator=getWidgetValidator(widget.type);const userInput=userInputMap[id];const validationData=widget.options;const score=validator?.(userInput,validationData,locale);if(score){return scoreIsEmpty(score)}})}
|
|
76
|
+
function emptyWidgetsFunctional(widgets,widgetIds,userInputMap,locale){return widgetIds.filter(id=>{const widget=widgets[id];if(!widget||widget.static===true){return false}const validator=getWidgetValidator(widget.type);const userInput=userInputMap[id];const validationData=widget.options;const score=validator?.(userInput,validationData,locale);if(score){return scoreIsEmpty(score)}return false})}
|
|
77
77
|
|
|
78
|
-
function validateGroup(userInput,validationData,locale){const emptyWidgets=emptyWidgetsFunctional(validationData.widgets,Object.keys(validationData.widgets),userInput,locale);if(emptyWidgets.length===0){return null}return {type:"invalid",message:null}}
|
|
78
|
+
function validateGroup(userInput,validationData,locale){if(userInput==null){return {type:"invalid",message:null}}const emptyWidgets=emptyWidgetsFunctional(validationData.widgets,Object.keys(validationData.widgets),userInput,locale);if(emptyWidgets.length===0){return null}return {type:"invalid",message:null}}
|
|
79
79
|
|
|
80
|
-
function validateLabelImage(userInput){let numAnswered=0;for(let i=0;i<userInput.markers.length;i++){const userSelection=userInput.markers[i].selected;if(userSelection&&userSelection.length>0){numAnswered++;}}if(numAnswered!==userInput.markers.length){return {type:"invalid",message:null}}return null}
|
|
80
|
+
function validateLabelImage(userInput){if(userInput==null){return {type:"invalid",message:null}}let numAnswered=0;for(let i=0;i<userInput.markers.length;i++){const userSelection=userInput.markers[i].selected;if(userSelection&&userSelection.length>0){numAnswered++;}}if(numAnswered!==userInput.markers.length){return {type:"invalid",message:null}}return null}
|
|
81
81
|
|
|
82
|
-
function validateMockWidget(userInput){if(userInput
|
|
82
|
+
function validateMockWidget(userInput){if(userInput?.currentValue==null||userInput.currentValue===""){return {type:"invalid",message:""}}return null}
|
|
83
83
|
|
|
84
|
-
function scoreMockWidget(userInput,rubric){const validationResult=validateMockWidget(userInput);if(validationResult!=null){return validationResult}return {type:"points",earned:userInput
|
|
84
|
+
function scoreMockWidget(userInput,rubric){const validationResult=validateMockWidget(userInput);if(validationResult!=null){return validationResult}return {type:"points",earned:userInput?.currentValue===rubric.value?1:0,total:1,message:""}}
|
|
85
85
|
|
|
86
86
|
const widgets=new Registry("Score widget registry");function registerWidget(type,scorer,validator){const logic={scorer,validator};widgets.set(type,logic);}const getWidgetValidator=type=>{return widgets.get(type)?.validator??null};const getWidgetScorer=type=>{return widgets.get(type)?.scorer??null};registerWidget("categorizer",scoreCategorizer,validateCategorizer);registerWidget("cs-program",scoreCSProgram);registerWidget("dropdown",scoreDropdown,validateDropdown);registerWidget("expression",scoreExpression,validateExpression);registerWidget("free-response",scoreFreeResponse,validateFreeResponse);registerWidget("grapher",scoreGrapher);registerWidget("group",scoreGroup,validateGroup);registerWidget("iframe",scoreIframe);registerWidget("input-number",scoreInputNumber);registerWidget("interactive-graph",scoreInteractiveGraph);registerWidget("label-image",scoreLabelImage,validateLabelImage);registerWidget("matcher",scoreMatcher);registerWidget("matrix",scoreMatrix,validateMatrix);registerWidget("mock-widget",scoreMockWidget,scoreMockWidget);registerWidget("number-line",scoreNumberLine);registerWidget("numeric-input",scoreNumericInput);registerWidget("orderer",scoreOrderer,validateOrderer);registerWidget("plotter",scorePlotter,validatePlotter);registerWidget("radio",scoreRadio,validateRadio);registerWidget("sorter",scoreSorter,validateSorter);registerWidget("table",scoreTable,validateTable);registerWidget("deprecated-standin",()=>scoreNoop(1));registerWidget("measurer",()=>scoreNoop(1));registerWidget("definition",scoreNoop);registerWidget("explanation",scoreNoop);registerWidget("image",scoreNoop);registerWidget("interaction",scoreNoop);registerWidget("molecule",scoreNoop);registerWidget("passage",scoreNoop);registerWidget("passage-ref",scoreNoop);registerWidget("passage-ref-target",scoreNoop);registerWidget("video",scoreNoop);
|
|
87
87
|
|