PageRenderTime 26ms CodeModel.GetById 29ms RepoModel.GetById 0ms app.codeStats 0ms

/utils/answer-types.js

https://github.com/winhamwr/khan-exercises
JavaScript | 699 lines | 548 code | 113 blank | 38 comment | 90 complexity | ed8401582a957548e82548014e1128d9 MD5 | raw file
  1. (function() {
  2. var inexactMessages = {
  3. unsimplified: "Your answer is almost correct, but it needs to be simplified.",
  4. missingPercentSign: "Your answer is almost correct, but it is missing a <code>\\%</code> at the end."
  5. };
  6. Khan.answerTypes = Khan.answerTypes || {};
  7. jQuery.extend( Khan.answerTypes, {
  8. text: function( solutionarea, solution, fallback, verifier ) {
  9. var input = jQuery('<input type="text">');
  10. jQuery( solutionarea ).append( input );
  11. var correct = typeof solution === "object" ? jQuery( solution ).text() : solution;
  12. if ( verifier == null ) {
  13. verifier = function( correct, guess ) {
  14. correct = jQuery.trim( correct );
  15. guess = jQuery.trim( guess );
  16. return correct === guess;
  17. };
  18. }
  19. var ret = function() {
  20. // we want the normal input if it's nonempty, the fallback converted to a string if
  21. // the input is empty and a fallback exists, and the empty string if the input
  22. // is empty and the fallback doesn't exist.
  23. var val = input.val().length > 0 ?
  24. input.val() :
  25. fallback ?
  26. fallback + "" :
  27. "";
  28. ret.guess = input.val();
  29. return verifier( correct, val );
  30. };
  31. ret.solution = jQuery.trim( correct );
  32. ret.examples = verifier.examples || [];
  33. ret.showGuess = function( guess ) {
  34. input.val( guess );
  35. };
  36. return ret;
  37. },
  38. line: function( solutionarea, solution, fallback ) {
  39. var verifier = function( correct, guess ){
  40. var result = true;
  41. for ( i = 0; i < 5; i++ ){
  42. var sampleX = KhanUtil.randRange( -100, 100 );
  43. if ( guess.match(/[A-W]|[a-w]|[y-z]|[Y-Z]/) !== null ){
  44. return false;
  45. }
  46. var newGuess = guess
  47. .replace( /\u2212/, "-" )
  48. .replace( /(\d)(x)/, "$1 * $2" )
  49. .replace( "x", sampleX )
  50. .replace( /(\d)(\()/, "$1 * $2" );
  51. var newCorrect = correct
  52. .replace( /(\d)(x)/, "$1 * $2" )
  53. .replace( "x", sampleX )
  54. .replace( /(\d)(\()/, "$1 * $2" )
  55. .replace( /-\s?-/, "");
  56. result = result && ( eval( newCorrect ) === eval( newGuess ) ) ;
  57. }
  58. return result;
  59. }
  60. verifier.examples = "An equation of a line, like 3(x+1)/2 or 2x + 1";
  61. return Khan.answerTypes.text( solutionarea, solution, fallback, verifier );
  62. }
  63. ,
  64. number: function( solutionarea, solution, fallback, forms ) {
  65. var options = jQuery.extend({
  66. simplify: "required",
  67. ratio: false,
  68. maxError: Math.pow( 2, -42 ),
  69. forms: "literal, integer, proper, improper, mixed, decimal"
  70. }, jQuery( solution ).data());
  71. var acceptableForms = ( forms || options.forms ).split(/\s*,\s*/);
  72. var fractionTransformer = function( text ) {
  73. text = text
  74. // Replace unicode minus sign with hyphen
  75. .replace( /\u2212/, "-" )
  76. // Remove space after +, -
  77. .replace( /([+-])\s+/g, "$1" );
  78. // Extract numerator and denominator
  79. var match = text.match( /^([+-]?\d+)\s*\/\s*([+-]?\d+)$/ );
  80. var parsedInt = parseInt( text, 10 );
  81. if ( match ) {
  82. var num = parseFloat( match[1] ),
  83. denom = parseFloat( match[2] );
  84. var simplified = denom > 0 &&
  85. ( options.ratio || match[2] !== "1" ) &&
  86. KhanUtil.getGCD( num, denom ) === 1;
  87. return [ {
  88. value: num / denom,
  89. exact: simplified
  90. } ];
  91. } else if ( !isNaN( parsedInt ) ) {
  92. return [ {
  93. value: parsedInt,
  94. exact: true
  95. } ];
  96. }
  97. return [];
  98. };
  99. var forms = {
  100. literal: {
  101. transformer: function( text ) {
  102. // Prevent literal comparisons for decimal-looking-like strings
  103. return [{ value: ( /[^+-\u2212\d\.\s]/ ).test( text ) ? text : null }];
  104. }
  105. },
  106. integer: {
  107. transformer: function( text ) {
  108. return forms.decimal.transformer( text );
  109. },
  110. example: "an integer, like <code>6</code>"
  111. },
  112. proper: {
  113. transformer: function( text ) {
  114. return jQuery.map( fractionTransformer( text ), function( o ) {
  115. if ( Math.abs(o.value) < 1 ) {
  116. return [o];
  117. } else {
  118. return [];
  119. }
  120. } );
  121. },
  122. example: (function() {
  123. if ( options.simplify === "optional" ) {
  124. return "a <em>proper</em> fraction, like <code>1/2</code> or <code>6/10</code>"
  125. } else {
  126. return "a <em>simplified proper</em> fraction, like <code>3/5</code>"
  127. }
  128. })()
  129. },
  130. improper: {
  131. transformer: function( text ) {
  132. return jQuery.map( fractionTransformer( text ), function( o ) {
  133. if ( Math.abs(o.value) >= 1 ) {
  134. return [o];
  135. } else {
  136. return [];
  137. }
  138. } );
  139. },
  140. example: (function() {
  141. if ( options.simplify === "optional" ) {
  142. return "an <em>improper</em> fraction, like <code>10/7</code> or <code>14/8</code>"
  143. } else {
  144. return "a <em>simplified improper</em> fraction, like <code>7/4</code>"
  145. }
  146. })()
  147. },
  148. pi: {
  149. transformer: function( text ) {
  150. var match, possibilities = [];
  151. // Replace unicode minus sign with hyphen
  152. text = text.replace( /\u2212/, "-" );
  153. // - pi
  154. if ( match = text.match( /^([+-]?)\s*(?:pi?|\u03c0)$/i ) ) {
  155. possibilities = [ { value: parseFloat( match[1] + "1" ), exact: true } ];
  156. // 5 / 6 pi
  157. } else if ( match = text.match( /^([+-]?\d+\s*(?:\/\s*[+-]?\d+)?)\s*\*?\s*(?:pi?|\u03c0)$/i ) ) {
  158. possibilities = fractionTransformer( match[1] );
  159. // 5 pi / 6
  160. } else if ( match = text.match( /^([+-]?\d+)\s*\*?\s*(?:pi?|\u03c0)\s*(?:\/\s*([+-]?\d+))?$/i ) ) {
  161. possibilities = fractionTransformer( match[1] + match[2] );
  162. // - pi / 4
  163. } else if ( match = text.match( /^([+-]?)\s*\*?\s*(?:pi?|\u03c0)\s*(?:\/\s*([+-]?\d+))?$/i ) ) {
  164. possibilities = fractionTransformer( match[1] + "1/" + match[2] );
  165. // 0
  166. } else if ( text === "0") {
  167. possibilities = [ { value: 0, exact: true } ];
  168. // 0.5 pi (fallback)
  169. } else if ( match = text.match( /^(\S+)\s*\*?\s*(?:pi?|\u03c0)$/i ) ) {
  170. possibilities = forms.decimal.transformer( match[1] );
  171. }
  172. jQuery.each( possibilities, function( ix, possibility ) {
  173. possibility.value *= Math.PI;
  174. } );
  175. return possibilities;
  176. },
  177. example: "a multiple of pi, like <code>12\\ \\text{pi}</code> or <code>2/3\\ \\text{pi}</code>"
  178. },
  179. // simple log( c ) form
  180. log: {
  181. transformer: function( text ) {
  182. var match, possibilities = [];
  183. // Replace unicode minus sign with hyphen
  184. text = text.replace( /\u2212/, "-" );
  185. if ( match = text.match( /^log\(\s*(\S+)\s*\)$/i ) ) {
  186. possibilities = forms.decimal.transformer( match[1] );
  187. } else if ( text === "0") {
  188. possibilities = [ { value: 0, exact: true } ];
  189. }
  190. return possibilities;
  191. },
  192. example: "an expression, like <code>\\log(100)</code>"
  193. },
  194. percent: {
  195. transformer: function( text ) {
  196. text = jQuery.trim( text );
  197. var hasPercentSign = false;
  198. if ( text.indexOf( "%" ) === ( text.length - 1 ) ) {
  199. text = jQuery.trim( text.substring( 0, text.length - 1) );
  200. hasPercentSign = true;
  201. }
  202. var transformed = forms.decimal.transformer( text );
  203. jQuery.each( transformed, function( ix, t ) {
  204. t.exact = hasPercentSign;
  205. });
  206. return transformed;
  207. },
  208. example: "a percent, like <code>12.34\\%</code>"
  209. },
  210. mixed: {
  211. transformer: function( text ) {
  212. var match = text
  213. // Replace unicode minus sign with hyphen
  214. .replace( /\u2212/, "-" )
  215. // Remove space after +, -
  216. .replace( /([+-])\s+/g, "$1" )
  217. // Extract integer, numerator and denominator
  218. .match( /^([+-]?)(\d+)\s+(\d+)\s*\/\s*(\d+)$/ );
  219. if ( match ) {
  220. var sign = parseFloat( match[1] + "1" ),
  221. integ = parseFloat( match[2] ),
  222. num = parseFloat( match[3] ),
  223. denom = parseFloat( match[4] );
  224. var simplified = num < denom && KhanUtil.getGCD( num, denom ) === 1;
  225. return [ {
  226. value: sign * ( integ + num / denom ),
  227. exact: simplified
  228. } ];
  229. }
  230. return [];
  231. },
  232. example: "a mixed number, like <code>1\\ 3/4</code>"
  233. },
  234. decimal: {
  235. transformer: function( text ) {
  236. var normal = function( text ) {
  237. var match = text
  238. // Replace unicode minus sign with hyphen
  239. .replace( /\u2212/, "-" )
  240. // Remove commas
  241. .replace( /,\s*/g, "" )
  242. // Extract integer, numerator and denominator
  243. // This matches [+-]?\.; will f
  244. .match( /^([+-]?(?:\d+\.?|\d*\.\d+))$/ );
  245. if ( match ) {
  246. var x = parseFloat( match[1] );
  247. if ( options.inexact === undefined ) {
  248. var factor = Math.pow( 10, 10 );
  249. x = Math.round( x * factor ) / factor;
  250. }
  251. return x;
  252. }
  253. };
  254. var commas = function( text ) {
  255. text = text.replace( /([\.,])/g, function( _, c ) { return ( c === "." ? "," : "." ); } );
  256. return normal( text );
  257. };
  258. return [
  259. { value: normal( text ), exact: true },
  260. { value: commas( text ), exact: true }
  261. ];
  262. },
  263. example: (function() {
  264. if ( options.inexact === undefined ) {
  265. return "an <em>exact</em> decimal, like <code>0.75</code>";
  266. } else {
  267. return "a decimal, like <code>0.75</code>";
  268. }
  269. })()
  270. }
  271. };
  272. var verifier = function( correct, guess ) {
  273. correct = jQuery.trim( correct );
  274. guess = jQuery.trim( guess );
  275. correctFloat = parseFloat( correct );
  276. var ret = false;
  277. jQuery.each( acceptableForms, function( i, form ) {
  278. var transformed = forms[ form ].transformer( jQuery.trim( guess ) );
  279. for ( var i = 0, l = transformed.length; i < l; i++ ) {
  280. var val = transformed[ i ].value;
  281. var exact = transformed[ i ].exact;
  282. if ( typeof val === "string" &&
  283. correct.toLowerCase() === val.toLowerCase() ) {
  284. ret = true;
  285. return false; // break;
  286. } if ( typeof val === "number" &&
  287. Math.abs( correctFloat - val ) < options.maxError ) {
  288. if ( exact || options.simplify === "optional" ) {
  289. ret = true;
  290. } else if ( form === "percent" ){
  291. ret = inexactMessages.missingPercentSign;
  292. } else {
  293. ret = inexactMessages.unsimplified;
  294. }
  295. return false; // break;
  296. }
  297. }
  298. } );
  299. return ret;
  300. };
  301. verifier.examples = [];
  302. jQuery.each( acceptableForms, function( i, form ) {
  303. if ( forms[ form ] != null && forms[ form ].example != null ) {
  304. verifier.examples.push( forms[ form ].example );
  305. }
  306. });
  307. return Khan.answerTypes.text( solutionarea, solution, fallback, verifier );
  308. },
  309. regex: function( solutionarea, solution, fallback ) {
  310. var verifier = function( correct, guess ) {
  311. return jQuery.trim( guess ).match( correct ) != null;
  312. };
  313. return Khan.answerTypes.text( solutionarea, solution, fallback, verifier );
  314. },
  315. decimal: function( solutionarea, solution, fallback ) {
  316. return Khan.answerTypes.number( solutionarea, solution, fallback, "decimal" );
  317. },
  318. rational: function( solutionarea, solution, fallback ) {
  319. return Khan.answerTypes.number( solutionarea, solution, fallback, "integer, proper, improper, mixed" );
  320. },
  321. // A little bit of a misnomer as proper fractions are also accepted
  322. improper: function( solutionarea, solution, fallback ) {
  323. return Khan.answerTypes.number( solutionarea, solution, fallback, "integer, proper, improper" );
  324. },
  325. mixed: function( solutionarea, solution, fallback ) {
  326. return Khan.answerTypes.number( solutionarea, solution, fallback, "integer, proper, mixed" );
  327. },
  328. radical: function( solutionarea, solution ) {
  329. var options = jQuery.extend({
  330. simplify: "required"
  331. }, jQuery( solution ).data());
  332. var ansSquared = parseFloat( jQuery( solution ).text() );
  333. var ans = KhanUtil.splitRadical( ansSquared );
  334. var inte = jQuery( "<span>" ), inteGuess, rad = jQuery( "<span>" ), radGuess;
  335. var inteValid = Khan.answerTypes.text( inte, null, "1", function( correct, guess ) { inteGuess = guess; } );
  336. var radValid = Khan.answerTypes.text( rad, null, "1", function( correct, guess ) { radGuess = guess; } );
  337. solutionarea.addClass( "radical" )
  338. .append( inte )
  339. .append( '<span class="surd">&radic;</span>')
  340. .append( rad.addClass( "overline" ) );
  341. var ret = function() {
  342. // Load entered values into inteGuess, radGuess
  343. inteValid();
  344. radValid();
  345. inteGuess = parseFloat( inteGuess );
  346. radGuess = parseFloat( radGuess );
  347. ret.guess = [ inteGuess, radGuess ];
  348. var simplified = inteGuess === ans[0] && radGuess == ans[1];
  349. var correct = Math.abs( inteGuess ) * inteGuess * radGuess === ansSquared;
  350. if ( correct ) {
  351. if ( simplified || options.simplify === "optional" ) {
  352. return true;
  353. } else {
  354. return inexactMessages.unsimplified;
  355. }
  356. } else {
  357. return false;
  358. }
  359. };
  360. if ( options.simplify === "required" ) {
  361. ret.examples = [ "a simplified radical, like <code>\\sqrt{2}</code> or <code>3\\sqrt{5}</code>" ];
  362. } else {
  363. ret.examples = [ "a radical, like <code>\\sqrt{8}</code> or <code>2\\sqrt{2}</code>" ];
  364. }
  365. ret.solution = ans;
  366. ret.showGuess = function( guess ) {
  367. inteValid.showGuess( guess ? guess[0] : '' );
  368. radValid.showGuess( guess ? guess[1] : '' );
  369. };
  370. return ret;
  371. },
  372. multiple: function( solutionarea, solution ) {
  373. var solutionarea = jQuery( solutionarea );
  374. // here be dragons
  375. solutionarea.append( jQuery( solution ).clone().contents().tmpl() );
  376. var solutionArray = [];
  377. solutionarea.find( ".sol" ).each(function() {
  378. var type = jQuery( this ).data( "type" );
  379. type = type != null ? type : "number";
  380. var sol = jQuery( this ).clone(),
  381. solarea = jQuery( this ).empty();
  382. var fallback = sol.data( "fallback" ),
  383. validator = Khan.answerTypes[type]( solarea, sol, fallback );
  384. jQuery( this ).data( "validator", validator );
  385. solutionArray.unshift( validator.solution );
  386. });
  387. var ret = function() {
  388. var valid = true,
  389. guess = [];
  390. solutionarea.find( ".sol" ).each(function() {
  391. var validator = jQuery( this ).data( "validator", validator );
  392. if ( validator != null ) {
  393. // Don't short-circuit so we can record all guesses
  394. valid = validator() && valid;
  395. guess.push( validator.guess );
  396. }
  397. });
  398. ret.guess = guess;
  399. return valid;
  400. };
  401. ret.showGuess = function( guess ) {
  402. guess = jQuery.extend( true, [], guess );
  403. solutionarea.find( ".sol" ).each(function() {
  404. var validator = jQuery( this ).data( "validator", validator );
  405. if ( validator != null ) {
  406. // Shift regardless of whether we can show the guess
  407. var next = guess.shift();
  408. if ( typeof validator.showGuess === "function" ) {
  409. validator.showGuess( next );
  410. }
  411. }
  412. });
  413. };
  414. ret.examples = solutionarea.find( ".example" ).remove()
  415. .map(function(i, el) {
  416. return jQuery( el ).html();
  417. });
  418. ret.solution = solutionArray;
  419. return ret;
  420. },
  421. radio: function( solutionarea, solution ) {
  422. var extractRawCode = function( solution ) {
  423. return jQuery( solution ).find('.value').clone()
  424. .find( ".MathJax" ).remove().end()
  425. .find( "code" ).removeAttr( "id" ).end()
  426. .html();
  427. };
  428. // Without this we get numbers twice and things sometimes
  429. var solutionText = extractRawCode( solution );
  430. var list = jQuery("<ul></ul>");
  431. jQuery( solutionarea ).append(list);
  432. // Get all of the wrong choices
  433. var choices = jQuery( solution ).siblings( ".choices" );
  434. // Set number of choices equal to all wrong plus one correct
  435. var numChoices = choices.children().length + 1;
  436. // Or set number as specified
  437. if ( choices.data("show") ) {
  438. numChoices = parseFloat( choices.data("show") );
  439. }
  440. // Optionally include none of the above as a choice
  441. var showNone = choices.data("none");
  442. if ( showNone ) {
  443. var noneIsCorrect = KhanUtil.rand(numChoices) === 0;
  444. numChoices -= 1;
  445. }
  446. // If a category exercise, the correct answer is already included in .choices
  447. // and choices are always presented in the same order
  448. var isCategory = choices.data("category");
  449. var possibleChoices = choices.children().get();
  450. if ( isCategory ) {
  451. numChoices -= 1;
  452. } else {
  453. possibleChoices = KhanUtil.shuffle( possibleChoices );
  454. }
  455. // Add the correct answer
  456. if( !noneIsCorrect && !isCategory) {
  457. jQuery( solution ).data( "correct", true );
  458. }
  459. // Insert correct answer as first of possibleChoices
  460. if ( !isCategory ) {
  461. possibleChoices.splice( 0, 0, solution );
  462. }
  463. var dupes = {};
  464. var shownChoices = [];
  465. var solutionTextSquish = solution.text().replace(/\s+/g, "");
  466. for ( var i = 0; i < possibleChoices.length && shownChoices.length < numChoices; i++ ) {
  467. var choice = jQuery( possibleChoices[i] );
  468. choice.runModules();
  469. var choiceTextSquish = choice.text().replace(/\s+/g, "");
  470. if ( isCategory && solutionTextSquish === choiceTextSquish ) {
  471. choice.data( "correct", true );
  472. }
  473. if ( !dupes[ choiceTextSquish ] ) {
  474. dupes[ choiceTextSquish ] = true;
  475. // i == 0 is the solution except in category mode; skip it when none is correct
  476. if ( !( noneIsCorrect && i == 0 ) || isCategory ) {
  477. shownChoices.push( choice );
  478. }
  479. }
  480. }
  481. if( shownChoices.length < numChoices ) {
  482. return false;
  483. }
  484. if ( !isCategory ) {
  485. shownChoices = KhanUtil.shuffle( shownChoices );
  486. }
  487. if( showNone ) {
  488. var none = jQuery( "<span>None of the above.</span>" );
  489. if( noneIsCorrect ) {
  490. none.data( "correct", true );
  491. solutionText = none.text();
  492. list.data( "real-answer",
  493. jQuery( solution ).runModules()
  494. .contents()
  495. .wrapAll( '<span class="value""></span>' )
  496. .parent() );
  497. }
  498. shownChoices.push( none );
  499. }
  500. jQuery.each(shownChoices, function( i, choice ) {
  501. var correct = choice.data( "correct" );
  502. choice.contents().wrapAll( '<li><label><span class="value"></span></label></li>' )
  503. .parent().before( '<input type="radio" name="solution" value="' + (correct ? 1 : 0) + '">' )
  504. .parent().parent()
  505. .appendTo(list);
  506. });
  507. var ret = function() {
  508. var choice = list.find("input:checked");
  509. if ( noneIsCorrect && choice.val() === "1") {
  510. choice.next()
  511. .fadeOut( "fast", function() {
  512. jQuery( this ).replaceWith( list.data( "real-answer" ) )
  513. .fadeIn( "fast" );
  514. });
  515. }
  516. ret.guess = jQuery.trim( extractRawCode(choice.closest("li")) );
  517. return choice.val() === "1";
  518. };
  519. ret.solution = jQuery.trim( solutionText );
  520. ret.showGuess = function( guess ) {
  521. list.find( 'input:checked' ).prop( 'checked', false);
  522. var li = list.children().filter( function() {
  523. return jQuery.trim( extractRawCode(this) ) === guess;
  524. } );
  525. li.find( "input[name=solution]" ).prop( "checked", true );
  526. };
  527. return ret;
  528. },
  529. list: function( solutionarea, solution ) {
  530. var input = jQuery("<select></select>");
  531. jQuery( solutionarea ).append( input );
  532. var choices = jQuery.tmpl.getVAR( jQuery( solution ).data("choices") );
  533. jQuery.each( choices, function(index, value) {
  534. input.append('<option value="' + value + '">'
  535. + value + '</option>');
  536. });
  537. var correct = jQuery( solution ).text();
  538. var verifier = function( correct, guess ) {
  539. correct = jQuery.trim( correct );
  540. guess = jQuery.trim( guess );
  541. return correct === guess;
  542. };
  543. var ret = function() {
  544. ret.guess = input.val();
  545. return verifier( correct, ret.guess );
  546. };
  547. ret.solution = jQuery.trim( correct );
  548. ret.showGuess = function( guess ) {
  549. input.val( guess );
  550. };
  551. return ret;
  552. },
  553. primeFactorization: function( solutionarea, solution, fallback ) {
  554. var verifier = function( correct, guess ) {
  555. guess = guess.split(" ").join("").toLowerCase();
  556. guess = KhanUtil.sortNumbers( guess.split( /x|\*|\u00d7/ ) ).join( "x" );
  557. return guess === correct;
  558. };
  559. verifier.examples = [
  560. "a product of prime factors, like <code>2 \\times 3</code>",
  561. "a single prime number, like <code>5</code>"
  562. ];
  563. return Khan.answerTypes.text( solutionarea, solution, fallback, verifier );
  564. }
  565. } );
  566. } )();