@khanacademy/perseus-score 8.10.5 → 8.11.1

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 CHANGED
@@ -1,10 +1,10 @@
1
1
  import { addLibraryVersionToPerseusDebug } from '@khanacademy/perseus-utils';
2
2
  import * as KAS from '@khanacademy/kas';
3
3
  import { KhanMath, geometry, coefficients, angles, number } from '@khanacademy/kmath';
4
- import { ErrorCodes, PerseusError, Errors, getDivideSymbol, getDecimalSeparator, approximateDeepEqual, GrapherUtil, approximateEqual, deepClone, getMatrixSize, getWidgetIdsFromContent, Registry, convertInputNumberOptionsToNumericInput } from '@khanacademy/perseus-core';
4
+ import { ErrorCodes, PerseusError, Errors, getDivideSymbol, getDecimalSeparator, approximateDeepEqual, GrapherUtil, approximateEqual, deepClone, getMatrixSize, convertInputNumberOptionsToNumericInput, getWidgetIdsFromContent, Registry } from '@khanacademy/perseus-core';
5
5
  import _ from 'underscore';
6
6
 
7
- const libName="@khanacademy/perseus-score";const libVersion="8.10.5";addLibraryVersionToPerseusDebug(libName,libVersion);
7
+ const libName="@khanacademy/perseus-score";const libVersion="8.11.1";addLibraryVersionToPerseusDebug(libName,libVersion);
8
8
 
