diff --git a/docs/datatypes/units.md b/docs/datatypes/units.md index 746d6bba3..0094f14b0 100644 --- a/docs/datatypes/units.md +++ b/docs/datatypes/units.md @@ -26,6 +26,7 @@ Example usage: var a = math.unit(45, 'cm'); // Unit 450 mm var b = math.unit('0.1 kilogram'); // Unit 100 gram var c = math.unit('2 inch'); // Unit 2 inch +var d = math.unit('90 km/h'); // Unit 90 km/h ``` A `Unit` contains the following functions: @@ -72,7 +73,7 @@ d.toString(); // String "5.08 cm" ## Calculations -Basic operations `add`, `subtract`, `multiply`, and `divide` can be performed +Basic operations `add`, `subtract`, `multiply`, `divide`, and `pow` can be performed on units. Trigonometric functions like `sin` support units with an angle as argument. @@ -84,6 +85,12 @@ math.multiply(b, 2); // Unit 200 mm var c = math.unit(45, 'deg'); // Unit 45 deg math.cos(c); // Number 0.7071067811865476 + +// Kinetic energy of average sedan on highway +var d = math.unit('80 mi/h') // Unit 80 mi/h +var e = math.unit('2 tonne') // Unit 2 tonne +var f = math.multiply(0.5, math.multipy(math.pow(d, 2), e)); + // 1.2790064742399996 MJ ``` The expression parser supports units too. This is described in the section about diff --git a/docs/expressions/syntax.md b/docs/expressions/syntax.md index 87d71ec09..84fe42810 100644 --- a/docs/expressions/syntax.md +++ b/docs/expressions/syntax.md @@ -325,8 +325,9 @@ parser.eval('number(a)'); // Error: 2 + i is no valid number ### Units -math.js supports units. Units can be used in basic arithmetic operations like -add and subtract, and units can be converted from one to another. +math.js supports units. Units can be used in the arithmetic operations +add, subtract, multiply, divide, and exponentiation. +Units can also be converted from one to another. An overview of all available units can be found on the page [Units](../datatypes/units.md). @@ -339,6 +340,7 @@ math.eval('5.4 kg'); // Unit, 5.4 kg // convert a unit math.eval('2 inch to cm'); // Unit, 5.08 cm math.eval('20 celsius in fahrenheit'); // Unit, ~68 fahrenheit +math.eval('90 km/h to m/s'); // Unit, 25 m / s // convert a unit to a number // A second parameter with the unit for the exported number must be provided @@ -350,6 +352,7 @@ math.eval('3 inch + 2 cm'); // Unit, 3.7874 inch math.eval('3 inch + 2 cm'); // Unit, 3.7874 inch math.eval('12 seconds * 2'); // Unit, 24 seconds math.eval('sin(45 deg)'); // Number, 0.7071067811865475 +math.eval('9.81 m/s^2 * 5 s to mi/h') // Unit, 109.72172512527 mi / h ``` diff --git a/docs/reference/units.md b/docs/reference/units.md index de4645204..2859b3b80 100644 --- a/docs/reference/units.md +++ b/docs/reference/units.md @@ -19,11 +19,16 @@ Electric current | ampere (A) Temperature | kelvin (K), celsius (degC), fahrenheit (degF), rankine (degR) Amount of substance | mole (mol) Luminous intensity | candela (cd) -Force | newton (N), poundforce (lbf) +Force | newton (N), dyne (dyn), poundforce (lbf) +Energy | J, erg, Wh, BTU +Power | W, hp +Pressure | Pa, psi, atm Binary | bit (b), byte (B) Note that all relevant units can also be written in plural form, for example `5 meters` instead of `5 meter` or `10 seconds` instead of `10 second`. +Surface and volume units can alternatively be expressed in terms of length units raised to a power, for example `100 in^2` instead of `100 sqin`. + ## Prefixes The following decimal prefixes are available. diff --git a/examples/units.js b/examples/units.js index db19ccf29..03cc9cae9 100644 --- a/examples/units.js +++ b/examples/units.js @@ -21,10 +21,14 @@ print(a); // 450 mm print(b); // 100 mm console.log(); -// units can be added, subtracted, and multiplied or divided by numbers +// units can be added, subtracted, and multiplied or divided by numbers and by other units console.log('perform operations'); print(math.add(a, b)); // 0.55 m print(math.multiply(b, 2)); // 200 mm +print(math.divide(math.unit('1 m'), math.unit('1 s'))); + // 1 m / s +print(math.pow(math.unit('12 in'), 3)); + // 1728 in^3 console.log(); // units can be converted to a specific type, or to a number @@ -32,14 +36,50 @@ console.log('convert to another type or to a number'); print(b.to('cm')); // 10 cm Alternatively: math.to(b, 'cm') print(math.to(b, 'inch')); // 3.937 inch print(b.toNumber('cm')); // 10 +print(math.number(b, 'cm')); // 10 console.log(); // the expression parser supports units too console.log('parse expressions'); print(math.eval('2 inch to cm')); // 5.08 cm print(math.eval('cos(45 deg)')); // 0.70711 +print(math.eval('90 km/h to m/s')); // 25 m / s console.log(); // convert a unit to a number // A second parameter with the unit for the exported number must be provided print(math.eval('number(5 cm, mm)')); // number, 50 +console.log(); + +// simplify units +console.log('simplify units'); +print(math.eval('100000 N / m^2')); // 100 kPa +print(math.eval('9.81 m/s^2 * 100 kg * 40 m')); // 39.24 kJ +console.log(); + +// example engineering calculations +console.log('compute molar volume of ideal gas at 65 C, 14.7 psi in L/mol'); +var Rg = math.unit('8.314 N m / mol K'); +var P = math.unit('14.7 psi'); +var T = math.unit('65 degF'); +var v = math.divide(math.multiply(Rg, T), P); +console.log('gas constant (Rg) = '); +print(Rg); +console.log('P = '); +print(P); +console.log('T = '); +print(T); +console.log('v = Rg * T / P ='); +print(math.to(v, 'L/mol')); +console.log(); + +console.log('compute speed of fluid flowing out of hole in a container'); +var g = math.unit('9.81 m / s^2'); +var h = math.unit('1 m'); +console.log('g = '); +var v = math.pow(math.multiply(2, math.multiply(g, h)), 0.5); +print(g); +console.log('h = '); +print(h); +console.log('v = (2 g h) ^ 0.5 ='); +print(v); diff --git a/lib/function/arithmetic/divideScalar.js b/lib/function/arithmetic/divideScalar.js index 85fc95513..cdb14b2df 100644 --- a/lib/function/arithmetic/divideScalar.js +++ b/lib/function/arithmetic/divideScalar.js @@ -33,7 +33,17 @@ function factory(type, config, load, typed) { var res = x.clone(); res.value = ((res.value === null) ? res._normalize(1) : res.value) / y; return res; + }, + + 'number, Unit': function (x, y) { + var xUnit = new type.Unit(x); + return xUnit.divide(y); + }, + + 'Unit, Unit': function (x, y) { + return x.divide(y); } + }); /** diff --git a/lib/function/arithmetic/multiplyScalar.js b/lib/function/arithmetic/multiplyScalar.js index 50ee0b591..fc7c6d41f 100644 --- a/lib/function/arithmetic/multiplyScalar.js +++ b/lib/function/arithmetic/multiplyScalar.js @@ -46,7 +46,12 @@ function factory(type, config, load, typed) { var res = x.clone(); res.value = (res.value === null) ? res._normalize(y) : (res.value * y); return res; + }, + + 'Unit, Unit': function (x, y) { + return x.multiply(y); } + }); return multiplyScalar; diff --git a/lib/function/arithmetic/pow.js b/lib/function/arithmetic/pow.js index c21c7ae25..b8b190795 100644 --- a/lib/function/arithmetic/pow.js +++ b/lib/function/arithmetic/pow.js @@ -76,7 +76,12 @@ function factory (type, config, load, typed) { 'Matrix, BigNumber': function (x, y) { return _powMatrix(x, y.toNumber()); + }, + + 'Unit, number': function (x, y) { + return x.pow(y); } + }); /** @@ -156,6 +161,8 @@ function factory (type, config, load, typed) { return matrix(_powArray(x.valueOf(), y)); } + + pow.toTex = '\\left(${args[0]}\\right)' + latex.operators['pow'] + '{${args[1]}}'; return pow; diff --git a/lib/type/unit/Unit.js b/lib/type/unit/Unit.js index f53074a9d..5850138dd 100644 --- a/lib/type/unit/Unit.js +++ b/lib/type/unit/Unit.js @@ -3,7 +3,9 @@ var format = require('../../utils/number').format; var endsWith = require('../../utils/string').endsWith; + function factory (type, config, load, typed) { + /** * @constructor Unit * @@ -16,9 +18,10 @@ function factory (type, config, load, typed) { * var a = new Unit(5, 'cm'); // 50 mm * var b = Unit.parse('23 kg'); // 23 kg * var c = math.in(a, new Unit(null, 'm'); // 0.05 m + * var d = new Unit(9.81, "m/s^2"); // 9.81 m/s^2 * * @param {number} [value] A value like 5.2 - * @param {string} [name] A unit name like "cm" or "inch". Can include a prefix + * @param {string} [name] A unit name like "cm" or "inch", or a derived unit of the form: "u1[^ex1] [u2[^ex2] ...] [/ u3[^ex3] [u4[^ex4]]]", such as "kg m^2/s^2", where each unit appearing after the forward slash is taken to be in the denominator. "kg m^2 s^-2" is a synonym and is also acceptable. Any of the units can include a prefix. */ function Unit(value, name) { if (!(this instanceof Unit)) { @@ -33,23 +36,31 @@ function factory (type, config, load, typed) { } if (name != undefined) { - // find the unit and prefix from the string - var res = _findUnit(name); - if (!res) { - throw new SyntaxError('Unknown unit "' + name + '"'); - } - this.unit = res.unit; - this.prefix = res.prefix; + var u = Unit.parse(name); + this.units = u.units; + this.dimensions = u.dimensions; } else { - this.unit = UNIT_NONE; - this.prefix = PREFIX_NONE; // link to a list with supported prefixes + this.units = [ + { + unit: UNIT_NONE, + prefix: PREFIX_NONE, // link to a list with supported prefixes + power: 0 + } + ]; + this.dimensions = [0, 0, 0, 0, 0, 0, 0, 0, 0]; } this.value = (value != undefined) ? this._normalize(value) : null; + this.fixPrefix = false; // if true, function format will not search for the // best prefix but leave it as initially provided. // fixPrefix is set true by the method Unit.to + + // The justification behind this is that if the constructor is explicitly called, + // the caller wishes the units to be returned exactly as he supplied. + this.isUnitListSimplified = true; + } /** @@ -131,21 +142,28 @@ function factory (type, config, load, typed) { // check for exponential notation like "2.3e-4" or "1.23e50" if (c == 'E' || c == 'e') { - number += c; + // The grammar branches here. This could either be part of an exponent or the start of a unit that begins with the letter e, such as "4exabytes" + + var tentativeNumber = ''; + var tentativeIndex = index; + + tentativeNumber += c; next(); if (c == '+' || c == '-') { - number += c; + tentativeNumber += c; next(); } - // Scientific notation MUST be followed by an exponent + // Scientific notation MUST be followed by an exponent (otherwise we assume it is not scientific notation) if (!isDigit(c)) { - // this is no legal number, exponent is missing. - revert(oldIndex); - return null; + // The e or E must belong to something else, so return the number without the e or E. + revert(tentativeIndex); + return number; } - + + // We can now safely say that this is scientific notation. + number = number + tentativeNumber; while (isDigit(c)) { number += c; next(); @@ -158,19 +176,41 @@ function factory (type, config, load, typed) { function parseUnit() { var unitName = ''; - skipWhitespace(); - while (c && c != ' ' && c != '\t') { + // Alphanumeric characters only; matches [a-zA-Z0-9] + var code = text.charCodeAt(index); + while ( (code >= 48 && code <= 57) || + (code >= 65 && code <= 90) || + (code >= 97 && code <= 122)) { unitName += c; next(); + var code = text.charCodeAt(index); } - return unitName || null; + // Must begin with [a-zA-Z] + var code = unitName.charCodeAt(0); + if ((code >= 65 && code <= 90) || + (code >= 97 && code <= 122)) { + return unitName || null; + } + else { + return null; + } + } + + function parseCharacter(toFind) { + if (c === toFind) { + next(); + return toFind; + } + else { + return null; + } } /** * Parse a string into a unit. Returns null if the provided string does not * contain a valid unit. - * @param {string} str A string like "5.2 inch", "4e2 kg" + * @param {string} str A string like "5.2 inch", "4e2 cm/s^2" * @return {Unit | null} unit */ Unit.parse = function (str) { @@ -179,54 +219,118 @@ function factory (type, config, load, typed) { c = ''; if (typeof text !== 'string') { + throw new TypeError('Invalid argument in Unit.parse. Valid types are {string}.'); return null; } + var unit = new Unit(); + unit.units = []; + + // A unit should follow this pattern: + // [number]unit[^number] [unit[^number]]...[/unit[^number] [unit[^number]]] + + // Rules: + // number is any floating point number. + // unit is any alphanumeric string beginning with an alpha. Units with names like e3 should be avoided because they look like the exponent of a floating point number! + // The string may optionally begin with a number. + // Each unit may optionally be followed by ^number. + // Whitespace or a forward slash is recommended between consecutive units, although the following technically is parseable: + // 2m^2kg/s^2 + // it is not good form. If a unit starts with e, then it could be confused as a floating point number: + // 4erg + next(); skipWhitespace(); - var value = parseNumber(); - var name; - if (value) { - name = parseUnit(); - - next(); - skipWhitespace(); - if (c) { - // garbage at the end. not good. - return null; - } - - if (value && name) { - try { - // constructor will throw an error when unit is not found - return new Unit(Number(value), name); - } - catch (err) { - } - } + // Optional number at the start of the string + var valueStr = parseNumber(); + var value = null; + if(valueStr) { + value = parseFloat(valueStr); } - else { - name = parseUnit(); + skipWhitespace(); // Whitespace is not required here - next(); - skipWhitespace(); - if (c) { - // garbage at the end. not good. - return null; - } + // Next, we read any number of unit[^number] + var powerMultiplier = 1.0; + var expectingDenominator = false; + while (true) { + skipWhitespace(); + if(c) { + var uStr = parseUnit(); + if(uStr == null) { + // No more units. + throw new SyntaxError('Could not parse'); + } + } + else { + // End of input. + break; + } + var res = _findUnit(uStr); + if(res == null) { + // Unit not found. + //return null; + throw new SyntaxError('Unit "' + uStr + '" not found.'); + } + var power = 1.0 * powerMultiplier; + // Is there a "^ number"? + skipWhitespace(); + if (parseCharacter('^')) { + skipWhitespace(); + var p = parseNumber(); + if(p == null) { + // No valid number found for the power! + throw new SyntaxError('In "' + str + '", "^" must be followed by a floating-point number'); + } + power = p * powerMultiplier; + } + // Add the unit + unit.units.push( { + unit: res.unit, + prefix: res.prefix, + power: power + }); + for(var i=0; i 1 || Math.abs(this.units[0].power - 1.0) > 1e-15; + } + + /** + * Normalize a value, based on its currently set unit(s) * @param {number} value * @return {number} normalized value * @private */ Unit.prototype._normalize = function (value) { - return (value + this.unit.offset) * this.unit.value * this.prefix.value; + if (this.units.length === 0) { + return value; + } + else if (this._isDerived()) { + // This is a derived unit, so do not apply offsets. + // For example, with J kg^-1 degC^-1 you would NOT want to apply the offset. + var res = value; + for(var i=0; i < this.units.length; i++) { + res = res * Math.pow(this.units[i].unit.value * this.units[i].prefix.value, this.units[i].power); + } + return res; + } + else { + // This is a single unit of power 1, like kg or degC + return (value + this.units[0].unit.offset) * this.units[0].unit.value * this.units[0].prefix.value; + } }; /** - * Denormalize a value, based on its currently set unit + * Denormalize a value, based on its currently set unit(s) * @param {number} value - * @param {number} [prefixValue] Optional prefix value to be used + * @param {number} [prefixValue] Optional prefix value to be used (ignored if this is a derived unit) * @return {number} denormalized value * @private */ Unit.prototype._denormalize = function (value, prefixValue) { - if (prefixValue == undefined) { - return value / this.unit.value / this.prefix.value - this.unit.offset; - } - else { - return value / this.unit.value / prefixValue - this.unit.offset; - } - }; + if (this.units.length === 0) { + return value; + } + else if (this._isDerived()) { + // This is a derived unit, so do not apply offsets. + // For example, with J kg^-1 degC^-1 you would NOT want to apply the offset. + // Also, prefixValue is ignored--but we will still use the prefix value stored in each unit, since kg is usually preferrable to g unless the user decides otherwise. + var res = value; + for(var i=0; i 1e-12) { + return false; + } + } + return true; + }; /** - * Check if this unit has a base equal to another base + * Check if this unit has a base or bases equal to another base or bases + * For derived units, the exponent on each base also must match * @param {Unit} other * @return {boolean} true if equal base */ Unit.prototype.equalBase = function (other) { - return (this.unit.base === other.unit.base); + // All dimensions must be the same + for(var i=0; i 1e-12) { + return false; + } + } + return true; }; /** @@ -336,20 +509,122 @@ function factory (type, config, load, typed) { return (this.equalBase(other) && this.value == other.value); }; + /** + * Multiply this unit with another one + * @param {Unit} other + * @return {Unit} product of this unit and the other unit + */ + Unit.prototype.multiply = function (other) { + + var res = this.clone(); + + for(var i=0; i 1e-12) { + proposedUnitList.push({ + unit: currentUnitSystem[baseDim].unit, + prefix: currentUnitSystem[baseDim].prefix, + power: this.dimensions[i] + }); + } + } + + // Is the proposed unit list "simpler" than the existing one? + if(proposedUnitList.length < this.units.length) { + // Replace this unit list with the proposed list + this.units = proposedUnitList; + } + this.isUnitListSimplified = true; + return; + } + } + + /** + * Get a string representation of the units of this Unit, without the value. + * @return {string} + */ + Unit.prototype.formatUnits = function () { + + // Lazy evaluation of the unit list + this.simplifyUnitListLazy(); + + var strNum = ""; + var strDen = ""; + var nNum = 0; + var nDen = 0; + + for(var i=0; i 0) { + nNum++; + strNum += " " + this.units[i].prefix.name + this.units[i].unit.name; + if(Math.abs(this.units[i].power - 1.0) > 1e-15) { + strNum += "^" + this.units[i].power; + } + } + else if(this.units[i].power < 0) { + nDen++; + } + } + + if(nDen > 0) { + for(var i=0; i 0) { + strDen += " " + this.units[i].prefix.name + this.units[i].unit.name; + if(Math.abs(this.units[i].power + 1.0) > 1e-15) { + strDen += "^" + (-this.units[i].power); + } + } + else { + strDen += " " + this.units[i].prefix.name + this.units[i].unit.name; + strDen += "^" + (this.units[i].power); + } + } + } + } + // Remove leading " " + strNum = strNum.substr(1); + strDen = strDen.substr(1); + + // Add parans for better copy/paste back into the eval, for example, or for better pretty print formatting + if(nNum > 1 && nDen > 0) { + strNum = "(" + strNum + ")"; + } + if(nDen > 1 && nNum > 0) { + strDen = "(" + strDen + ")"; + } + + var str = strNum; + if(nNum > 0 && nDen > 0) { + str += " / "; + } + str += strDen; + + return str; + }; + /** * Get a string representation of the Unit, with optional formatting options. * @param {Object | number | Function} [options] Formatting options. See @@ -432,41 +847,63 @@ function factory (type, config, load, typed) { * options. * @return {string} */ - Unit.prototype.format = function (options) { - var value, - str; + Unit.prototype.format = function (options) { - if (this.value !== null && !this.fixPrefix) { - var bestPrefix = this._bestPrefix(); - value = this._denormalize(this.value, bestPrefix.value); - str = format(value, options) + ' '; - str += bestPrefix.name + this.unit.name; - } - else { - value = this._denormalize(this.value); - str = (this.value !== null) ? (format(value, options) + ' ') : ''; - str += this.prefix.name + this.unit.name; - } +// Simplfy the unit list, if necessary + this.simplifyUnitListLazy(); - return str; - }; + var value, + str; + if (this._isDerived()) { + value = this._denormalize(this.value); + str = (this.value !== null) ? (format(value, options)) : ''; + var unitStr = this.formatUnits(); + if(unitStr.length > 0 && str.length > 0) { + str += " "; + } + str += unitStr; + } + else if (this.units.length === 1) { + if (this.value !== null && !this.fixPrefix) { + var bestPrefix = this._bestPrefix(); + value = this._denormalize(this.value, bestPrefix.value); + str = format(value, options) + ' '; + str += bestPrefix.name + this.units[0].unit.name; + } + else { + value = this._denormalize(this.value); + str = (this.value !== null) ? (format(value, options) + ' ') : ''; + str += this.units[0].prefix.name + this.units[0].unit.name; + } + } + else if (this.units.length === 0) { + str = format(this.value, options); + } + + + return str; + }; /** * Calculate the best prefix using current value. * @returns {Object} prefix * @private */ - Unit.prototype._bestPrefix = function () { + Unit.prototype._bestPrefix = function () { + if(this._isDerived()) { + throw "Can only compute the best prefix for non-derived units, like kg, s, N, and so forth!"; + } + // find the best prefix value (resulting in the value of which // the absolute value of the log10 is closest to zero, // though with a little offset of 1.2 for nicer values: you get a // sequence 1mm 100mm 500mm 0.6m 1m 10m 100m 500m 0.6km 1km ... - var absValue = Math.abs(this.value / this.unit.value); + var absValue = Math.abs(this.value / this.units[0].unit.value); var bestPrefix = PREFIX_NONE; var bestDiff = Math.abs( Math.log(absValue / bestPrefix.value) / Math.LN10 - 1.2); - var prefixes = this.unit.prefixes; + var prefixes = this.units[0].unit.prefixes; for (var p in prefixes) { if (prefixes.hasOwnProperty(p)) { var prefix = prefixes[p]; @@ -628,32 +1065,92 @@ function factory (type, config, load, typed) { 'exi': {name: 'exi', value: Math.pow(1024, 6), scientific: true}, 'zebi': {name: 'zebi', value: Math.pow(1024, 7), scientific: true}, 'yobi': {name: 'yobi', value: Math.pow(1024, 8), scientific: true} + }, + BTU: { + '': {name: '', value: 1, scientific: true}, + 'MM': {name: 'MM', value: 1e6, scientific: true} } }; var PREFIX_NONE = {name: '', value: 1, scientific: true}; + /* Internally, each unit is represented by a value and a dimension array. The elements of the dimensions array have the following meaning: + * Index Dimension + * ----- --------- + * 0 Length + * 1 Mass + * 2 Time + * 3 Current + * 4 Temperature + * 5 Luminous intensity + * 6 Amount of substance + * 7 Angle + * 8 Bit (digital) + * For example, the unit "298.15 K" is a pure temperature and would have a value of 298.15 and a dimension array of [0, 0, 0, 0, 1, 0, 0, 0, 0]. The unit "1 cal / (gm °C)" can be written in terms of the 9 fundamental dimensions as [length^2] / ([time^2] * [temperature]), and would a value of (after conversion to SI) 4184.0 and a dimensions array of [2, 0, -2, 0, -1, 0, 0, 0, 0]. + * + */ + + var BASE_DIMENSIONS = ["MASS", "LENGTH", "TIME", "CURRENT", "TEMPERATURE", "LUMINOUS_INTENSITY", "AMOUNT_OF_SUBSTANCE", "ANGLE", "BIT"]; + var BASE_UNITS = { - NONE: {}, + NONE: { + dimensions: [0, 0, 0, 0, 0, 0, 0, 0, 0] + }, + MASS: { + dimensions: [1, 0, 0, 0, 0, 0, 0, 0, 0] + }, + LENGTH: { + dimensions: [0, 1, 0, 0, 0, 0, 0, 0, 0] + }, + TIME: { + dimensions: [0, 0, 1, 0, 0, 0, 0, 0, 0] + }, + CURRENT: { + dimensions: [0, 0, 0, 1, 0, 0, 0, 0, 0] + }, + TEMPERATURE: { + dimensions: [0, 0, 0, 0, 1, 0, 0, 0, 0] + }, + LUMINOUS_INTENSITY: { + dimensions: [0, 0, 0, 0, 0, 1, 0, 0, 0] + }, + AMOUNT_OF_SUBSTANCE: { + dimensions: [0, 0, 0, 0, 0, 0, 1, 0, 0] + }, - LENGTH: {}, // meter - MASS: {}, // kilogram - TIME: {}, // second - CURRENT: {}, // ampere - TEMPERATURE: {}, // kelvin - LUMINOUS_INTENSITY: {}, // candela - AMOUNT_OF_SUBSTANCE: {}, // mole - - FORCE: {}, // Newton - SURFACE: {}, // m2 - VOLUME: {}, // m3 - ANGLE: {}, // rad - BIT: {} // bit (digital) + FORCE: { + dimensions: [1, 1, -2, 0, 0, 0, 0, 0, 0] + }, + SURFACE: { + dimensions: [0, 2, 0, 0, 0, 0, 0, 0, 0] + }, + VOLUME: { + dimensions: [0, 3, 0, 0, 0, 0, 0, 0, 0] + }, + ENERGY: { + dimensions: [1, 2, -2, 0, 0, 0, 0, 0, 0] + }, + POWER: { + dimensions: [1, 2, -3, 0, 0, 0, 0, 0, 0] + }, + PRESSURE: { + dimensions: [1, -1, -2, 0, 0, 0, 0, 0, 0] + }, + ANGLE: { + dimensions: [0, 0, 0, 0, 0, 0, 0, 1, 0] + }, + BIT: { + dimensions: [0, 0, 0, 0, 0, 0, 0, 0, 1] + } }; + for(var key in BASE_UNITS) { + BASE_UNITS[key].key = key; + } + var BASE_UNIT_NONE = {}; - var UNIT_NONE = {name: '', base: BASE_UNIT_NONE, value: 1, offset: 0}; + var UNIT_NONE = {name: '', base: BASE_UNIT_NONE, value: 1, offset: 0, dimensions: [0,0,0,0,0,0,0,0,0]}; var UNITS = { // length @@ -1410,6 +1907,20 @@ function factory (type, config, load, typed) { value: 1, offset: 0 }, + dyn: { + name: 'dyn', + base: BASE_UNITS.FORCE, + prefixes: PREFIXES.SHORT, + value: 0.00001, + offset: 0 + }, + dyne: { + name: 'dyne', + base: BASE_UNITS.FORCE, + prefixes: PREFIXES.LONG, + value: 0.00001, + offset: 0 + }, lbf: { name: 'lbf', base: BASE_UNITS.FORCE, @@ -1424,6 +1935,74 @@ function factory (type, config, load, typed) { value: 4.4482216152605, offset: 0 }, + // Energy + J: { + name: 'J', + base: BASE_UNITS.ENERGY, + prefixes: PREFIXES.SHORT, + value: 1, + offset: 0 + }, + erg: { + name: 'erg', + base: BASE_UNITS.ENERGY, + prefixes: PREFIXES.NONE, + value: 1e-5, + offset: 0 + }, + Wh: { + name: 'Wh', + base: BASE_UNITS.ENERGY, + prefixes: PREFIXES.SHORT, + value: 3600, + offset: 0 + }, + BTU: { + name: 'BTU', + base: BASE_UNITS.ENERGY, + prefixes: PREFIXES.BTU, + value: 1055.05585262, + offset: 0 + }, + + // Power + W: { + name: 'W', + base: BASE_UNITS.POWER, + prefixes: PREFIXES.SHORT, + value: 1, + offset: 0 + }, + hp: { + name: 'hp', + base: BASE_UNITS.POWER, + prefixes: PREFIXES.NONE, + value: 745.6998715386, + offset: 0 + }, + + // Pressure + Pa: { + name: 'Pa', + base: BASE_UNITS.PRESSURE, + prefixes: PREFIXES.SHORT, + value: 1, + offset: 0 + }, + psi: { + name: 'psi', + base: BASE_UNITS.PRESSURE, + prefixes: PREFIXES.NONE, + value: 6894.75729276459, + offset: 0 + }, + atm: { + name: 'atm', + base: BASE_UNITS.PRESSURE, + prefixes: PREFIXES.NONE, + value: 101325, + offset: 0 + }, // Binary b: { @@ -1508,6 +2087,103 @@ function factory (type, config, load, typed) { moles: 'mole' }; + /** + * A unit system is a set of dimensionally independent base units plus a set of derived units, formed by multiplication and division of the base units, that are by convention used with the unit system. + * A user perhaps could issue a command to select a preferred unit system, or use the default (see below). + * Auto unit system: The default unit system is updated on the fly anytime a unit is parsed. The corresponding unit in the default unit system is updated, so that answers are given in the same units the user supplies. + */ + var UNIT_SYSTEMS = { + si: { + // Base units + NONE: {unit: UNIT_NONE, prefix: PREFIXES.NONE['']}, + LENGTH: {unit: UNITS.m, prefix: PREFIXES.SHORT['']}, + MASS: {unit: UNITS.g, prefix: PREFIXES.SHORT['k']}, + TIME: {unit: UNITS.s, prefix: PREFIXES.SHORT['']}, + CURRENT: {unit: UNITS.A, prefix: PREFIXES.SHORT['']}, + TEMPERATURE: {unit: UNITS.K, prefix: PREFIXES.SHORT['']}, + LUMINOUS_INTENSITY: {unit: UNITS.cd, prefix: PREFIXES.SHORT['']}, + AMOUNT_OF_SUBSTANCE: {unit: UNITS.mol, prefix: PREFIXES.SHORT['']}, + ANGLE: {unit: UNITS.rad, prefix: PREFIXES.SHORT['']}, + BIT: {unit: UNITS.bit, prefix: PREFIXES.SHORT['']}, + + // Derived units + FORCE: {unit: UNITS.N, prefix: PREFIXES.SHORT['']}, + ENERGY: {unit: UNITS.J, prefix: PREFIXES.SHORT['']}, + POWER: {unit: UNITS.W, prefix: PREFIXES.SHORT['']}, + PRESSURE: {unit: UNITS.Pa, prefix: PREFIXES.SHORT['']}, + } + }; + + // Clone to create the other unit systems + UNIT_SYSTEMS.cgs = JSON.parse(JSON.stringify(UNIT_SYSTEMS.si)); + UNIT_SYSTEMS.cgs.LENGTH = {unit: UNITS.m, prefix: PREFIXES.SHORT['c']}; + UNIT_SYSTEMS.cgs.MASS = {unit: UNITS.g, prefix: PREFIXES.SHORT['']}; + UNIT_SYSTEMS.cgs.FORCE = {unit: UNITS.dyn, prefix: PREFIXES.SHORT['']}; + UNIT_SYSTEMS.cgs.ENERGY = {unit: UNITS.erg, prefix: PREFIXES.NONE['']}; + + UNIT_SYSTEMS.us = JSON.parse(JSON.stringify(UNIT_SYSTEMS.si)); + UNIT_SYSTEMS.us.LENGTH = {unit: UNITS.ft, prefix: PREFIXES.NONE['']}; + UNIT_SYSTEMS.us.MASS = {unit: UNITS.lbm, prefix: PREFIXES.NONE['']}; + UNIT_SYSTEMS.us.TEMPERATURE = {unit: UNITS.degF, prefix: PREFIXES.NONE['']}; + UNIT_SYSTEMS.us.FORCE = {unit: UNITS.lbf, prefix: PREFIXES.NONE['']}; + UNIT_SYSTEMS.us.ENERGY = {unit: UNITS.BTU, prefix: PREFIXES.BTU['']}; + UNIT_SYSTEMS.us.POWER = {unit: UNITS.hp, prefix: PREFIXES.NONE['']}; + UNIT_SYSTEMS.us.PRESSURE = {unit: UNITS.psi, prefix: PREFIXES.NONE['']}; + + // Add additional unit systems here. + + + + // Choose a unit system to seed the auto unit system. + UNIT_SYSTEMS.auto = JSON.parse(JSON.stringify(UNIT_SYSTEMS.si)); + + // Set the current unit system + var currentUnitSystem = UNIT_SYSTEMS.auto; + + /** + * Set a unit system for formatting derived units. + * @param {string} [name] The name of the unit system. + */ + Unit.setUnitSystem = function(name) { + if(UNIT_SYSTEMS.hasOwnProperty(name)) { + currentUnitSystem = UNIT_SYSTEMS[name]; + } + else { + var mess = "Unit system " + name + " does not exist. Choices are: " + listAvailableUnitSystems(); + } + } + + /** + * Return a list of the available unit systems. + * @return {string} A space-delimited string of the available unit systems. + */ + Unit.listAvailableUnitSystems = function() { + var mess = ""; + for(var key in UNIT_SYSTEMS) { + mess += " " + key; + } + return mess.substr(1); + } + + /** + * Return the current unit system. + * @return {string} The current unit system. + */ + Unit.getUnitSystem = function() { + for(var key in UNIT_SYSTEMS) { + if(UNIT_SYSTEMS[key] === currentUnitSystem) { + return key; + } + } + } + + + // Add dimensions to each built-in unit + for (var key in UNITS) { + var unit = UNITS[key]; + unit.dimensions = unit.base.dimensions; + } + for (var name in PLURALS) { /* istanbul ignore next (we cannot really test next statement) */ if (PLURALS.hasOwnProperty(name)) { @@ -1528,6 +2204,7 @@ function factory (type, config, load, typed) { Unit.PREFIXES = PREFIXES; Unit.BASE_UNITS = BASE_UNITS; Unit.UNITS = UNITS; + Unit.UNIT_SYSTEMS = UNIT_SYSTEMS; return Unit; } diff --git a/test/function/arithmetic/divide.test.js b/test/function/arithmetic/divide.test.js index 567a06170..f8f2828a2 100644 --- a/test/function/arithmetic/divide.test.js +++ b/test/function/arithmetic/divide.test.js @@ -123,6 +123,23 @@ describe('divide', function() { assert.equal(divide(math.unit('m'), 2).toString(), '500 mm'); }); + it('should divide a number by a unit', function() { + assert.equal(divide(20, math.unit('4 N s')).toString(), '5 N^-1 s^-1'); + }); + + it('should divide two units', function() { + assert.equal(divide(math.unit('75 mi/h'), math.unit('40 mi/gal')).to('gal/minute').toString(), '0.03125 gal / minute'); + }); + + it('should divide one valued unit by a valueless unit and vice-versa', function() { + assert.equal(divide(math.unit('4 gal'), math.unit('L')).format(5), '15.142'); + assert.equal(divide(math.unit('gal'), math.unit('4 L')).format(3), '0.946'); + }); + + it('should divide (but not simplify) two valueless units', function() { + assert.equal(divide(math.unit('gal'), math.unit('L')).toString(), 'gal / L'); + }); + // TODO: divide units by a bignumber it('should divide units by a big number', function() { //assert.equal(divide(math.unit('5 m'), bignumber(10)).toString(), '500 mm'); // TODO @@ -160,6 +177,8 @@ describe('divide', function() { assert.throws(function () {divide(a, [[1]])}); }); + /* + // These are supported now --ericman314 it('should throw an error if dividing a number by a unit', function() { assert.throws(function () {divide(10, math.unit('5 m')).toString()}); }); @@ -167,6 +186,7 @@ describe('divide', function() { it('should throw an error if dividing a unit by a non-number', function() { assert.throws(function () {divide(math.unit('5 m'), math.unit('5cm')).toString()}); }); + */ it('should throw an error if there\'s wrong number of arguments', function() { assert.throws(function () {divide(2,3,4); }); diff --git a/test/function/arithmetic/dotDivide.test.js b/test/function/arithmetic/dotDivide.test.js index ff23a9cca..74b907cd2 100644 --- a/test/function/arithmetic/dotDivide.test.js +++ b/test/function/arithmetic/dotDivide.test.js @@ -52,9 +52,16 @@ describe('dotDivide', function() { assert.equal(dotDivide(math.unit('5 m'), 10).toString(), '500 mm'); }); + it('should divide a number by a unit', function() { + assert.equal(dotDivide(10, math.unit('5 m')).toString(), '2 m^-1'); + }); + + /* + // This is supported not --ericman314 it('should throw an error if dividing a number by a unit', function() { assert.throws(function () {dotDivide(10, math.unit('5 m')).toString();}); }); + */ describe('Array', function () { diff --git a/test/function/arithmetic/multiply.test.js b/test/function/arithmetic/multiply.test.js index 62fec64ef..3a98d27d1 100644 --- a/test/function/arithmetic/multiply.test.js +++ b/test/function/arithmetic/multiply.test.js @@ -154,6 +154,22 @@ describe('multiply', function() { assert.equal(multiply(unit('inch'), 2).toString(), '2 inch'); }); + it('should multiply two units correctly', function() { + assert.equal(multiply(unit('2 m'), unit('4 m')).toString(), '8 m^2'); + assert.equal(multiply(unit('2 ft'), unit('4 ft')).toString(), '8 ft^2'); + assert.equal(multiply(unit('65 mi/h'), unit('2 h')).to('mi').toString(), '130 mi'); + assert.equal(multiply(unit('2 L'), unit('1 s^-1')).toString(), '2 L / s'); + assert.equal(multiply(unit('2 m/s'), unit('0.5 s/m')).toString(), '1'); + }); + + it('should multiply valueless units correctly', function() { + assert.equal(multiply(unit('m'), unit('4 m')).toString(), '4 m^2'); + assert.equal(multiply(unit('ft'), unit('4 ft')).format(5), '4 ft^2'); + assert.equal(multiply(unit('65 mi/h'), unit('h')).to('mi').toString(), '65 mi'); + assert.equal(multiply(unit('2 L'), unit('s^-1')).toString(), '2 L / s'); + assert.equal(multiply(unit('m/s'), unit('h/m')).toString(), '(m h) / (s m)'); + }); + // TODO: cleanup once decided to not downgrade BigNumber to number it.skip('should multiply a bignumber and a unit correctly', function() { assert.equal(multiply(bignumber(2), unit('5 mm')).toString(), '10 mm'); @@ -172,12 +188,15 @@ describe('multiply', function() { assert.equal(multiply(unit('inch'), bignumber(2)).toString(), '2 inch'); }); + it('should throw an error in case of unit non-numeric argument', function() { - assert.throws(function () {multiply(math.unit('5cm'), math.unit('4cm'));}, /TypeError: Unexpected type/); + // Multiplying two units is supported now --ericman314 + //assert.throws(function () {multiply(math.unit('5cm'), math.unit('4cm'));}, /TypeError: Unexpected type/); assert.throws(function () {multiply(math.unit('5cm'), math.complex('2+3i'));}, /TypeError: Unexpected type/); assert.throws(function () {multiply(math.complex('2+3i'), math.unit('5cm'));}, /TypeError: Unexpected type/); }); + it('should throw an error if used with strings', function() { assert.throws(function () {multiply("hello", "world");}); assert.throws(function () {multiply("hello", 2);}); diff --git a/test/function/arithmetic/pow.test.js b/test/function/arithmetic/pow.test.js index b5ae83223..fcd1ddda1 100644 --- a/test/function/arithmetic/pow.test.js +++ b/test/function/arithmetic/pow.test.js @@ -152,8 +152,27 @@ describe('pow', function() { approx.deepEqual(pow(complex(0, 2), math.bignumber(2)), complex(-4, 0)); }); - it('should throw an error if used with a unit', function() { - assert.throws(function () {pow(unit('5cm'), 2)}); + it('should correctly calculate unit ^ number', function() { + assert.equal(pow(unit('4 N'), 2).toString(), "16 N^2"); + assert.equal(pow(unit('0.25 m/s'), -0.5).toString(), "2 s^0.5 / m^0.5"); + assert.equal(pow(unit('123 hogshead'), 0).toString(), "1"); + }); + + it('should return a cloned value and not affect the argument', function() { + var unit1 = unit('2 m'); + var unit2 = pow(unit1, 2); + + assert.equal(unit1.toString(), '2 m'); + assert.equal(unit2.toString(), '4 m^2'); + }); + + it('should return a valuelessUnit when calculating valuelessUnit ^ number', function() { + assert.equal(pow(unit('kg^0.5 m^0.5 s^-1'), 2).toString(), "(kg m) / s^2"); + }); + + it('should throw an error when doing number ^ unit', function() { + // This is supported now --ericman314 + //assert.throws(function () {pow(unit('5cm'), 2)}); assert.throws(function () {pow(2, unit('5cm'))}); }); diff --git a/test/function/units/to.test.js b/test/function/units/to.test.js index 1bc68e4e9..083ef0c6d 100644 --- a/test/function/units/to.test.js +++ b/test/function/units/to.test.js @@ -60,7 +60,7 @@ describe('to', function() { it('should throw an error if converting between incompatible units', function() { assert.throws(function () {math.to(unit('20 kg'), unit('cm'));}); assert.throws(function () {math.to(unit('20 celsius'), unit('litre'));}); - assert.throws(function () {math.to(unit('5 cm'), unit('2 m'));}); + assert.throws(function () {math.to(unit('5 cm'), unit('2 m^2'));}); }); it('should throw an error if called with a wrong number of arguments', function() { @@ -74,7 +74,7 @@ describe('to', function() { it('should throw an error if called with a number', function() { assert.throws(function () {math.to(5, unit('m'));}, TypeError); - assert.throws(function () {math.to(unit('5cm'), 2);}, /SyntaxError: Unknown unit "2"/); + assert.throws(function () {math.to(unit('5cm'), 2);}, /SyntaxError: "2" contains no units/); }); it('should throw an error if called with a string', function() { diff --git a/test/type/unit/Unit.test.js b/test/type/unit/Unit.test.js index 54a9f0afc..102553e2e 100644 --- a/test/type/unit/Unit.test.js +++ b/test/type/unit/Unit.test.js @@ -10,33 +10,39 @@ describe('unit', function() { it('should create unit correctly', function() { var unit1 = new Unit(5000, 'cm'); assert.equal(unit1.value, 50); - assert.equal(unit1.unit.name, 'm'); + assert.equal(unit1.units[0].unit.name, 'm'); unit1 = new Unit(5, 'kg'); assert.equal(unit1.value, 5); - assert.equal(unit1.unit.name, 'g'); + assert.equal(unit1.units[0].unit.name, 'g'); unit1 = new Unit(null, 'kg'); assert.equal(unit1.value, null); - assert.equal(unit1.unit.name, 'g'); + assert.equal(unit1.units[0].unit.name, 'g'); + + unit1 = new Unit(9.81, "kg m/s^2"); + assert.equal(unit1.value, 9.81); + assert.equal(unit1.units[0].unit.name, 'g'); + assert.equal(unit1.units[1].unit.name, 'm'); + assert.equal(unit1.units[2].unit.name, 's'); }); it('should create square meter correctly', function() { var unit1 = new Unit(0.000001, 'km2'); assert.equal(unit1.value, 1); - assert.equal(unit1.unit.name, 'm2'); + assert.equal(unit1.units[0].unit.name, 'm2'); }); it('should create cubic meter correctly', function() { var unit1 = new Unit(0.000000001, 'km3'); assert.equal(unit1.value, 1); - assert.equal(unit1.unit.name, 'm3'); + assert.equal(unit1.units[0].unit.name, 'm3'); }); it('should ignore properties on Object.prototype', function() { Object.prototype.foo = Unit.UNITS['meter']; - assert.throws(function () {new Unit(1, 'foo')}, /Unknown unit/); + assert.throws(function () {new Unit(1, 'foo')}, /Unit "foo" not found/); delete Object.prototype.foo; }); @@ -54,6 +60,16 @@ describe('unit', function() { assert.throws(function () { new Unit(0, 3); }); }); + it('should flag unit as already simplified', function() { + unit1 = new Unit(9.81, "kg m/s^2"); + assert.equal(unit1.isUnitListSimplified, true); + assert.equal(unit1.toString(), "9.81 (kg m) / s^2"); + + unit1 = new Unit(null, "kg m/s^2"); + assert.equal(unit1.isUnitListSimplified, true); + assert.equal(unit1.toString(), "(kg m) / s^2"); + }); + }); describe('isValuelessUnit', function() { @@ -90,6 +106,7 @@ describe('unit', function() { it('should test whether a unit has a certain base unit', function() { assert.equal(new Unit(5, 'cm').hasBase(Unit.BASE_UNITS.ANGLE), false); assert.equal(new Unit(5, 'cm').hasBase(Unit.BASE_UNITS.LENGTH), true); + assert.equal(new Unit(5, 'kg m / s ^ 2').hasBase(Unit.BASE_UNITS.FORCE), true); }); }); @@ -99,6 +116,8 @@ describe('unit', function() { it('should test whether two units have the same base unit', function() { assert.equal(new Unit(5, 'cm').equalBase(new Unit(10, 'm')), true); assert.equal(new Unit(5, 'cm').equalBase(new Unit(10, 'kg')), false); + assert.equal(new Unit(5, 'N').equalBase(new Unit(10, 'kg m / s ^ 2')), true); + assert.equal(new Unit(8.314, 'J / mol K').equalBase(new Unit(0.02366, 'ft^3 psi / mol degF')), true); }); }); @@ -109,6 +128,9 @@ describe('unit', function() { assert.equal(new Unit(100, 'cm').equals(new Unit(1, 'm')), true); assert.equal(new Unit(100, 'cm').equals(new Unit(2, 'm')), false); assert.equal(new Unit(100, 'cm').equals(new Unit(1, 'kg')), false); + assert.equal(new Unit(100, 'ft lbf').equals(new Unit(1200, 'in lbf')), true); + assert.equal(new Unit(100, 'N').equals(new Unit(100, 'kg m / s ^ 2')), true); + assert.equal(new Unit(100, 'N').equals(new Unit(100, 'kg m / s')), false); }); }); @@ -131,6 +153,11 @@ describe('unit', function() { assert(u5 !== u6); assert.deepEqual(u5, u6); + var u7 = new Unit(8.314, 'kg m^2 / s^2 K mol'); + var u8 = u7.clone(); + assert(u7 !== u8); + assert.deepEqual(u7, u8); + }); }); @@ -141,12 +168,18 @@ describe('unit', function() { approx.equal(u.toNumber('mm'), 50000); approx.equal(new Unit(5.08, 'cm').toNumber('inch'), 2); + + approx.equal(new Unit(101325, 'N/m^2').toNumber('lbf/in^2'), 14.6959487763741); }); it ('should convert a unit with fixed prefix to a number', function () { var u1 = new Unit(5000, 'cm'); var u2 = u1.to('km'); approx.equal(u2.toNumber('mm'), 50000); + + var u1 = new Unit(981, 'cm/s^2'); + var u2 = u1.to('km/ms^2'); + approx.equal(u2.toNumber('m/s^2'), 9.81); }); }); @@ -155,81 +188,143 @@ describe('unit', function() { it ('should convert a unit to a fixed unitName', function () { var u1 = new Unit(5000, 'cm'); assert.equal(u1.value, 50); - assert.equal(u1.unit.name, 'm'); - assert.equal(u1.prefix.name, 'c'); + assert.equal(u1.units[0].unit.name, 'm'); + assert.equal(u1.units[0].prefix.name, 'c'); assert.equal(u1.fixPrefix, false); var u2 = u1.to('inch'); assert.notStrictEqual(u1, u2); // u2 must be a clone assert.equal(u2.value, 50); - assert.equal(u2.unit.name, 'inch'); - assert.equal(u2.prefix.name, ''); + assert.equal(u2.units[0].unit.name, 'inch'); + assert.equal(u2.units[0].prefix.name, ''); assert.equal(u2.fixPrefix, true); + + var u3 = new Unit(299792.458, 'km/s'); + assert.equal(u3.value, 299792458); + assert.equal(u3.units[0].unit.name, 'm'); + assert.equal(u3.units[1].unit.name, 's'); + assert.equal(u3.units[0].prefix.name, 'k'); + assert.equal(u3.fixPrefix, false); + + var u4 = u3.to('mi/h'); + assert.notStrictEqual(u3, u4); // u4 must be a clone + assert.equal(u4.value, 299792458); + assert.equal(u4.units[0].unit.name, 'mi'); + assert.equal(u4.units[1].unit.name, 'h'); + assert.equal(u4.units[0].prefix.name, ''); + assert.equal(u4.fixPrefix, true); }); it ('should convert a unit to a fixed unit', function () { var u1 = new Unit(5000, 'cm'); assert.equal(u1.value, 50); - assert.equal(u1.unit.name, 'm'); - assert.equal(u1.prefix.name, 'c'); + assert.equal(u1.units[0].unit.name, 'm'); + assert.equal(u1.units[0].prefix.name, 'c'); assert.equal(u1.fixPrefix, false); var u2 = u1.to(new Unit(null, 'km')); assert.notStrictEqual(u1, u2); // u2 must be a clone assert.equal(u2.value, 50); - assert.equal(u2.unit.name, 'm'); - assert.equal(u2.prefix.name, 'k'); + assert.equal(u2.units[0].unit.name, 'm'); + assert.equal(u2.units[0].prefix.name, 'k'); + assert.equal(u2.fixPrefix, true); + + var u1 = new Unit(5000, 'cm/s'); + assert.equal(u1.value, 50); + assert.equal(u1.units[0].unit.name, 'm'); + assert.equal(u1.units[1].unit.name, 's'); + assert.equal(u1.units[0].prefix.name, 'c'); + assert.equal(u1.fixPrefix, false); + + var u2 = u1.to(new Unit(null, 'km/h')); + assert.notStrictEqual(u1, u2); // u2 must be a clone + assert.equal(u2.value, 50); + assert.equal(u2.units[0].unit.name, 'm'); + assert.equal(u2.units[1].unit.name, 'h'); + assert.equal(u2.units[0].prefix.name, 'k'); assert.equal(u2.fixPrefix, true); }); it ('should convert a valueless unit', function () { var u1 = new Unit(null, 'm'); assert.equal(u1.value, null); - assert.equal(u1.unit.name, 'm'); - assert.equal(u1.prefix.name, ''); + assert.equal(u1.units[0].unit.name, 'm'); + assert.equal(u1.units[0].prefix.name, ''); assert.equal(u1.fixPrefix, false); var u2 = u1.to(new Unit(null, 'cm')); assert.notStrictEqual(u1, u2); // u2 must be a clone assert.equal(u2.value, 1); // u2 must have a value - assert.equal(u2.unit.name, 'm'); - assert.equal(u2.prefix.name, 'c'); + assert.equal(u2.units[0].unit.name, 'm'); + assert.equal(u2.units[0].prefix.name, 'c'); + assert.equal(u2.fixPrefix, true); + + var u1 = new Unit(null, 'm/s'); + assert.equal(u1.value, null); + assert.equal(u1.units[0].unit.name, 'm'); + assert.equal(u1.units[1].unit.name, 's'); + assert.equal(u1.units[0].prefix.name, ''); + assert.equal(u1.fixPrefix, false); + + var u2 = u1.to(new Unit(null, 'cm/s')); + assert.notStrictEqual(u1, u2); // u2 must be a clone + assert.equal(u2.value, 1); // u2 must have a value + assert.equal(u2.units[0].unit.name, 'm'); + assert.equal(u2.units[1].unit.name, 's'); + assert.equal(u2.units[0].prefix.name, 'c'); assert.equal(u2.fixPrefix, true); }); it ('should convert a binary prefixes (1)', function () { var u1 = new Unit(1, 'Kib'); assert.equal(u1.value, 1024); - assert.equal(u1.unit.name, 'b'); - assert.equal(u1.prefix.name, 'Ki'); + assert.equal(u1.units[0].unit.name, 'b'); + assert.equal(u1.units[0].prefix.name, 'Ki'); assert.equal(u1.fixPrefix, false); var u2 = u1.to(new Unit(null, 'b')); assert.notStrictEqual(u1, u2); // u2 must be a clone assert.equal(u2.value, 1024); // u2 must have a value - assert.equal(u2.unit.name, 'b'); - assert.equal(u2.prefix.name, ''); + assert.equal(u2.units[0].unit.name, 'b'); + assert.equal(u2.units[0].prefix.name, ''); + assert.equal(u2.fixPrefix, true); + + var u1 = new Unit(1, 'Kib/s'); + assert.equal(u1.value, 1024); + assert.equal(u1.units[0].unit.name, 'b'); + assert.equal(u1.units[1].unit.name, 's'); + assert.equal(u1.units[0].prefix.name, 'Ki'); + assert.equal(u1.fixPrefix, false); + + var u2 = u1.to(new Unit(null, 'b/s')); + assert.notStrictEqual(u1, u2); // u2 must be a clone + assert.equal(u2.value, 1024); // u2 must have a value + assert.equal(u2.units[0].unit.name, 'b'); + assert.equal(u2.units[1].unit.name, 's'); + assert.equal(u2.units[0].prefix.name, ''); assert.equal(u2.fixPrefix, true); }); it ('should convert a binary prefixes (2)', function () { var u1 = new Unit(1, 'kb'); assert.equal(u1.value, 1000); - assert.equal(u1.unit.name, 'b'); - assert.equal(u1.prefix.name, 'k'); + assert.equal(u1.units[0].unit.name, 'b'); + assert.equal(u1.units[0].prefix.name, 'k'); assert.equal(u1.fixPrefix, false); var u2 = u1.to(new Unit(null, 'b')); assert.notStrictEqual(u1, u2); // u2 must be a clone assert.equal(u2.value, 1000); // u2 must have a value - assert.equal(u2.unit.name, 'b'); - assert.equal(u2.prefix.name, ''); + assert.equal(u2.units[0].unit.name, 'b'); + assert.equal(u2.units[0].prefix.name, ''); assert.equal(u2.fixPrefix, true); }); it ('should throw an error when converting to an incompatible unit', function () { var u1 = new Unit(5000, 'cm'); assert.throws(function () {u1.to('kg')}, /Units do not match/); + var u1 = new Unit(5000, 'N s'); + assert.throws(function () {u1.to('kg^5 / s')}, /Units do not match/); }); it ('should throw an error when converting to a unit having a value', function () { @@ -249,6 +344,11 @@ describe('unit', function() { assert.equal(new Unit(5000, 'cm').toString(), '50 m'); assert.equal(new Unit(5, 'kg').toString(), '5 kg'); assert.equal(new Unit(2/3, 'm').toString(), '0.6666666666666666 m'); + assert.equal(new Unit(5, 'N').toString(), '5 N'); + assert.equal(new Unit(5, 'kg^1.0e0 m^1.0e0 s^-2.0e0').toString(), '5 (kg m) / s^2'); + assert.equal(new Unit(5, 's^-2').toString(), '5 s^-2'); + assert.equal(new Unit(5, 'm / s ^ 2').toString(), '5 m / s^2'); + assert.equal(new Unit(null, 'kg m^2 / s^2 mol').toString(), '(kg m^2) / (s^2 mol)'); }); it('should render with the best prefix', function() { @@ -266,6 +366,60 @@ describe('unit', function() { assert.equal(new Unit(1000 ,'m').toString(), '1 km'); }); + + }); + + describe('simplifyUnitListLazy', function() { + it('should not simplify units created with new Unit()', function() { + var unit1 = new Unit(10, "kg m/s^2"); + assert.equal(unit1.units[0].unit.name, "g"); + assert.equal(unit1.units[1].unit.name, "m"); + assert.equal(unit1.units[2].unit.name, "s"); + assert.equal(unit1.toString(), "10 (kg m) / s^2"); + }); + + it('should only simplify units with values', function() { + var unit1 = new Unit(null, "kg m mol / s^2 mol"); + unit1.isUnitListSimplified = false; + unit1.simplifyUnitListLazy(); + assert.equal(unit1.toString(), "(kg m mol) / (s^2 mol)"); + unit1 = math.multiply(unit1, 1); + assert.equal(unit1.toString(), "1 N"); + }); + + it('should simplify units resulting from multiply/divide/power functions only when formatting for output', function() { + var unit1 = new Unit(2, "kg"); + var unit2 = new Unit(5, "m/s^2"); + var unit3 = math.multiply(unit1, unit2); + assert.equal(unit3.units[0].unit.name, "g"); + assert.equal(unit3.units[1].unit.name, "m"); + assert.equal(unit3.units[2].unit.name, "s"); + assert.equal(unit3.toString(), "10 N"); // Triggers simplification + assert.equal(unit3.units[0].unit.name, "N"); + + }); + + it('should simplify units according to chosen unit system', function() { + var unit1 = new Unit(10, "N"); + Unit.setUnitSystem('us'); + unit1.isUnitListSimplified = false; + assert.equal(unit1.toString(), "2.248089430997105 lbf"); + assert.equal(unit1.units[0].unit.name, "lbf"); + + Unit.setUnitSystem('cgs'); + unit1.isUnitListSimplified = false; + assert.equal(unit1.format(2), "1 Mdyn"); + assert.equal(unit1.units[0].unit.name, "dyn"); + }); + + it('should correctly simplify units when unit system is "auto"', function() { + Unit.setUnitSystem('auto'); + var unit1 = new Unit(5, "lbf min / s"); + unit1.isUnitListSimplified = false; + assert.equal(unit1.toString(), "300 lbf"); + }); + + }); describe('valueOf', function() { @@ -274,6 +428,9 @@ describe('unit', function() { assert.strictEqual(new Unit(5000, 'cm').valueOf(), '50 m'); assert.strictEqual(new Unit(5, 'kg').valueOf(), '5 kg'); assert.strictEqual(new Unit(2/3, 'm').valueOf(), '0.6666666666666666 m'); + assert.strictEqual(new Unit(5, 'N').valueOf(), '5 N'); + assert.strictEqual(new Unit(5, 'kg^1.0e0 m^1.0e0 s^-2.0e0').valueOf(), '5 (kg m) / s^2'); + assert.strictEqual(new Unit(5, 's^-2').valueOf(), '5 s^-2'); }); }); @@ -285,6 +442,8 @@ describe('unit', function() { {'mathjs': 'Unit', value: 5, unit: 'cm', fixPrefix: false}); assert.deepEqual(new Unit(5, 'cm').to('mm').toJSON(), {'mathjs': 'Unit', value: 50, unit: 'mm', fixPrefix: true}); + assert.deepEqual(new Unit(5, 'kN').to('kg m s ^ -2').toJSON(), + {'mathjs': 'Unit', value: 5000, unit: '(kg m) / s^2', fixPrefix: true}); }); it('fromJSON', function () { @@ -297,6 +456,11 @@ describe('unit', function() { var u4 = Unit.fromJSON({'mathjs': 'Unit', value: 50, unit: 'mm', fixPrefix: true}); assert.ok(u4 instanceof Unit); assert.deepEqual(u4, u3); + + var u5 = new Unit(5, 'kN').to('kg m/s^2'); + var u6 = Unit.fromJSON({'mathjs': 'Unit', value: 5000, unit: 'kg m s^-2', fixPrefix: true}); + assert.ok(u6 instanceof Unit); + assert.deepEqual(u5, u6); }); }); @@ -312,11 +476,13 @@ describe('unit', function() { it('should format a unit without value', function() { assert.equal(new Unit(null, 'cm').format(), 'cm'); assert.equal(new Unit(null, 'm').format(), 'm'); + assert.equal(new Unit(null, 'kg m/s').format(), '(kg m) / s'); }); it('should format a unit with fixed prefix and without value', function() { assert.equal(new Unit(null, 'km').to('cm').format(), '1e+5 cm'); assert.equal(new Unit(null, 'inch').to('cm').format(), '2.54 cm'); + assert.equal(new Unit(null, 'N/m^2').to('lbf/inch^2').format(5), '1.4504e-4 lbf / inch^2'); }); it('should ignore properties in Object.prototype when finding the best prefix', function() { @@ -336,91 +502,189 @@ describe('unit', function() { unit1 = Unit.parse('5kg'); assert.equal(unit1.value, 5); - assert.equal(unit1.unit.name, 'g'); - assert.equal(unit1.prefix.name, 'k'); + assert.equal(unit1.units[0].unit.name, 'g'); + assert.equal(unit1.units[0].prefix.name, 'k'); unit1 = Unit.parse('5 kg'); assert.equal(unit1.value, 5); - assert.equal(unit1.unit.name, 'g'); - assert.equal(unit1.prefix.name, 'k'); + assert.equal(unit1.units[0].unit.name, 'g'); + assert.equal(unit1.units[0].prefix.name, 'k'); unit1 = Unit.parse(' 5 kg '); assert.equal(unit1.value, 5); - assert.equal(unit1.unit.name, 'g'); - assert.equal(unit1.prefix.name, 'k'); + assert.equal(unit1.units[0].unit.name, 'g'); + assert.equal(unit1.units[0].prefix.name, 'k'); unit1 = Unit.parse('5e-3kg'); assert.equal(unit1.value, 0.005); - assert.equal(unit1.unit.name, 'g'); - assert.equal(unit1.prefix.name, 'k'); + assert.equal(unit1.units[0].unit.name, 'g'); + assert.equal(unit1.units[0].prefix.name, 'k'); unit1 = Unit.parse('5e+3kg'); assert.equal(unit1.value, 5000); - assert.equal(unit1.unit.name, 'g'); - assert.equal(unit1.prefix.name, 'k'); + assert.equal(unit1.units[0].unit.name, 'g'); + assert.equal(unit1.units[0].prefix.name, 'k'); unit1 = Unit.parse('5e3kg'); assert.equal(unit1.value, 5000); - assert.equal(unit1.unit.name, 'g'); - assert.equal(unit1.prefix.name, 'k'); + assert.equal(unit1.units[0].unit.name, 'g'); + assert.equal(unit1.units[0].prefix.name, 'k'); unit1 = Unit.parse('-5kg'); assert.equal(unit1.value, -5); - assert.equal(unit1.unit.name, 'g'); - assert.equal(unit1.prefix.name, 'k'); + assert.equal(unit1.units[0].unit.name, 'g'); + assert.equal(unit1.units[0].prefix.name, 'k'); unit1 = Unit.parse('+5kg'); assert.equal(unit1.value, 5); - assert.equal(unit1.unit.name, 'g'); - assert.equal(unit1.prefix.name, 'k'); + assert.equal(unit1.units[0].unit.name, 'g'); + assert.equal(unit1.units[0].prefix.name, 'k'); unit1 = Unit.parse('.5kg'); assert.equal(unit1.value, .5); - assert.equal(unit1.unit.name, 'g'); - assert.equal(unit1.prefix.name, 'k'); + assert.equal(unit1.units[0].unit.name, 'g'); + assert.equal(unit1.units[0].prefix.name, 'k'); unit1 = Unit.parse('-5mg'); assert.equal(unit1.value, -0.000005); - assert.equal(unit1.unit.name, 'g'); - assert.equal(unit1.prefix.name, 'm'); + assert.equal(unit1.units[0].unit.name, 'g'); + assert.equal(unit1.units[0].prefix.name, 'm'); unit1 = Unit.parse('5.2mg'); approx.equal(unit1.value, 0.0000052); - assert.equal(unit1.unit.name, 'g'); - assert.equal(unit1.prefix.name, 'm'); + assert.equal(unit1.units[0].unit.name, 'g'); + assert.equal(unit1.units[0].prefix.name, 'm'); + + unit1 = Unit.parse('300 kg/minute'); + approx.equal(unit1.value, 5); + assert.equal(unit1.units[0].unit.name, 'g'); + assert.equal(unit1.units[1].unit.name, 'minute'); + assert.equal(unit1.units[0].prefix.name, 'k'); + + unit1 = Unit.parse('981 cm/s^2'); + approx.equal(unit1.value, 9.81); + assert.equal(unit1.units[0].unit.name, 'm'); + assert.equal(unit1.units[1].unit.name, 's'); + assert.equal(unit1.units[1].power, -2); + assert.equal(unit1.units[0].prefix.name, 'c'); + + unit1 = Unit.parse('8.314 kg m^2 / s^2 / K / mol'); + approx.equal(unit1.value, 8.314); + assert.equal(unit1.units[0].unit.name, 'g'); + assert.equal(unit1.units[1].unit.name, 'm'); + assert.equal(unit1.units[2].unit.name, 's'); + assert.equal(unit1.units[3].unit.name, 'K'); + assert.equal(unit1.units[4].unit.name, 'mol'); + assert.equal(unit1.units[0].power, 1); + assert.equal(unit1.units[1].power, 2); + assert.equal(unit1.units[2].power, -2); + assert.equal(unit1.units[3].power, -1); + assert.equal(unit1.units[4].power, -1); + assert.equal(unit1.units[0].prefix.name, 'k'); + + unit1 = Unit.parse('5exabytes'); + approx.equal(unit1.value, 4e19); + assert.equal(unit1.units[0].unit.name, 'bytes'); }); - it('should return null when parsing an invalid unit', function() { - assert.equal(Unit.parse('.meter'), null); - assert.equal(Unit.parse('5e'), null); - assert.equal(Unit.parse('5e. meter'), null); - assert.equal(Unit.parse('5e1.3 meter'), null); - assert.equal(Unit.parse('5'), null); - assert.equal(Unit.parse(''), null); + it('should return null (update: throw exception --ericman314) when parsing an invalid unit', function() { + // I'm worried something else will break if Unit.parse throws an exception instead of returning null???? --ericman314 + assert.throws(function () {Unit.parse('.meter')}, /Could not parse/); + assert.throws(function () {Unit.parse('5e')}, /Unit "e" not found/); + assert.throws(function () {Unit.parse('5e.')}, /Unit "e" not found/); + assert.throws(function () {Unit.parse('5e1.3')}, /Could not parse/); + assert.throws(function () {Unit.parse('5')}, /contains no units/); + assert.throws(function () {Unit.parse('')}, /contains no units/); + assert.throws(function () {Unit.parse('meter.')}, /Could not parse/); + assert.throws(function () {Unit.parse('meter/')}, /Trailing characters/); + assert.throws(function () {Unit.parse('/meter')}, /Could not parse/); + assert.throws(function () {Unit.parse('45 kg 34 m')}, /Could not parse/); +// assert.equal(Unit.parse('.meter'), null); +// assert.equal(Unit.parse('5e'), null); +// assert.equal(Unit.parse('5e. meter'), null); +// assert.equal(Unit.parse('5e1.3 meter'), null); +// assert.equal(Unit.parse('5'), null); +// assert.equal(Unit.parse(''), null); +// assert.equal(Unit.parse('meter.'), null); +// assert.equal(Unit.parse('meter/'), null); +// assert.equal(Unit.parse('/meter'), null); }); it('should return null when parsing an invalid type of argument', function() { - assert.equal(Unit.parse(123), null); + assert.throws(function () {Unit.parse(123)}, /Invalid argument in Unit.parse. Valid types are/); +// assert.equal(Unit.parse(123), null); }); }); + describe('_isDerived', function() { + it('should return the correct value', function () { + assert.equal(Unit.parse('34 kg')._isDerived(), false); + assert.equal(Unit.parse('34 kg/s')._isDerived(), true); + assert.equal(Unit.parse('34 kg^2')._isDerived(), true); + assert.equal(Unit.parse('34 N')._isDerived(), false); + assert.equal(Unit.parse('34 kg m / s^2')._isDerived(), true); + var unit1 = Unit.parse('34 kg m / s^2'); + assert.equal(unit1._isDerived(), true); + unit1.isUnitListSimplified = false; + unit1.simplifyUnitListLazy(); + assert.equal(unit1._isDerived(), false); + }); + }); + + describe('multiply, divide, and pow', function() { + it('should flag the unit as requiring simplification', function() { + var unit1 = new Unit(10, 'kg'); + var unit2 = new Unit(9.81, 'm/s^2'); + assert.equal(unit1.multiply(unit2).isUnitListSimplified, false); + assert.equal(unit1.divide(unit2).isUnitListSimplified, false); + assert.equal(unit1.pow(2).isUnitListSimplified, false); + }); + + it('should retain the units of their operands without simplifying', function() { + var unit1 = new Unit(10, "N/s"); + var unit2 = new Unit(10, "h"); + var unitM = unit1.multiply(unit2); + assert.equal(unitM.units[0].unit.name, 'N'); + assert.equal(unitM.units[1].unit.name, 's'); + assert.equal(unitM.units[2].unit.name, 'h'); + + var unit3 = new Unit(14.7, "lbf"); + var unit4 = new Unit(1, "in in"); + var unitD = unit3.divide(unit4); + assert.equal(unitD.units[0].unit.name, 'lbf'); + assert.equal(unitD.units[1].unit.name, 'in'); + assert.equal(unitD.units[2].unit.name, 'in'); + + var unit5 = new Unit(1, "N h/s"); + var unitP = unit5.pow(-3.5); + assert.equal(unitP.units[0].unit.name, 'N'); + assert.equal(unitP.units[1].unit.name, 'h'); + assert.equal(unitP.units[2].unit.name, 's'); + }); + }); + describe('plurals', function() { it('should support plurals', function () { var unit1 = new Unit(5, 'meters'); assert.equal(unit1.value, 5); - assert.equal(unit1.unit.name, 'meters'); - assert.equal(unit1.prefix.name, ''); + assert.equal(unit1.units[0].unit.name, 'meters'); + assert.equal(unit1.units[0].prefix.name, ''); var unit2 = new Unit(5, 'kilometers'); assert.equal(unit2.value, 5000); - assert.equal(unit2.unit.name, 'meters'); - assert.equal(unit2.prefix.name, 'kilo'); + assert.equal(unit2.units[0].unit.name, 'meters'); + assert.equal(unit2.units[0].prefix.name, 'kilo'); var unit3 = new Unit(5, 'inches'); approx.equal(unit3.value, 0.127); - assert.equal(unit3.unit.name, 'inches'); - assert.equal(unit3.prefix.name, ''); + assert.equal(unit3.units[0].unit.name, 'inches'); + assert.equal(unit3.units[0].prefix.name, ''); + + var unit3 = new Unit(9.81, 'meters/second^2'); + approx.equal(unit3.value, 9.81); + assert.equal(unit3.units[0].unit.name, 'meters'); + assert.equal(unit3.units[0].prefix.name, ''); }); }); @@ -430,15 +694,15 @@ describe('unit', function() { var unit1 = new Unit(5, 'lt'); assert.equal(unit1.value, 5e-3); - assert.equal(unit1.unit.name, 'l'); - assert.equal(unit1.prefix.name, ''); + assert.equal(unit1.units[0].unit.name, 'l'); + assert.equal(unit1.units[0].prefix.name, ''); var unit2 = new Unit(1, 'lb'); assert.equal(unit2.value, 453.59237e-3); - assert.equal(unit2.unit.name, 'lbm'); - assert.equal(unit2.prefix.name, ''); + assert.equal(unit2.units[0].unit.name, 'lbm'); + assert.equal(unit2.units[0].prefix.name, ''); }); }); // TODO: test the value of each of the available units... -}); \ No newline at end of file +}); diff --git a/test/type/unit/function/unit.test.js b/test/type/unit/function/unit.test.js index f358fae59..40bcf4c0c 100644 --- a/test/type/unit/function/unit.test.js +++ b/test/type/unit/function/unit.test.js @@ -35,7 +35,7 @@ describe('unit', function() { }); it('should throw an error if called with a number', function() { - assert.throws(function () {unit(2)}, /SyntaxError: String "2" is no valid unit/); + assert.throws(function () {unit(2)}, /SyntaxError: "2" contains no units/); }); it('should throw an error if called with a complex', function() {