UNPKG

@instructure/quiz-interactions

Version:

A React UI component Library for quiz interaction types.

220 lines (193 loc) • 6.72 kB
import Decimal from 'decimal.js' import sortBy from 'lodash/sortBy' import { formatScientificNotation, isScientificNotation, parseScientificNotation, } from '@instructure/quiz-scientific-notation' import t from '@instructure/quiz-i18n/format-message' import setFromArray from '../../../util/setFromArray' import {mathjsIsLoaded, mathjsLoadingWrapper} from '../common/util' export function toPrecision(n, precision) { if (isScientificNotation(n)) { const [mantissa, exponent] = parseScientificNotation(n) return formatScientificNotation(Number(mantissa).toFixed(precision), exponent) } return isNaN(n) ? n : Number(n).toFixed(precision) } function toDecimal(n) { if (isScientificNotation(n)) { const [mantissa, exponent] = parseScientificNotation(n) return new Decimal(`${mantissa}e${exponent}`) } return new Decimal(n) } function toInput(n) { return isScientificNotation(n) ? toDecimal(n) : Number(n) } const decimalToString = input => { if (input.minMaxScientific) { return toDecimal(input.value).toString() } return toDecimal(input.value) } const toScientificNotation = (n, precision) => { const [mantissa, exponent] = n .toExponential(precision) .match(/^(.+)e\+?(.+)$/) .slice(1) return formatScientificNotation(mantissa, exponent) } const randomBetween = (min, max, random) => { return max.minus(min).times(random()).plus(min) } const randomInRange = (min, max, precision, random, scientificNotation) => { if (min.greaterThan(max)) { return null } const value = randomBetween(min, max, random) return scientificNotation ? toScientificNotation(value, precision) : fixNegativeZero(value.toFixed(precision)) } const fixNegativeZero = num => { const math = mathjsLoadingWrapper.mathjs if (math.isZero(num) && num[0] === '-') { return num.substring(1) } return num } const generateInputs = (variables, random) => { return variables.map(({name, min, max, precision}) => { const minMaxScientific = isScientificNotation(min) && isScientificNotation(max) const input = { name, value: randomInRange( toDecimal(min), toDecimal(max), Number(precision), random, minMaxScientific, ), } if (minMaxScientific) { input.minMaxScientific = true } return input }) } const hashifyInputs = (inputs, toInputNumber) => { const result = {} inputs.forEach(input => { result[input.name] = toInputNumber !== decimalToString ? toInputNumber(input.value) : toInputNumber(input) }) return result } const serializeInputs = inputs => { return sortBy(inputs, input => input.name) .map(input => `${input.name}:${input.value}`) .join('|') } const SEARCH_COUNT = 100 export const searchForFormulaSolution = ( variables, formula, previousSolutions, precision, scientificNotation = false, random = Math.random, searchCount = SEARCH_COUNT, genInputs = generateInputs, ) => { const usedInputCache = setFromArray(previousSolutions.map(soln => serializeInputs(soln.inputs))) const precisionValue = Math.min(precision || 0, 16) for (let i = 0; i < searchCount; i++) { const inputs = genInputs(variables, random) if (usedInputCache.has(serializeInputs(inputs))) { continue // eslint-disable-line no-continue } if (!mathjsIsLoaded()) { throw new Error('attempt to call searchForFormulaSolution before math.js is loaded') } try { const math = mathjsLoadingWrapper.mathjs // we replace function e() with the constant e // and function pi() with the constant pi // this is to match legacy canvas quizzes behavior which accepted both e/pi and e()/pi() const node = math.parse(formula) const transformed = node.transform((node, path, parent) => { if (node.isFunctionNode && node.fn.isSymbolNode && node.fn.name === 'e') { return new math.expression.node.ConstantNode(Math.E) } else if (node.isFunctionNode && node.fn.isSymbolNode && node.fn.name === 'pi') { return new math.expression.node.ConstantNode(Math.PI) } else { return node } }) const compiled = transformed.compile() let outputArray try { // first try using Decimal for floating point precision outputArray = compiled.eval(hashifyInputs(inputs, toDecimal)) } catch { // Since some mathjs methods don't support Decimal, // catch here and try again without it. // If this one throws, it will be caught by outer try block try { outputArray = compiled.eval(hashifyInputs(inputs, toInput)) } catch { // If everything else fails, then the inputs are converted to string // because mathjs couldn't support Decimal in scientific notation // if this one throws, it will be caugh by outer try block outputArray = compiled.eval(hashifyInputs(inputs, decimalToString)) } } // `mathjs.eval` spits out a number if the formula has one clause, and a ResultSet object // with an `entries` array field if it has multiple clauses const output = outputArray.entries ? outputArray.entries[outputArray.entries.length - 1] : outputArray if (output !== void 0) { const decimal = new Decimal(output.toString()) if (!decimal.isNaN() && decimal.isFinite()) { return { inputs, output: scientificNotation ? toScientificNotation(decimal, precisionValue) : decimal.toFixed(precisionValue), } } } } catch { return null } } return null } export const buildSolutionsGeneratedMessage = (status, solutionsCount) => { if (status === STATUS_FAILED && solutionsCount === 0) { return t('We were not able to find any solutions.') } else if (status === STATUS_FAILED) { return t( `{ number, plural, one {We were only able to find 1 solution.} other {We were only able to find # solutions.} }`, {number: solutionsCount}, ) } else { return t( `{ number, plural, one {We were able to find 1 solution.} other {We were able to find # solutions.} }`, {number: solutionsCount}, ) } } export const STATUS_STOPPED = 'STATUS_STOPPED' export const STATUS_RUNNING = 'STATUS_RUNNING' export const STATUS_FAILED = 'STATUS_FAILED' export const STATUS_FORMULA_SETUP_INVALID = 'STATUS_FORMULA_SETUP_INVALID' export const STATUS_CANCELED = 'STATUS_CANCELED'