9
9
  const MAXERROR_EPSILON=Math.pow(2,-42);const KhanAnswerTypes={predicate:{defaultForms:"integer, proper, improper, mixed, decimal",createValidatorFunctional:function(predicate,rawOptions){const options={simplify:"required",ratio:false,forms:KhanAnswerTypes.predicate.defaultForms,...rawOptions};let acceptableForms;if(!_.isArray(options.forms)){acceptableForms=options.forms.split(/\s*,\s*/);}else {acceptableForms=options.forms;}if(options.inexact===undefined){options.maxError=0;}const maxError=+(options.maxError??0)+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,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:{createValidatorFunctional:function(correctAnswer,options){const correctFloat=parseFloat(correctAnswer);return KhanAnswerTypes.predicate.createValidatorFunctional((guess,maxError)=>Math.abs(guess-correctFloat)<maxError,{...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.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}}}};
10
10
 
@@ -26,7 +26,37 @@ function getCoefficientsByType(data){if(data.coords==null){return undefined}if(d
26
26
 
27
27
  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!"}}
28
28
 
29
- const{collinear,canonicalSineCoefficients,canonicalTangentCoefficients,similar,clockwise}=geometry;const{getClockwiseAngle}=angles;const{getAbsoluteValueCoefficients,getSinusoidCoefficients,getQuadraticCoefficients,getExponentialCoefficients,getLogarithmCoefficients,getTangentCoefficients}=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==="exponential"&&rubric.correct.type==="exponential"&&userInput.coords!=null&&userInput.asymptote!=null){const guessCoeffs=getExponentialCoefficients(userInput.coords,userInput.asymptote);const correctCoeffs=getExponentialCoefficients(rubric.correct.coords,rubric.correct.asymptote);if(guessCoeffs!=null&&correctCoeffs!=null&&approximateDeepEqual([guessCoeffs.a,guessCoeffs.b,guessCoeffs.c],[correctCoeffs.a,correctCoeffs.b,correctCoeffs.c])){return {type:"points",earned:1,total:1,message:null}}}else if(userInput.type==="logarithm"&&rubric.correct.type==="logarithm"&&userInput.coords!=null&&userInput.asymptote!=null){const guessCoeffs=getLogarithmCoefficients(userInput.coords,userInput.asymptote);const correctCoeffs=getLogarithmCoefficients(rubric.correct.coords,rubric.correct.asymptote);if(guessCoeffs!=null&&correctCoeffs!=null&&approximateDeepEqual([guessCoeffs.a,guessCoeffs.b,guessCoeffs.c],[correctCoeffs.a,correctCoeffs.b,correctCoeffs.c])){return {type:"points",earned:1,total:1,message:null}}}else if(userInput.type==="absolute-value"&&rubric.correct.type==="absolute-value"&&userInput.coords!=null){const userCoeffs=getAbsoluteValueCoefficients(userInput.coords);const rubricCoeffs=getAbsoluteValueCoefficients(rubric.correct.coords);if(userCoeffs!==undefined&&rubricCoeffs!==undefined&&approximateDeepEqual(userCoeffs,rubricCoeffs)){return {type:"points",earned:1,total:1,message:null}}}else if(userInput.type==="tangent"&&rubric.correct.type==="tangent"&&userInput.coords!=null){const guessCoeffs=getTangentCoefficients(userInput.coords);const correctCoeffs=getTangentCoefficients(rubric.correct.coords);const canonicalGuessCoeffs=canonicalTangentCoefficients(guessCoeffs);const canonicalCorrectCoeffs=canonicalTangentCoefficients(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}}}else if(userInput.type==="vector"&&rubric.correct.type==="vector"&&userInput.coords!=null&&rubric.correct.coords!=null){const guess=userInput.coords;const correct=rubric.correct.coords;let match;if(rubric.correct.match==="congruent"){const guessDelta=[guess[1][0]-guess[0][0],guess[1][1]-guess[0][1]];const correctDelta=[correct[1][0]-correct[0][0],correct[1][1]-correct[0][1]];match=approximateDeepEqual(guessDelta,correctDelta);}else {match=approximateDeepEqual(guess[0],correct[0])&&approximateDeepEqual(guess[1],correct[1]);}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
+ const{getAbsoluteValueCoefficients}=coefficients;function scoreAbsoluteValue(userInput,rubric){if(!userInput.coords||!rubric.coords){return {type:"invalid",message:null}}const userCoeffs=getAbsoluteValueCoefficients(userInput.coords);const rubricCoeffs=getAbsoluteValueCoefficients(rubric.coords);const isCorrect=userCoeffs!==undefined&&rubricCoeffs!==undefined&&approximateDeepEqual(userCoeffs,rubricCoeffs);return {type:"points",earned:isCorrect?1:0,total:1,message:null}}
30
+
31
+ const{collinear: collinear$3,clockwise}=geometry;const{getClockwiseAngle}=angles;function scoreAngle(userInput,rubric){const coords=userInput.coords;const correct=rubric.coords;const allowReflexAngles=rubric.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.match==="congruent"){const guessAngle=getClockwiseAngle(guess,allowReflexAngles);const correctAngle=correct?getClockwiseAngle(correct,allowReflexAngles):null;match=correctAngle!==null&&approximateEqual(guessAngle,correctAngle);}else {match=correct!=null&&approximateDeepEqual(guess[1],correct[1])&&collinear$3(correct[1],correct[0],guess[0])&&collinear$3(correct[1],correct[2],guess[2]);}return {type:"points",earned:match?1:0,total:1,message:null}}
32
+
33
+ function scoreCircle(userInput,rubric){if(userInput.center==null||userInput.radius==null||rubric.center==null||rubric.radius==null){return {type:"invalid",message:null}}const isCorrect=approximateDeepEqual(userInput.center,rubric.center)&&approximateEqual(userInput.radius,rubric.radius);return {type:"points",earned:isCorrect?1:0,total:1,message:null}}
34
+
35
+ const{getExponentialCoefficients}=coefficients;function scoreExponential(userInput,rubric){if(!userInput.coords||userInput.asymptote==null||!rubric.coords||rubric.asymptote==null){return {type:"invalid",message:null}}const guessCoeffs=getExponentialCoefficients(userInput.coords,userInput.asymptote);const correctCoeffs=getExponentialCoefficients(rubric.coords,rubric.asymptote);const isCorrect=guessCoeffs!=null&&correctCoeffs!=null&&approximateDeepEqual([guessCoeffs.a,guessCoeffs.b,guessCoeffs.c],[correctCoeffs.a,correctCoeffs.b,correctCoeffs.c]);return {type:"points",earned:isCorrect?1:0,total:1,message:null}}
36
+
37
+ const{collinear: collinear$2}=geometry;function scoreLinear(userInput,rubric){if(!userInput.coords||!rubric.coords){return {type:"invalid",message:null}}const guess=userInput.coords;const correct=rubric.coords;const isCorrect=collinear$2(correct[0],correct[1],guess[0])&&collinear$2(correct[0],correct[1],guess[1]);return {type:"points",earned:isCorrect?1:0,total:1,message:null}}
38
+
39
+ const{collinear: collinear$1}=geometry;function scoreLinearSystem(userInput,rubric){if(!userInput.coords||!rubric.coords){return {type:"invalid",message:null}}const guess=userInput.coords;const correct=rubric.coords;const isCorrect=collinear$1(correct[0][0],correct[0][1],guess[0][0])&&collinear$1(correct[0][0],correct[0][1],guess[0][1])&&collinear$1(correct[1][0],correct[1][1],guess[1][0])&&collinear$1(correct[1][0],correct[1][1],guess[1][1])||collinear$1(correct[0][0],correct[0][1],guess[1][0])&&collinear$1(correct[0][0],correct[0][1],guess[1][1])&&collinear$1(correct[1][0],correct[1][1],guess[0][0])&&collinear$1(correct[1][0],correct[1][1],guess[0][1]);return {type:"points",earned:isCorrect?1:0,total:1,message:null}}
40
+
41
+ const{getLogarithmCoefficients}=coefficients;function scoreLogarithm(userInput,rubric){if(!userInput.coords||userInput.asymptote==null||!rubric.coords||rubric.asymptote==null){return {type:"invalid",message:null}}const guessCoeffs=getLogarithmCoefficients(userInput.coords,userInput.asymptote);const correctCoeffs=getLogarithmCoefficients(rubric.coords,rubric.asymptote);const isCorrect=guessCoeffs!=null&&correctCoeffs!=null&&approximateDeepEqual([guessCoeffs.a,guessCoeffs.b,guessCoeffs.c],[correctCoeffs.a,correctCoeffs.b,correctCoeffs.c]);return {type:"points",earned:isCorrect?1:0,total:1,message:null}}
42
+
43
+ function scorePoint(userInput,rubric){if(!userInput.coords){return {type:"invalid",message:null}}if(rubric.coords==null){throw new Error("Point graph rubric has null coords")}const guess=userInput.coords.slice();const correct=rubric.coords.slice();guess.sort();correct.sort();return {type:"points",earned:approximateDeepEqual(guess,correct)?1:0,total:1,message:null}}
44
+
45
+ const{similar}=geometry;function scorePolygon(userInput,rubric){if(!userInput.coords||!rubric.coords){return {type:"invalid",message:null}}const guess=userInput.coords.slice();const correct=rubric.coords.slice();let match;if(rubric.match==="similar"){match=similar(guess,correct,Number.POSITIVE_INFINITY);}else if(rubric.match==="congruent"){match=similar(guess,correct,number.DEFAULT_TOLERANCE);}else if(rubric.match==="approx"){match=similar(guess,correct,.1);}else {guess.sort();correct.sort();match=approximateDeepEqual(guess,correct);}return {type:"points",earned:match?1:0,total:1,message:null}}
46
+
47
+ const{getQuadraticCoefficients}=coefficients;function scoreQuadratic(userInput,rubric){if(!userInput.coords||!rubric.coords){return {type:"invalid",message:null}}const guessCoeffs=getQuadraticCoefficients(userInput.coords);const correctCoeffs=getQuadraticCoefficients(rubric.coords);const isCorrect=approximateDeepEqual(guessCoeffs,correctCoeffs);return {type:"points",earned:isCorrect?1:0,total:1,message:null}}
48
+
49
+ const{collinear}=geometry;function scoreRay(userInput,rubric){if(!userInput.coords||!rubric.coords){return {type:"invalid",message:null}}const guess=userInput.coords;const correct=rubric.coords;const isCorrect=approximateDeepEqual(guess[0],correct[0])&&collinear(correct[0],correct[1],guess[1]);return {type:"points",earned:isCorrect?1:0,total:1,message:null}}
50
+
51
+ function scoreSegment(userInput,rubric){if(!userInput.coords||!rubric.coords){return {type:"invalid",message:null}}let guess=deepClone(userInput.coords);let correct=deepClone(rubric.coords);guess=_.invoke(guess,"sort").sort();correct=_.invoke(correct,"sort").sort();return {type:"points",earned:approximateDeepEqual(guess,correct)?1:0,total:1,message:null}}
52
+
53
+ const{getSinusoidCoefficients}=coefficients;const{canonicalSineCoefficients}=geometry;function scoreSinusoid(userInput,rubric){if(!userInput.coords||!rubric.coords){return {type:"invalid",message:null}}const guessCoeffs=getSinusoidCoefficients(userInput.coords);const correctCoeffs=getSinusoidCoefficients(rubric.coords);const canonicalGuessCoeffs=canonicalSineCoefficients(guessCoeffs);const canonicalCorrectCoeffs=canonicalSineCoefficients(correctCoeffs);const isCorrect=approximateDeepEqual(canonicalGuessCoeffs,canonicalCorrectCoeffs);return {type:"points",earned:isCorrect?1:0,total:1,message:null}}
54
+
55
+ const{getTangentCoefficients}=coefficients;const{canonicalTangentCoefficients}=geometry;function scoreTangent(userInput,rubric){if(!userInput.coords||!rubric.coords){return {type:"invalid",message:null}}const guessCoeffs=getTangentCoefficients(userInput.coords);const correctCoeffs=getTangentCoefficients(rubric.coords);const canonicalGuessCoeffs=canonicalTangentCoefficients(guessCoeffs);const canonicalCorrectCoeffs=canonicalTangentCoefficients(correctCoeffs);const isCorrect=approximateDeepEqual(canonicalGuessCoeffs,canonicalCorrectCoeffs);return {type:"points",earned:isCorrect?1:0,total:1,message:null}}
56
+
57
+ function scoreVector(userInput,rubric){if(!userInput.coords||!rubric.coords){return {type:"invalid",message:null}}const guess=userInput.coords;const correct=rubric.coords;let match;if(rubric.match==="congruent"){const guessDelta=[guess[1][0]-guess[0][0],guess[1][1]-guess[0][1]];const correctDelta=[correct[1][0]-correct[0][0],correct[1][1]-correct[0][1]];match=approximateDeepEqual(guessDelta,correctDelta);}else {match=approximateDeepEqual(guess[0],correct[0])&&approximateDeepEqual(guess[1],correct[1]);}return {type:"points",earned:match?1:0,total:1,message:null}}
58
+
59
+ 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==="absolute-value"&&rubric.correct.type==="absolute-value"){return scoreAbsoluteValue(userInput,rubric.correct)}else if(userInput.type==="angle"&&rubric.correct.type==="angle"){return scoreAngle(userInput,rubric.correct)}else if(userInput.type==="circle"&&rubric.correct.type==="circle"){return scoreCircle(userInput,rubric.correct)}else if(userInput.type==="exponential"&&rubric.correct.type==="exponential"&&userInput.asymptote!=null){return scoreExponential(userInput,rubric.correct)}else if(userInput.type==="linear"&&rubric.correct.type==="linear"){return scoreLinear(userInput,rubric.correct)}else if(userInput.type==="linear-system"&&rubric.correct.type==="linear-system"){return scoreLinearSystem(userInput,rubric.correct)}else if(userInput.type==="logarithm"&&rubric.correct.type==="logarithm"&&userInput.asymptote!=null){return scoreLogarithm(userInput,rubric.correct)}else if(userInput.type==="point"&&rubric.correct.type==="point"){return scorePoint(userInput,rubric.correct)}else if(userInput.type==="polygon"&&rubric.correct.type==="polygon"){return scorePolygon(userInput,rubric.correct)}else if(userInput.type==="quadratic"&&rubric.correct.type==="quadratic"){return scoreQuadratic(userInput,rubric.correct)}else if(userInput.type==="ray"&&rubric.correct.type==="ray"){return scoreRay(userInput,rubric.correct)}else if(userInput.type==="segment"&&rubric.correct.type==="segment"){return scoreSegment(userInput,rubric.correct)}else if(userInput.type==="sinusoid"&&rubric.correct.type==="sinusoid"){return scoreSinusoid(userInput,rubric.correct)}else if(userInput.type==="tangent"&&rubric.correct.type==="tangent"){return scoreTangent(userInput,rubric.correct)}else if(userInput.type==="vector"&&rubric.correct.type==="vector"){return scoreVector(userInput,rubric.correct)}if(!hasValue||_.isEqual(userInput,rubric.graph)){return {type:"invalid",message:null}}return {type:"points",earned:0,total:1,message:null}}
30
60
 
31
61
  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}}
