UNPKG

bali-component-framework

Version:

This library provides a JavaScript based implementation of the Bali Nebula™ Component Framework.

568 lines (515 loc) 22.8 kB
/************************************************************************ * Copyright (c) Crater Dog Technologies(TM). All Rights Reserved. * ************************************************************************ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * * * This code is free software; you can redistribute it and/or modify it * * under the terms of The MIT License (MIT), as published by the Open * * Source Initiative. (See http://opensource.org/licenses/MIT) * ************************************************************************/ 'use strict'; /** * This class implements numeric calculations and limits the results to the correct * number of significant figures. It also handles operations involving zero and * infinity correctly. It's just a really smart class! It uses the algorithms for * calculating the significant digits defined here: * https://en.wikipedia.org/wiki/Significance_arithmetic * * It also corrects the Math library with respect to indeterminate forms: * <pre> * 0/0, ∞/∞, 0*∞, 1^∞, ∞-∞, 0^0, and ∞^0 * </pre> * * See the following link for derivations: * https://en.wikipedia.org/wiki/Indeterminate_form */ // PUBLIC FUNCTIONS /** * This function creates a new calculator object. * * An optional debug argument may be specified that controls the level of debugging that * should be applied during execution. The allowed levels are as follows: * <pre> * 0: no debugging is applied (this is the default value and has the best performance) * 1: log any exceptions to console.error before throwing them * 2: perform argument validation checks on each call (poor performance) * 3: log interesting arguments, states and results to console.log * </pre> * * @returns {Calculator} The new generator. */ const Calculator = function(debug) { this.debug = debug || 0; return this; }; Calculator.prototype.constructor = Calculator; exports.Calculator = Calculator; // PUBLIC CONSTANTS Calculator.MAXIMUM_PRECISION = Number.MAX_SAFE_INTEGER.toString().length; /** * This method checks to see if the specified number is close enough to zero or infinity to * cause it to lock onto one of those values. This helps with the hysteresis that occurs when * doing round trip conversions using arithmetic functions. * * @param {Number} number The number to be checked. * @returns {Number} The potentially converted number. */ Calculator.prototype.lockOnExtreme = function(number) { // use single precision comparisons to lock on const extreme = Math.fround(Math.abs(number)); if (extreme === 0 || extreme === Infinity) return extreme; return number; }; /** * This method checks to see if the specified number is close enough to one of the four poles * on a Riemann circle (-1, 0, 1, and Infinity) to cause it to lock onto one of those values. * This helps with the hysteresis that occurs when doing round trip conversions with trigonometry. * * @param {Number} number The number to be checked. * @returns {Number} The potentially converted number. */ Calculator.prototype.lockOnPole = function(number) { // NOTE: it would be great if the single precision comparision worked here as well, or // better yet, the Math trigonometric functions returned Infinity and 0 when they should, // but alas... https://github.com/nodejs/node-v0.x-archive/issues/7852 // // So, we must check each special case explicitly: // Math.sin(0) => 0 // Math.cos(0) => 1 // Math.tan(0) => 0 // // Math.sin(Math.PI/2) => 1 // Math.cos(Math.PI/2) => 6.123233995736766e-17 (not zero) // Math.tan(Math.PI/2) => 16331239353195370 (not Infinity) // // Math.sin(Math.PI) => 1.2246467991473532e-16 (not zero) // Math.cos(Math.PI) => -1 // Math.tan(Math.PI) => -1.2246467991473532e-16 (not zero) // // Math.sin(Math.PI * 3/2) => -1 // Math.cos(Math.PI * 3/2) => -1.8369701987210297e-16 (not zero) // Math.tan(Math.PI * 3/2) => 5443746451065123 (not -Infinity) // // Math.sin(Math.PI * 2) => -2.4492935982947064e-16 (not zero) // Math.cos(Math.PI * 2) => 1 // Math.tan(Math.PI * 2) => -2.4492935982947064e-16 (not zero) // // Math.sin(-Math.PI/2) => -1 // Math.cos(-Math.PI/2) => 6.123233995736766e-17 (not zero) // Math.tan(-Math.PI/2) => -16331239353195370 (not -Infinity) // // Math.sin(-Math.PI) => -1.2246467991473532e-16 (not zero) // Math.cos(-Math.PI) => -1 // Math.tan(-Math.PI) => 1.2246467991473532e-16 (not zero) // // Math.sin(-Math.PI * 3/2) => 1 // Math.cos(-Math.PI * 3/2) => -1.8369701987210297e-16 (not zero) // Math.tan(-Math.PI * 3/2) => -5443746451065123 (not -Infinity) // // Math.sin(-Math.PI * 2) => 2.4492935982947064e-16 (not zero) // Math.cos(-Math.PI * 2) => 1 // Math.tan(-Math.PI * 2) => 2.4492935982947064e-16 (not zero) if (number === -0) return 0; if (number > -2.4492935982947068e-16 && number < 2.4492935982947068e-16) return 0; if (number > 0.9999999999999996 && number < 1.0000000000000004) return 1; if (number > -1.0000000000000004 && number < -0.9999999999999996) return -1; if (number < -5443746451065119 || number > 5443746451065119) return Infinity; if (number === -Infinity) return Infinity; return number; }; /** * This method checks to see if the specified angle is close enough to pi or -pi to * cause it to lock onto one of those values. This helps with the hysteresis that occurs when * doing round trip conversions using trigonometric functions. * * @param {Number} angle The angle to be checked. * @returns {Number} The potentially converted angle. */ Calculator.prototype.lockOnAngle = function(angle) { // use single precision comparisons to lock on if (Math.fround(Math.PI) === Math.fround(Math.abs(angle))) angle = Math.PI; return angle; }; /** * This method calculates the smallest number of significant digits for a list of values * that are passed as arguments to this function. All integers including zero have the * maximum number of significant digits. * * @params {Numbers} arguments The values to be evaluated. * @returns {Number} The number of significant digits for the list of values. */ Calculator.prototype.valueDigits = function() { var significantDigits = Calculator.MAXIMUM_PRECISION; for (var i = 0; i < arguments.length; i++) { const value = arguments[i]; if (Number.isFinite(value) && !Number.isInteger(value)) { const parsed = parse(value); const digits = parsed.coefficient.length + parsed.decimal.length; significantDigits = Math.min(significantDigits, digits); } } return significantDigits; }; /** * This method normalizes a raw numeric value to the specified number of significant * digits minus the error digits associated with the specified error factor. The number * of error digits is equal to the base 10 logarithm of the error factor. * * @param {Number} value The raw value to be normalized. * @param {Number} significantDigits The number of significant digits for the value. * @param {Number} error The error factor to use to adjust the significant digits. * @returns {Number} The normalized value. */ Calculator.prototype.normalizeValue = function(value, significantDigits, error) { var errorDigits; if (isFinite(error) && error !== 0) { errorDigits = Math.round(Math.abs(Math.log10(Math.abs(error)))); significantDigits -= errorDigits; significantDigits = Math.max(significantDigits, 1); } return Number(value.toPrecision(significantDigits)); }; /** * This method calculates the smallest number of significant digits to the right of the * decimal place for a list of values that are passed as arguments to this function. All * integers have unlimited significant digits to the right of the decimal. * * @params {Numbers} arguments The values to be evaluated. * @returns {Number} The number of significant digits to the right of the decimal for the * list of values. */ Calculator.prototype.decimalDigits = function() { var significantDigits = Calculator.MAXIMUM_PRECISION; for (var i = 0; i < arguments.length; i++) { const value = arguments[i]; if (Number.isFinite(value) && !Number.isInteger(value)) { const parsed = parse(value); const digits = parsed.decimal.length - parsed.exponent; significantDigits = Math.min(significantDigits, digits); } } return significantDigits; }; /** * This method normalizes a raw numeric value to the specified number of significant * digits to the right of the decimal place. * * @param {Number} value The raw value to be normalized. * @param {Number} significantDigits The number of significant digits to the right of the * decimal for the value. * @returns {Number} The normalized value. */ Calculator.prototype.normalizeDecimal = function(value, significantDigits) { return Number(value.toFixed(significantDigits)); }; /** * This method returns the sum of a list of numbers. The number of significant digits in the decimal * of the result is equal to the number of significant digits in the decimal of the operand with * the least number of digits in its decimal. For example: * <pre> * 12.345 (3 significant decimal digits) * + 543.21 (2 significant decimal digits) * --------- * 555.56 (2 significant decimal digits) * </pre> * * @params {Numbers} arguments The values to be added. * @return {Number} The resulting sum of the numbers. */ Calculator.prototype.sum = function() { var result = 0; const digits = this.decimalDigits.apply(this, arguments); for (var i = 0; i < arguments.length; i++) { const value = this.lockOnExtreme(arguments[i]); result = this.lockOnExtreme(result + value); } result = this.normalizeDecimal(result, digits); return result; }; /** * This method returns the difference of two numbers. The number of significant digits in the decimal * of the result is equal to the number of significant digits in the decimal of the operand with * the least number of digits in its decimal. For example: * <pre> * 123.45 (2 significant decimal digits) * - 54.321 (3 significant decimal digits) * --------- * 69.13 (2 significant decimal digits) * </pre> * * @param {Number} minuend The number to be subtracted from. * @param {Number} subtrahend The number to be subtracted from the first number. * @return {Number} The resulting difference of the two numbers. */ Calculator.prototype.difference = function(minuend, subtrahend) { minuend = this.lockOnExtreme(minuend); subtrahend = this.lockOnExtreme(subtrahend); var result = this.lockOnExtreme(minuend - subtrahend); const digits = this.decimalDigits(minuend, subtrahend); result = this.normalizeDecimal(result, digits); return result; }; /** * This method returns the product of a list of numbers. The number of significant digits in the * result is equal to the number of significant digits in the operand with the least number * of significant digits. For example: * <pre> * 123.45 (5 significant digits) * x 67.8 (3 significant digits) * --------- * 8.37e+3 (3 significant digits) * </pre> * * @params {Numbers} arguments The values to be multiplied. * @return {Number} The resulting product of the numbers. */ Calculator.prototype.product = function() { var result = 1; const digits = this.valueDigits.apply(this, arguments); for (var i = 0; i < arguments.length; i++) { const value = this.lockOnExtreme(arguments[i]); result = this.lockOnExtreme(result * value); } result = this.normalizeValue(result, digits); return result; }; /** * This method returns the quotient of two numbers. The number of significant digits in the * result is equal to the number of significant digits in the operand with the least number * of significant digits. For example: * <pre> * 123.45 (5 significant digits) * ÷ 67.8 (3 significant digits) * --------- * 1.82 (3 significant digits) * </pre> * * @param {Number} dividend The number to be divided into. * @param {Number} divisor The number to be divided by. * @return {Number} The resulting quotient of the two numbers. */ Calculator.prototype.quotient = function(dividend, divisor) { dividend = this.lockOnExtreme(dividend); divisor = this.lockOnExtreme(divisor); var result = this.lockOnExtreme(dividend / divisor); const digits = this.valueDigits(dividend, divisor); result = this.normalizeValue(result, digits); return result; }; /** * This method returns the remainder that is left from the quotient of two numbers. The * number of significant digits in the result is equal to the number of significant digits * in the operand with the least number of significant digits. For example: * <pre> * 123.45 (5 significant digits) * % 67.8 (3 significant digits) * --------- * 55.6 (3 significant digits) * </pre> * * @param {Number} dividend The number to be divided into. * @param {Number} divisor The number to be divided by. * @return {Number} The resulting remainder of the quotient of the two numbers. */ Calculator.prototype.remainder = function(dividend, divisor) { dividend = this.lockOnExtreme(dividend); divisor = this.lockOnExtreme(divisor); var result = this.lockOnExtreme(dividend % divisor); const digits = this.valueDigits(dividend, divisor); result = this.normalizeValue(result, digits); return result; }; /** * This method returns the value of e raised to an exponential power. The * number of significant digits in the result is equal to the number of significant digits * in the operand with the least number of significant digits minus the order of magnitude * of the error for the function which is calculated as follows: * <pre> * | | * error digits: log | exponent | * 10| | * </pre> * * @param {Number} exponent The exponent value. * @returns {Number} The value of the base raised to the exponent. */ Calculator.prototype.exponential = function(exponent) { exponent = this.lockOnExtreme(exponent); var result = this.lockOnExtreme(Math.exp(exponent)); const digits = this.valueDigits(exponent); const error = exponent; result = this.normalizeValue(result, digits, error); return result; }; /** * This method returns the value of the natural logarithm of an exponential value. * The number of significant digits in the result is equal to the number of significant digits * in the operand with the least number of significant digits minus the order of magnitude * of the error for the function which is calculated as follows: * <pre> * | 1 | * error digits: log | ----------------- | * 10| log (exponential) | * | e | * </pre> * * @param {Number} exponential The value that is equal to base^exponent. * @returns {Number} The value of the base logarith of the exponential. */ Calculator.prototype.logarithm = function(exponential) { exponential = this.lockOnExtreme(exponential); var result = this.lockOnExtreme(Math.log(exponential)); const digits = this.valueDigits(exponential); const error = this.lockOnExtreme(1 / Math.log(exponential)); result = this.normalizeValue(result, digits, error); return result; }; /** * This method returns the ratio of the length of the side opposite of an angle to the length * of the hypotenuse of a right triangle. The number of significant digits in the result is * equal to the number of significant digits in the operand minus the order of magnitude of the * error for the function which is calculated as follows: * <pre> * | angle | * error digits: log | -------------- | * 10| tangent(angle) | * </pre> * * @param {Number} angle The angle within the right triangle. * @returns {Number} The ratio of the opposite to the hypotenuse. */ Calculator.prototype.sine = function(angle) { var result = this.lockOnPole(Math.sin(angle)); const digits = this.valueDigits(angle); const error = this.lockOnExtreme(angle / this.lockOnPole(Math.tan(angle))); result = this.normalizeValue(result, digits, error); return result; }; /** * This method returns the ratio of the length of the side adjacent to an angle to the length * of the hypotenuse of a right triangle. The number of significant digits in the result is * equal to the number of significant digits in the operand minus the order of magnitude of the * error for the function which is calculated as follows: * <pre> * | | * error digits: log | angle * tangent(angle) | * 10| | * </pre> * * @param {Number} angle The angle within the right triangle. * @returns {Number} The ratio of the adjacent to the hypotenuse. */ Calculator.prototype.cosine = function(angle) { var result = this.lockOnPole(Math.cos(angle)); const digits = this.valueDigits(angle); const error = this.lockOnExtreme(angle * this.lockOnPole(Math.tan(angle))); result = this.normalizeValue(result, digits, error); return result; }; /** * This method returns the ratio of the length of the side opposite of an angle to the length * of the side adjacent to the angle of a right triangle. The number of significant digits in * the result is equal to the number of significant digits in the operand minus the order of * magnitude of the error for the function which is calculated as follows: * <pre> * | / 1 \ | * error digits: log | angle * | tangent(angle) + -------------- | | * 10| \ tangent(angle) / | * </pre> * * @param {Number} angle The angle within the right triangle. * @returns {Number} The ratio of the opposite to the adjacent. */ Calculator.prototype.tangent = function(angle) { var result = this.lockOnPole(Math.tan(angle)); const digits = this.valueDigits(angle); const error = this.lockOnExtreme(angle * (result + 1 / result)); result = this.normalizeValue(result, digits, error); return result; }; /** * This method returns the angle defined by the ratio of the length of the side opposite of * the angle to the length of the hypotenuse of a right triangle. The number of significant * digits in the result is equal to the number of significant digits in the operand minus the * order of magnitude of the error for the function which is calculated as follows: * <pre> * | ratio | * error digits: log | --------------------------------- | * 10| 2 1/2 | * | arcsine(ratio) * (1 - ratio ) | * </pre> * * @param {Number} ratio The ratio of the opposite to the hypotenuse within the right triangle. * @returns {Number} The corresponding angle. */ Calculator.prototype.arcsine = function(ratio) { var angle = this.lockOnAngle(Math.asin(ratio)); const digits = this.valueDigits(ratio); const error = this.lockOnExtreme(ratio / (Math.sqrt(1 - ratio * ratio) * angle)); angle = this.normalizeValue(angle, digits, error); return angle; }; /** * This method returns the angle defined by the ratio of the length of the side adjacent to * the angle to the length of the hypotenuse of a right triangle. The number of significant * digits in the result is equal to the number of significant digits in the operand minus the * order of magnitude of the error for the function which is calculated as follows: * <pre> * | ratio | * error digits: log | ----------------------------------- | * 10| 2 1/2 | * | arccosine(ratio) * (1 - ratio ) | * </pre> * * @param {Number} ratio The ratio of the adjacent to the hypotenuse within the right triangle. * @returns {Number} The corresponding angle. */ Calculator.prototype.arccosine = function(ratio) { var angle = this.lockOnAngle(Math.acos(ratio)); const digits = this.valueDigits(ratio); const error = this.lockOnExtreme(ratio / (Math.sqrt(1 - ratio * ratio) * angle)); angle = this.normalizeValue(angle, digits, error); return angle; }; /** * This method returns the angle defined by the ratio of the length of the side opposite of * the angle to the length of the side adjacent to the angle in a right triangle. The number * of significant digits in the result is equal to the number of significant digits in the * operand with the least number of significant digits minus the order of magnitude of the * error for the function which is calculated as follows: * <pre> * | ratio | * error digits: log | ------------------------------------ | * 10| 2 1/2 | * | arctangent(ratio) * (1 + ratio ) | * </pre> * * @param {Number} opposite The length of the side opposite the angle in the right triangle. * @param {Number} adjacent The length of the side adjacent to the angle in the right triangle. * @returns {Number} The corresponding angle. */ Calculator.prototype.arctangent = function(opposite, adjacent) { const ratio = this.quotient(opposite, adjacent); var angle = this.lockOnAngle(Math.atan2(opposite, adjacent)); const digits = this.valueDigits(ratio); const error = this.lockOnExtreme(ratio / (Math.sqrt(1 + ratio * ratio) * angle)); angle = this.normalizeValue(angle, digits, error); return angle; }; // PRIVATE FUNCTIONS /* * This function parses a floating point number into its three parts. */ const parse = function(number) { const pattern = /([0-9]+)\.([0-9]*[1-9])(e[+-][1-9][0-9]*)?/; const matches = number.toString().match(pattern); var coefficient = matches[1]; const decimal = matches[2]; const exponent = matches[3] ? Number(matches[3].slice(1)) : 0; if (coefficient === '0') coefficient = ''; // leading zero does not count return { coefficient: coefficient, decimal: decimal, exponent: exponent }; };