32
62
 
@@ -64,7 +94,7 @@ function validateTable(userInput){const supplied=filterNonEmpty(userInput);const
64
94
 
65
95
  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}}
66
96
 
67
- 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}}
97
+ 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){return scoreNumericInput(userInput,convertInputNumberOptionsToNumericInput(rubric),locale)}
68
98
 
69
99
  const noScore={type:"points",earned:0,total:0,message:null};function combineScores(scoreA,scoreB){let message;if(scoreA.type==="points"&&scoreB.type==="points"){if(scoreA.message&&scoreB.message&&scoreA.message!==scoreB.message){message=null;}else {message=scoreA.message||scoreB.message;}return {type:"points",earned:scoreA.earned+scoreB.earned,total:scoreA.total+scoreB.total,message:message}}if(scoreA.type==="points"&&scoreB.type==="invalid"){return scoreB}if(scoreA.type==="invalid"&&scoreB.type==="points"){return scoreA}if(scoreA.type==="invalid"&&scoreB.type==="invalid"){if(scoreA.message&&scoreB.message&&scoreA.message!==scoreB.message){message=null;}else {message=scoreA.message||scoreB.message;}return {type:"invalid",message:message}}throw new PerseusError("PerseusScore with unknown type encountered",Errors.InvalidInput,{metadata:{scoreA:JSON.stringify(scoreA),scoreB:JSON.stringify(scoreB)}})}function flattenScores(widgetScoreMap){return Object.values(widgetScoreMap).reduce(combineScores,noScore)}
70
100