From 9ae8ea36dffd7af63f6f60298353419c91d328ef Mon Sep 17 00:00:00 2001 From: Ubuntu Date: Sat, 25 Jul 2015 03:33:07 +0000 Subject: [PATCH 01/10] Just committing so I can checkout master again to revert a few lines. --- lib/function/arithmetic/multiplyScalar.js | 7 +- lib/type/unit/Unit.js | 302 +++++++++++++++++++--- 2 files changed, 269 insertions(+), 40 deletions(-) diff --git a/lib/function/arithmetic/multiplyScalar.js b/lib/function/arithmetic/multiplyScalar.js index 50ee0b591..46c83e237 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/type/unit/Unit.js b/lib/type/unit/Unit.js index f53074a9d..09d53d4e0 100644 --- a/lib/type/unit/Unit.js +++ b/lib/type/unit/Unit.js @@ -38,12 +38,22 @@ function factory (type, config, load, typed) { if (!res) { throw new SyntaxError('Unknown unit "' + name + '"'); } - this.unit = res.unit; - this.prefix = res.prefix; + this.units = [ + { + unit: res.unit, + prefix: res.prefix, + power: 1.0 + } + ]; } 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.value = (value != undefined) ? this._normalize(value) : null; @@ -85,6 +95,9 @@ function factory (type, config, load, typed) { c = text.charAt(index); } + // Would this work better as a regex? + // For example, /[-+]?[0-9]*\.?[0-9]+([eE][-+]?[0-9]+)?/ + // --ericman314 function parseNumber() { var number = ''; var oldIndex; @@ -244,30 +257,69 @@ function factory (type, config, load, typed) { return clone; }; + /** - * Normalize a value, based on its currently set unit + * Return whether the unit is derived (such as m/s, or cm^2, but not N) + * @return {boolean} True if the unit is derived + */ + Unit.prototype._isDerived = function() { + return this.units.length > 1 || Math.abs(this.units[0].unit.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 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; + } + else { + // 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; + } }; /** - * 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; + }; /** @@ -336,6 +442,82 @@ 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(); + res.units = []; + // For now, just create a default SI representation. Later we can make it prettier. + + console.log("this = "); + console.log(util.inspect(this)); + + var sumDimensions = []; + // Add the dimensions of the this unit + for(var i=0; i Date: Sat, 25 Jul 2015 18:48:04 +0000 Subject: [PATCH 02/10] First attempt at derived units. Updated unit tests and examples. --- examples/basic_usage.js | 1 + examples/units.js | 27 +- lib/function/arithmetic/divideScalar.js | 11 + lib/function/arithmetic/multiplyScalar.js | 6 +- lib/function/arithmetic/pow.js | 7 + lib/type/unit/Unit.js | 643 ++++++++++++++------- test/function/arithmetic/divide.test.js | 17 + test/function/arithmetic/dotDivide.test.js | 7 + test/function/arithmetic/multiply.test.js | 21 +- test/function/arithmetic/pow.test.js | 11 +- test/type/unit/Unit.test.js | 248 ++++++-- 11 files changed, 710 insertions(+), 289 deletions(-) diff --git a/examples/basic_usage.js b/examples/basic_usage.js index e85f275fc..744efd818 100644 --- a/examples/basic_usage.js +++ b/examples/basic_usage.js @@ -34,6 +34,7 @@ console.log(); console.log('mixed use of data types'); print(math.add(4, [5, 6])); // number + Array, [9, 10] print(math.multiply(math.unit('5 mm'), 3)); // Unit * number, 15 mm +print(math.multiply(math.unit('8 ft'), math.unit('20 lbf')).to('ft lbf')); // Unit * number, 15 mm print(math.subtract([2, 3, 4], 5)); // Array - number, [-3, -2, -1] print(math.add(math.matrix([2, 3]), [4, 5])); // Matrix + Array, [6, 8] console.log(); diff --git a/examples/units.js b/examples/units.js index 936dc2875..1870a4384 100644 --- a/examples/units.js +++ b/examples/units.js @@ -32,7 +32,7 @@ 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, 'gram')); // 10 +print(math.number(b, 'cm')); // 10 console.log(); // the expression parser supports units too @@ -44,3 +44,28 @@ 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(); + +// derived units +console.log('multiply, divide, exponentiate units'); +print(math.multiply(a, b)); // 0.045 m^2 +print(math.divide(a, b)); // 4.5 +print(math.pow(a, 3)); // 91125 cm^3 +console.log(); + +console.log('compute molar volume of ideal gas at STP in L/mol:'); +var Rg = math.unit('8.314 N m / mol K'); +var P = math.unit('14.7 lbf/in^2'); +var T = math.unit('25 celsius'); +var v = math.divide(math.multiply(Rg, T), P); +console.log('gas constant (Rg) = '); +print(Rg); +console.log(); +console.log('P = '); +print(P); +console.log(); +console.log('T = '); +print(T); +console.log(); +console.log('v = Rg * T / P ='); +print(math.to(v, 'L/mol')); diff --git a/lib/function/arithmetic/divideScalar.js b/lib/function/arithmetic/divideScalar.js index 85fc95513..1137ec024 100644 --- a/lib/function/arithmetic/divideScalar.js +++ b/lib/function/arithmetic/divideScalar.js @@ -33,7 +33,18 @@ 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); + console.log(xUnit); + 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 46c83e237..fc7c6d41f 100644 --- a/lib/function/arithmetic/multiplyScalar.js +++ b/lib/function/arithmetic/multiplyScalar.js @@ -48,9 +48,9 @@ function factory(type, config, load, typed) { return res; }, - 'Unit, Unit': function (x, y) { - return x.multiply(y); - } + 'Unit, Unit': function (x, y) { + return x.multiply(y); + } }); 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 09d53d4e0..76bc66561 100644 --- a/lib/type/unit/Unit.js +++ b/lib/type/unit/Unit.js @@ -16,9 +16,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)) { @@ -32,27 +33,78 @@ function factory (type, config, load, typed) { throw new TypeError('Second parameter in Unit constructor must be a string'); } + // Matches a unit with an optional exponent, such as: + // "kg" + // "m^2" + // "s ^ -3" + // The unit is contained in the first capturing group + // The exponent (without the "^") is contained in the second capturing group + + if (name != undefined) { - // find the unit and prefix from the string - var res = _findUnit(name); - if (!res) { - throw new SyntaxError('Unknown unit "' + name + '"'); - } - this.units = [ - { - unit: res.unit, - prefix: res.prefix, - power: 1.0 - } - ]; + + this.units = []; + + // Split the string by the '/' character + var parts = name.split('/'); + + var multiplier = 1; + for(var i=0; i0) { + // After the first pass through the loop the units will go in the denominator + multiplier = -1; + } + var unitWithExpPattern = /\s*([a-zA-Z0-9]+)\s*(?:\^\s*([-+]?[0-9]*\.?[0-9]+(?:[eE][-+]?[0-9]+)?))?\s*/g; + var result; + var lastIndex = unitWithExpPattern.lastIndex; + var reachedEndOfString = false; + while ((result = unitWithExpPattern.exec(parts[i])) !== null) { + if(typeof(result[1]) === 'undefined') { + throw new SyntaxError('Error encountered while parsing unit: "' + name + '"'); + } + if(result.index != lastIndex) { + // Oops, there were some extra characters at the beginning of the string. + throw new SyntaxError('Stray characters encountered while parsing unit: "' + name + '"'); + } + lastIndex = unitWithExpPattern.lastIndex; + if(lastIndex === parts[i].length) + reachedEndOfString = true; + + var thisName = result[1]; + var thisPower; + if(typeof(result[2]) === 'undefined') { + thisPower = multiplier; + } + else { + var thisPower = parseFloat(result[2]) * multiplier; + } + + var res = _findUnit(thisName); + if (!res) { + throw new SyntaxError('Unknown unit "' + thisName + '"'); + } + + this.units.push({ + unit: res.unit, + prefix: res.prefix, + power: thisPower + }); + + } + // Ensure the entire string was consumed by the regex + if(!reachedEndOfString) { + throw new SyntaxError('Trailing characters encountered while parsing unit: "' + name + '"'); + } + } + } else { - this.units = [ - { - unit: UNIT_NONE, - prefix: PREFIX_NONE, // link to a list with supported prefixes - power: 0 - } + this.units = [ + { + unit: UNIT_NONE, + prefix: PREFIX_NONE, // link to a list with supported prefixes + power: 0 + } ]; } @@ -168,6 +220,26 @@ function factory (type, config, load, typed) { return number; } + + /** + * Match a regex pattern for units such as kg m / s^2. Only matches the pattern u1[^ex1] [u2[^ex2] ...] [/ u3[^ex3] [u4[^ex4]]]; does not check for the existence or validity of units. + */ + function parseUnit() { + var unitsPattern = /^\s*(?:(?:(?:[a-zA-Z][a-zA-Z0-9]*)\s*(?:\^\s*(?:[-+]?[0-9]*\.?[0-9]+(?:[eE][-+]?[0-9]+)?))?)\s*\/?\s*)*\s*(?:(?:[a-zA-Z][a-zA-Z0-9]*)\s*(?:\^\s*(?:[-+]?[0-9]*\.?[0-9]+(?:[eE][-+]?[0-9]+)?))?)$/; + // This regex is quite ugly...Alternatively, do we just want to return the entire string without testing and let the Unit constructor check for valid format? + var result = unitsPattern.exec(text.substr(index)); + if(result) { + index = text.length; + c = text.charAt(index); + return result[0]; + } + else { + return null; + } + } + + /* + // The old parseUnit function did not accept units like "m / s" --ericman314 function parseUnit() { var unitName = ''; @@ -179,6 +251,7 @@ function factory (type, config, load, typed) { return unitName || null; } + */ /** * Parse a string into a unit. Returns null if the provided string does not @@ -263,7 +336,7 @@ function factory (type, config, load, typed) { * @return {boolean} True if the unit is derived */ Unit.prototype._isDerived = function() { - return this.units.length > 1 || Math.abs(this.units[0].unit.power - 1.0) < 1e-15); + return this.units.length > 1 || Math.abs(this.units[0].power - 1.0) > 1e-15; } /** @@ -273,22 +346,22 @@ function factory (type, config, load, typed) { * @private */ Unit.prototype._normalize = function (value) { - if (this.units.length === 0) { - return value; - } - else if (this._isDerived()) { - // 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; - } - else { - // 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; - } + 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; + } }; /** @@ -299,27 +372,28 @@ function factory (type, config, load, typed) { * @private */ Unit.prototype._denormalize = function (value, prefixValue) { - 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; + } + } - // Is the total zero for each dimension? - for(var key in diffDimensions) { - if(Math.abs(diffDimensions[key]) > 1e-12) { - return false; - } - } - - return true; + return true; }; @@ -448,76 +520,145 @@ function factory (type, config, load, typed) { * @return {Unit} product of this unit and the other unit */ Unit.prototype.multiply = function (other) { - var res = this.clone(); - res.units = []; - // For now, just create a default SI representation. Later we can make it prettier. - - console.log("this = "); - console.log(util.inspect(this)); + var res = this.clone(); + res.units = []; + // For now, just create a default SI representation. Later we can make it prettier. - var sumDimensions = []; - // Add the dimensions of the this unit - for(var i=0; i 0) { + anyPositivePowers = true; + str += " " + this.units[i].prefix.name + this.units[i].unit.name; + if(Math.abs(this.units[i].power - 1.0) > 1e-15) { + str += "^" + this.units[i].power; + } + } + else if (this.units[i].power < 0) { + anyNegativePowers = true; + } + } + if(anyNegativePowers && anyPositivePowers) { + str += " /"; + } + for(var i=0; i 1e-15) { + str += "^" + (-this.units[i].power); + } + } + else { + str += "^" + (this.units[i].power); + } + } + } + return str.substr(1); // Remove first ' ' from beginning of string + }; + /** * Get a string representation of the Unit, with optional formatting options. * @param {Object | number | Function} [options] Formatting options. See @@ -615,28 +803,31 @@ function factory (type, config, load, typed) { * @return {string} */ Unit.prototype.format = function (options) { - var value, - str; - if(this._isDerived()) { - // Multiple units or units with powers are formatted like this: - // 5 kg m^2 / s^3 - - value = this._denormalize(this.value); - } - else { + 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 += this.formatUnits(); + } + 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.unit.name; - } - else { - value = this._denormalize(this.value); - str = (this.value !== null) ? (format(value, options) + ' ') : ''; - str += this.prefix.name + this.unit.name; - } - } + 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; + } + } return str; }; @@ -647,16 +838,20 @@ function factory (type, config, load, typed) { * @private */ 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]; @@ -844,7 +1039,7 @@ function factory (type, config, load, typed) { 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: []}; var UNITS = { // length @@ -1702,35 +1897,35 @@ function factory (type, config, load, typed) { // Add dimensions for all built-in units // This is what allows us to say that, for example, 440 ft * 33 yards = 1 acre for (var key in UNITS) { - var unit = UNITS[key]; + var unit = UNITS[key]; if (unit.base === BASE_UNITS.LENGTH - || unit.base === BASE_UNITS.MASS - || unit.base === BASE_UNITS.TIME - || unit.base === BASE_UNITS.TEMPERATURE - || unit.base === BASE_UNITS.CURRENT - || unit.base === BASE_UNITS.LUMINOUS_INTENSITY - || unit.base === BASE_UNITS.AMOUNT_OF_SUBSTANCE - || unit.base === BASE_UNITS.ANGLE // ANGLE and BIT are really dimensionless but whatever - || unit.base === BASE_UNITS.BIT) { - unit.dimensions = [{base: unit.base, power: 1}]; - } - else if (unit.base === BASE_UNITS.SURFACE) - unit.dimensions = [{base: BASE_UNITS.LENGTH, power: 2}]; - else if (unit.base === BASE_UNITS.VOLUME) - unit.dimensions = [{base: BASE_UNITS.LENGTH, power: 3}]; - else if (unit.base === BASE_UNITS.FORCE) - unit.dimensions = [{base: BASE_UNITS.LENGTH, power: 1}, {base: BASE_UNITS.MASS, power: 1}, {base: BASE_UNITS.TIME, power: -2}]; - else { - // If in the future you add another dimension (SURFACE, FORCE, ANGLE, etc), you'll have to add that above - var mess = "Unrecognized base for unit " + key + ": "; - for(var base in BASE_UNITS) { - if(unit.base === BASE_UNITS[base]) { - mess += base; - } - } - throw mess; + || unit.base === BASE_UNITS.MASS + || unit.base === BASE_UNITS.TIME + || unit.base === BASE_UNITS.TEMPERATURE + || unit.base === BASE_UNITS.CURRENT + || unit.base === BASE_UNITS.LUMINOUS_INTENSITY + || unit.base === BASE_UNITS.AMOUNT_OF_SUBSTANCE + || unit.base === BASE_UNITS.ANGLE // ANGLE and BIT are really dimensionless but whatever + || unit.base === BASE_UNITS.BIT) { + unit.dimensions = [{base: unit.base, power: 1}]; } - } + else if (unit.base === BASE_UNITS.SURFACE) + unit.dimensions = [{base: BASE_UNITS.LENGTH, power: 2}]; + else if (unit.base === BASE_UNITS.VOLUME) + unit.dimensions = [{base: BASE_UNITS.LENGTH, power: 3}]; + else if (unit.base === BASE_UNITS.FORCE) + unit.dimensions = [{base: BASE_UNITS.MASS, power: 1}, {base: BASE_UNITS.LENGTH, power: 1}, {base: BASE_UNITS.TIME, power: -2}]; + else { + // If in the future you add another dimension (SURFACE, FORCE, ANGLE, etc), you'll have to add that above + var mess = "Unrecognized base for unit " + key + ": "; + for(var base in BASE_UNITS) { + if(unit.base === BASE_UNITS[base]) { + mess += base; + } + } + throw mess; + } + } for (var name in PLURALS) { /* istanbul ignore next (we cannot really test next statement) */ diff --git a/test/function/arithmetic/divide.test.js b/test/function/arithmetic/divide.test.js index 2e69636b2..68d5a3d0c 100644 --- a/test/function/arithmetic/divide.test.js +++ b/test/function/arithmetic/divide.test.js @@ -123,6 +123,20 @@ 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 s / kg m'); + }); + + it('should divide two units', function() { + assert.equal(divide(math.unit('75 mi/h'), math.unit('40 mi/gal')).to('gal/min').toString(), '0.03125 gal / min'); + }); + + it('should divide valueless units', function() { + assert.equal(divide(math.unit('gal'), math.unit('L')).format(4), '3.785'); + 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'); + }); + // 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 +174,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 +183,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 5f7df8fca..ea35ccc32 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')).format(5), '0.74322 m^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(), '0.002 m^3 / 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), '0.37161 m^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(), '0.002 m^3 / s'); + assert.equal(multiply(unit('m/s'), unit('h/m')).toString(), '3600'); + }); + // 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 b309709dd..6c6cc59af 100644 --- a/test/function/arithmetic/pow.test.js +++ b/test/function/arithmetic/pow.test.js @@ -153,8 +153,15 @@ 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 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/type/unit/Unit.test.js b/test/type/unit/Unit.test.js index 54a9f0afc..ad9b0f6dc 100644 --- a/test/type/unit/Unit.test.js +++ b/test/type/unit/Unit.test.js @@ -10,27 +10,32 @@ 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, "m/s^2"); + assert.equal(unit1.value, 9.81); + assert.equal(unit1.units[0].unit.name, 'm'); + assert.equal(unit1.units[1].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() { @@ -99,6 +104,7 @@ 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); }); }); @@ -109,6 +115,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(001, 'kg m / s')), false); }); }); @@ -131,6 +140,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 +155,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 +175,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 +331,9 @@ 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'); }); it('should render with the best prefix', function() { @@ -274,6 +359,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 +373,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 +387,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 +407,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,58 +433,85 @@ 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/min'); + approx.equal(unit1.value, 5); + assert.equal(unit1.units[0].unit.name, 'g'); + assert.equal(unit1.units[1].unit.name, 'min'); + 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'); }); it('should return null when parsing an invalid unit', function() { @@ -397,6 +521,9 @@ describe('unit', function() { 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() { @@ -409,18 +536,23 @@ describe('unit', 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 +562,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 +}); From f689a00edb51a2007418d8f82dfa60ac424cec7d Mon Sep 17 00:00:00 2001 From: Eric Date: Wed, 29 Jul 2015 03:23:51 +0000 Subject: [PATCH 03/10] Made a few notes --- lib/type/unit/Unit.js | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/lib/type/unit/Unit.js b/lib/type/unit/Unit.js index 76bc66561..9f1813a84 100644 --- a/lib/type/unit/Unit.js +++ b/lib/type/unit/Unit.js @@ -665,6 +665,13 @@ function factory (type, config, load, typed) { * @returns {Unit} unit having fixed, specified unit */ Unit.prototype.to = function (valuelessUnit) { + // TODO: Doing math.eval('4 m/s to mi/h') fails with the error, "Cannot convert to a unit with a value." + // This is because "mi/h" is evaluated as "0.44704 m / s", which has a value. + // Even if values were allowed, it still wouldn't work because the original + // units "mi/h" have been discarded, so there's no way to know what to convert it to. + // However, I was surprised to discover that math.eval('4 m/s to "mi/h"') does in fact work, + // but requiring the quotes is rather bad form. --ericman314 + var other; var value = this.value == null ? this._normalize(1) : this.value; if (typeof valuelessUnit === 'string') { From a4974134a43b474862011e0c5b3dac9966f571a9 Mon Sep 17 00:00:00 2001 From: Eric Date: Thu, 30 Jul 2015 03:03:39 +0000 Subject: [PATCH 04/10] Halfway done working on the dimensions array approach for units, but running into trouble with formatting. May have to change many things to get it to work. --- lib/type/unit/Unit.js | 401 +++++++++++++++++++++++++++++++++--------- 1 file changed, 320 insertions(+), 81 deletions(-) diff --git a/lib/type/unit/Unit.js b/lib/type/unit/Unit.js index 9f1813a84..bb8997a87 100644 --- a/lib/type/unit/Unit.js +++ b/lib/type/unit/Unit.js @@ -43,6 +43,35 @@ function factory (type, config, load, typed) { if (name != undefined) { + // Is name a single unit or a combination? + + var singleUnitPattern = /^\s*([a-zA-Z][a-zA-Z0-9]*)\s*$/; + if(singleUnitPattern.test(name)) { + console.log(name + ' matches singleUnitPattern'); + var res = _findUnit(name); + if (!res) { + throw new SyntaxError('Unknown unit "' + thisName + '"'); + } +/* this.units = [{ + unit: res.unit, + prefix: res.prefix, + power: 1.0 + }];*/ + // Clone the built-in unit's dimensions array + this.dimensions = res.unit.dimensions.slice(0); + } + else { + // Not a single unit, so send it off to the parser + var u = math.eval(name); + console.log(name); + console.log(u); + this.units = u.units; + this.dimensions = u.dimensions; + } + + + /* + this.units = []; // Split the string by the '/' character @@ -54,8 +83,10 @@ function factory (type, config, load, typed) { // After the first pass through the loop the units will go in the denominator multiplier = -1; } - var unitWithExpPattern = /\s*([a-zA-Z0-9]+)\s*(?:\^\s*([-+]?[0-9]*\.?[0-9]+(?:[eE][-+]?[0-9]+)?))?\s*/g; - var result; + */ + //var unitWithExpPattern = /\s*([a-zA-Z0-9]+)\s*(?:\^\s*([-+]?[0-9]*\.?[0-9]+(?:[eE][-+]?[0-9]+)?))?\s*/g; + /* + var result; var lastIndex = unitWithExpPattern.lastIndex; var reachedEndOfString = false; while ((result = unitWithExpPattern.exec(parts[i])) !== null) { @@ -96,6 +127,7 @@ function factory (type, config, load, typed) { throw new SyntaxError('Trailing characters encountered while parsing unit: "' + name + '"'); } } + */ } else { @@ -106,12 +138,17 @@ function factory (type, config, load, typed) { power: 0 } ]; + this.dimensions = [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 + + console.log(this); + + console.log("Finished constructing " + this.toString()); } /** @@ -336,7 +373,17 @@ function factory (type, config, load, typed) { * @return {boolean} True if the unit is derived */ Unit.prototype._isDerived = function() { + // TODO: Fix this for non-base units expressible by a single unit, such as N + var nNonZero = 0; + for(var i=0; i<7; i++) { + if (Math.abs(this.dimensions[i]) > 1e-12) { + nNonZero++; + } + } + return nNonZero > 1; + /* return this.units.length > 1 || Math.abs(this.units[0].power - 1.0) > 1e-15; + */ } /** @@ -372,6 +419,9 @@ function factory (type, config, load, typed) { * @private */ Unit.prototype._denormalize = function (value, prefixValue) { + + return value == null ? 1.0 : value; + if (this.units.length === 0) { return value; } @@ -442,12 +492,23 @@ function factory (type, config, load, typed) { * @param {BASE_UNITS | undefined} base */ Unit.prototype.hasBase = function (base) { + + // All dimensions must be the same + for(var i=0; i<7; i++) { + if (Math.abs(this.dimensions[i] - base.dimensions[i]) > 1e-12) { + return false; + } + } + return true; + + /* if(this._isDerived()) { return false; } else { return (this.units[0].unit.base === base); } + */ }; /** @@ -457,6 +518,15 @@ function factory (type, config, load, typed) { * @return {boolean} true if equal base */ Unit.prototype.equalBase = function (other) { + // All dimensions must be the same + for(var i=0; i<7; i++) { + if (Math.abs(this.dimensions[i] - other.dimensions[i]) > 1e-12) { + return false; + } + } + return true; + + /* // We might have multiple units, each with multiple dimensions, each with non-unity powers // For example, newton^2 (although I have no idea what that could possibly represent physically) @@ -502,7 +572,7 @@ function factory (type, config, load, typed) { } return true; - + */ }; /** @@ -520,6 +590,21 @@ function factory (type, config, load, typed) { * @return {Unit} product of this unit and the other unit */ Unit.prototype.multiply = function (other) { + + var res = this.clone(); + + for(var i=0; i<7; i++) { + res.dimensions[i] = this.dimensions[i] + other.dimensions[i]; + } + var valThis = this.value == null ? 1 : this.value; + var valOther = other.value == null ? 1 : other.value; + res.value = valThis * valOther; + + return res; + + /* + + var res = this.clone(); res.units = []; // For now, just create a default SI representation. Later we can make it prettier. @@ -574,8 +659,8 @@ function factory (type, config, load, typed) { var valThis = this.value == null ? this._normalize(1) : this.value; var valOther = other.value == null ? other._normalize(1) : other.value; res.value = res._normalize(valThis * valOther); + */ - return res; } /** @@ -584,6 +669,18 @@ function factory (type, config, load, typed) { * @return {Unit} result of dividing this unit by the other unit */ Unit.prototype.divide = function (other) { + var res = this.clone(); + + for(var i=0; i<7; i++) { + res.dimensions[i] = this.dimensions[i] - other.dimensions[i]; + } + var valThis = this.value == null ? 1 : this.value; + var valOther = other.value == null ? 1 : other.value; + res.value = valThis / valOther; + + return res; + + /* var res = this.clone(); res.units = []; // For now, just create a default SI representation. Later we can make it prettier. @@ -640,6 +737,8 @@ function factory (type, config, load, typed) { res.value = res._normalize(valThis / valOther); return res; + */ + }; /** @@ -648,6 +747,18 @@ function factory (type, config, load, typed) { * @returns {Unit} The result: this^p */ Unit.prototype.pow = function (p) { + var res = this.clone(); + + for(var i=0; i<7; i++) { + res.dimensions[i] = this.dimensions[i] * p; + } + + if(res.value != null) { + res.value = Math.pow(res.value, p); + } + + return res; + /* var ret = this.clone(); for(var i=0; i 0) { - anyPositivePowers = true; - str += " " + this.units[i].prefix.name + this.units[i].unit.name; - if(Math.abs(this.units[i].power - 1.0) > 1e-15) { - str += "^" + this.units[i].power; - } - } - else if (this.units[i].power < 0) { - anyNegativePowers = true; - } - } - if(anyNegativePowers && anyPositivePowers) { - str += " /"; - } - for(var i=0; i 1e-15) { - str += "^" + (-this.units[i].power); - } - } - else { - str += "^" + (this.units[i].power); - } - } - } - return str.substr(1); // Remove first ' ' from beginning of string + + // Search for a matching base + var matchingBase; + for(var key in UNIT_SYSTEMS.current) { + if(this.hasBase(BASE_UNITS[key])) { + matchingBase = BASE_UNITS[key]; + break; + } + } + + var matchingUnit; + if(matchingBase) { + // Does the unit system have a matching unit? + if(UNIT_SYSTEMS.current.hasOwnProperty(matchingBase)) { + matchingUnit = UNIT_SYSTEMS.current[matchingBase] + } + } + + var value; + var str; + if(matchingUnit) { + return matchingUnit; + } + else { + // Multiple units or units with powers are formatted like this: + // 5 kg m^2 / s^3 + + var str = ""; + var anyNegativePowers = false; + var anyPositivePowers = false; + for(var i=0; i<7; i++) { + var baseDim = BASE_DIMENSIONS[i]; + if(this.dimensions[i] > 1e-12) { + anyPositivePowers = true; + console.log(baseDim); + console.log(UNIT_SYSTEMS.current[baseDim]); + str += " " + UNIT_SYSTEMS.current[baseDim].unit.name; + if(Math.abs(this.dimensions[i] - 1.0) > 1e-15) { + str += "^" + this.dimensions[i]; + } + } + else if (this.dimensions[i] < -1e-12) { + anyNegativePowers = true; + } + } + if(anyNegativePowers && anyPositivePowers) { + str += " /"; + } + for(var i=0; i<7; i++) { + var baseDim = BASE_DIMENSIONS[i]; + if(this.dimensions[i] < -1e-12) { + str += " " + UNIT_SYSTEMS.current[baseDim].unit.name; + if(anyPositivePowers) { + if(Math.abs(this.dimensions[i] + 1.0) > 1e-15) { + str += "^" + (-this.dimensions[i]); + } + } + else { + str += "^" + (this.dimensions[i]); + } + } + } + return str.substr(1); // Remove first ' ' from beginning of string + } }; /** @@ -810,6 +951,54 @@ function factory (type, config, load, typed) { * @return {string} */ Unit.prototype.format = function (options) { + + // Search for a matching base + var matchingBase; + for(var key in UNIT_SYSTEMS.current) { + if(this.hasBase(BASE_UNITS[key])) { + matchingBase = BASE_UNITS[key]; + break; + } + } + + var matchingUnit; + if(matchingBase) { + // Does the unit system have a matching unit? + if(UNIT_SYSTEMS.current.hasOwnProperty(matchingBase)) { + matchingUnit = UNIT_SYSTEMS.current[matchingBase] + } + } + + var value; + var str; + if(matchingUnit) { + // The current unit system has a matching unit + 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 { + // There is no matching unit in the unit system, so format a group of base units + 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 += this.formatUnits(); + + } + + + /* var value, str; if(this._isDerived()) { @@ -835,6 +1024,7 @@ function factory (type, config, load, typed) { str += this.units[0].prefix.name + this.units[0].unit.name; } } + */ return str; }; @@ -1025,23 +1215,63 @@ function factory (type, config, load, typed) { var PREFIX_NONE = {name: '', value: 1, scientific: true}; - // Style choice.... want to make "defaultUnit" a property of each unit instead? --ericman314 + /* 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 + * + * 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]. The unit "1 cal / (gm °C)" can be written in terms of the 7 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]. + * + */ + + var BASE_DIMENSIONS = ["LENGTH", "MASS", "TIME", "CURRENT", "TEMPERATURE", "LUMINOUS_INTENSITY", "AMOUNT_OF_SUBSTANCE"]; + var BASE_UNITS = { NONE: {}, - LENGTH: {defaultUnit: "m"}, // meter - MASS: {defaultUnit: "g", defaultPrefix: "k"}, // kilogram - TIME: {defaultUnit: "s"}, // second - CURRENT: {defaultUnit: "A"}, // ampere - TEMPERATURE: {defaultUnit: "K"}, // kelvin - LUMINOUS_INTENSITY: {defaultUnit: "cd"}, // candela - AMOUNT_OF_SUBSTANCE: {defaultUnit: "mol"}, // mole + LENGTH: { + dimensions: [1, 0, 0, 0, 0, 0, 0] + }, + MASS: { + dimensions: [0, 1, 0, 0, 0, 0, 0] + }, + TIME: { + dimensions: [0, 0, 1, 0, 0, 0, 0] + }, + CURRENT: { + dimensions: [0, 0, 0, 1, 0, 0, 0] + }, + TEMPERATURE: { + dimensions: [0, 0, 0, 0, 1, 0, 0] + }, + LUMINOUS_INTENSITY: { + dimensions: [0, 0, 0, 0, 0, 1, 0] + }, + AMOUNT_OF_SUBSTANCE: { + dimensions: [0, 0, 0, 0, 0, 0, 1] + }, - FORCE: {}, // Newton - SURFACE: {}, // m2 - VOLUME: {}, // m3 - ANGLE: {defaultUnit: "rad"}, // rad - BIT: {defaultUnit: "bit"} // bit (digital) + FORCE: { + dimensions: [1, 1, -2, 0, 0, 0, 0] + }, + SURFACE: { + dimensions: [2, 0, 0, 0, 0, 0, 0] + }, + VOLUME: { + dimensions: [3, 0, 0, 0, 0, 0, 0] + }, + ANGLE: { + dimensions: [0, 0, 0, 0, 0, 0, 0] + }, + BIT: { + dimensions: [0, 0, 0, 0, 0, 0, 0] + } }; var BASE_UNIT_NONE = {}; @@ -1901,37 +2131,45 @@ function factory (type, config, load, typed) { moles: 'mole' }; - // Add dimensions for all built-in units - // This is what allows us to say that, for example, 440 ft * 33 yards = 1 acre + /** + * 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). + * Default unit system: If I figure out how (or even if it's possible) to make a "static" property of the Unit "class" (sorry, coming from C++/C#) then the default unit system can be updated on the fly anytime a unit is constructed. The unit string is parsed and the corresponding units in the default unit system are updated, so that answers are given in the same units the user supplies. + */ + var UNIT_SYSTEMS = { + def: { + LENGTH: {unit: UNITS.m, prefix: ''}, + MASS: {unit: UNITS.g, prefix: 'k'}, + TIME: {unit: UNITS.s, prefix: ''}, + CURRENT: {unit: UNITS.A, prefix: ''}, + TEMPERATURE: {unit: UNITS.K, prefix: ''}, + LUMINOUS_INTENSITY: {unit: UNITS.cd, prefix: ''}, + AMOUNT_OF_SUBSTANCE: {unit: UNITS.mol, prefix: ''}, + + FORCE: {unit: UNITS.N, prefix: ''}, + ANGLE: {unit: UNITS.rad, prefix: ''}, + BIT: {unit: UNITS.bit, prefix: ''}, + } + }; + + // Clone "default" to create the other unit systems + UNIT_SYSTEMS.si = JSON.parse(JSON.stringify(UNIT_SYSTEMS.def)); + + UNIT_SYSTEMS.cgs = JSON.parse(JSON.stringify(UNIT_SYSTEMS.def)); + UNIT_SYSTEMS.cgs.LENGTH = {unit: UNITS.m, prefix: 'c'}; + UNIT_SYSTEMS.cgs.MASS = {unit: UNITS.g, prefix: ''}; + + // Add additional unit systems here. + + + + // Set the current unit system + UNIT_SYSTEMS.current = UNIT_SYSTEMS.def; + + // Add dimensions to each built-in unit for (var key in UNITS) { var unit = UNITS[key]; - if (unit.base === BASE_UNITS.LENGTH - || unit.base === BASE_UNITS.MASS - || unit.base === BASE_UNITS.TIME - || unit.base === BASE_UNITS.TEMPERATURE - || unit.base === BASE_UNITS.CURRENT - || unit.base === BASE_UNITS.LUMINOUS_INTENSITY - || unit.base === BASE_UNITS.AMOUNT_OF_SUBSTANCE - || unit.base === BASE_UNITS.ANGLE // ANGLE and BIT are really dimensionless but whatever - || unit.base === BASE_UNITS.BIT) { - unit.dimensions = [{base: unit.base, power: 1}]; - } - else if (unit.base === BASE_UNITS.SURFACE) - unit.dimensions = [{base: BASE_UNITS.LENGTH, power: 2}]; - else if (unit.base === BASE_UNITS.VOLUME) - unit.dimensions = [{base: BASE_UNITS.LENGTH, power: 3}]; - else if (unit.base === BASE_UNITS.FORCE) - unit.dimensions = [{base: BASE_UNITS.MASS, power: 1}, {base: BASE_UNITS.LENGTH, power: 1}, {base: BASE_UNITS.TIME, power: -2}]; - else { - // If in the future you add another dimension (SURFACE, FORCE, ANGLE, etc), you'll have to add that above - var mess = "Unrecognized base for unit " + key + ": "; - for(var base in BASE_UNITS) { - if(unit.base === BASE_UNITS[base]) { - mess += base; - } - } - throw mess; - } + unit.dimensions = unit.base.dimensions; } for (var name in PLURALS) { @@ -1954,6 +2192,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; } From 4c1f160e101332a623da127ab2fc91d3f3f66301 Mon Sep 17 00:00:00 2001 From: Eric Date: Thu, 30 Jul 2015 05:38:18 +0000 Subject: [PATCH 05/10] Mostly done but now everything's broken. --- lib/type/unit/Unit.js | 216 +++++++++++++++++++++++++++--------------- 1 file changed, 138 insertions(+), 78 deletions(-) diff --git a/lib/type/unit/Unit.js b/lib/type/unit/Unit.js index bb8997a87..acea252d8 100644 --- a/lib/type/unit/Unit.js +++ b/lib/type/unit/Unit.js @@ -22,6 +22,7 @@ function factory (type, config, load, typed) { * @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) { + console.log("Entering Unit("+(value==null?"null":value).toString()+", '"+name+"')"); if (!(this instanceof Unit)) { throw new Error('Constructor must be called with the new operator'); } @@ -45,27 +46,27 @@ function factory (type, config, load, typed) { // Is name a single unit or a combination? - var singleUnitPattern = /^\s*([a-zA-Z][a-zA-Z0-9]*)\s*$/; + var singleUnitPattern = /^([a-zA-Z][a-zA-Z0-9]*)$/; if(singleUnitPattern.test(name)) { console.log(name + ' matches singleUnitPattern'); var res = _findUnit(name); if (!res) { - throw new SyntaxError('Unknown unit "' + thisName + '"'); + throw new SyntaxError('Unknown unit "' + name + '"'); } -/* this.units = [{ + this.units = [{ unit: res.unit, prefix: res.prefix, power: 1.0 - }];*/ + }]; + this.isUnitListValid = true; // Clone the built-in unit's dimensions array this.dimensions = res.unit.dimensions.slice(0); } else { // Not a single unit, so send it off to the parser var u = math.eval(name); - console.log(name); - console.log(u); this.units = u.units; + this.isUnitListValid = true; this.dimensions = u.dimensions; } @@ -138,6 +139,7 @@ function factory (type, config, load, typed) { power: 0 } ]; + this.isUnitListValid = true; this.dimensions = [0, 0, 0, 0, 0, 0, 0]; } @@ -146,9 +148,9 @@ function factory (type, config, load, typed) { // best prefix but leave it as initially provided. // fixPrefix is set true by the method Unit.to - console.log(this); - console.log("Finished constructing " + this.toString()); + console.log("The finished unit is: "); + console.log(this); } /** @@ -310,34 +312,37 @@ function factory (type, config, load, typed) { var value = parseNumber(); var name; if (value) { - name = parseUnit(); - - next(); +// name = parseUnit(); + name = str.substr(index); // Everything after the number is potentially the units + /*next(); skipWhitespace(); if (c) { // garbage at the end. not good. + console.log('garbage'); return null; - } - + }*/ if (value && name) { try { // constructor will throw an error when unit is not found return new Unit(Number(value), name); } catch (err) { + console.log(err); } } } else { - name = parseUnit(); - + name = str.substr(index); // Everything after the value is potentially the units + //name = parseUnit(); +/* next(); skipWhitespace(); if (c) { // garbage at the end. not good. + console.log('more garbage'); return null; } - +*/ if (name) { try { // constructor will throw an error when unit is not found @@ -347,7 +352,7 @@ function factory (type, config, load, typed) { } } } - + console.log('oh noes'); return null; }; @@ -364,6 +369,9 @@ function factory (type, config, load, typed) { } } + clone.dimensions = this.dimensions.slice(0); + clone.units = this.units.slice(0); + return clone; }; @@ -393,6 +401,7 @@ function factory (type, config, load, typed) { * @private */ Unit.prototype._normalize = function (value) { + this.generateUnitListLazy(); if (this.units.length === 0) { return value; } @@ -419,9 +428,7 @@ function factory (type, config, load, typed) { * @private */ Unit.prototype._denormalize = function (value, prefixValue) { - - return value == null ? 1.0 : value; - + this.generateUnitListLazy(); if (this.units.length === 0) { return value; } @@ -591,15 +598,21 @@ function factory (type, config, load, typed) { */ Unit.prototype.multiply = function (other) { + console.log(this.dimensions); var res = this.clone(); + console.log(this.dimensions); + console.log(other.dimensions); for(var i=0; i<7; i++) { res.dimensions[i] = this.dimensions[i] + other.dimensions[i]; } - var valThis = this.value == null ? 1 : this.value; - var valOther = other.value == null ? 1 : other.value; + var valThis = this.value == null ? this._normalize(1) : this.value; + var valOther = other.value == null ? other._normalize(1) : other.value; res.value = valThis * valOther; - + + // Trigger lazy evaluation of the unit list + res.isUnitListValid = false; + console.log(this.toString() + " times " + other.toString() + " equals " + res.toString()); return res; /* @@ -674,10 +687,13 @@ function factory (type, config, load, typed) { for(var i=0; i<7; i++) { res.dimensions[i] = this.dimensions[i] - other.dimensions[i]; } - var valThis = this.value == null ? 1 : this.value; - var valOther = other.value == null ? 1 : other.value; + var valThis = this.value == null ? this._normalize(1) : this.value; + var valOther = other.value == null ? other._normalize(1) : other.value; res.value = valThis / valOther; + // Trigger lazy evaluation of the unit list + res.isUnitListValid = false; + console.log(this.toString() + " divide " + other.toString() + " equals " + res.toString()); return res; /* @@ -756,7 +772,12 @@ function factory (type, config, load, typed) { if(res.value != null) { res.value = Math.pow(res.value, p); } + else { + res.value = Math.pow(this._normalize(1), p); + } + // Trigger lazy evaluation of the unit list + res.isUnitListValid = false; return res; /* var ret = this.clone(); @@ -871,17 +892,30 @@ function factory (type, config, load, typed) { */ Unit.prototype.valueOf = Unit.prototype.toString; + + /** - * Get a string representation of the units of this Unit, without the value. - * @return {string} + * Generate a list of units for this unit according to the dimensions array and the current unit system. After the call, this Unit will contain a list of the "best" units for formatting. + * Intended to be used with lazy evaluation. You must set isUnitListValid = false before the call! After the call, isUnitListValid will be set to true. */ - Unit.prototype.formatUnits = function () { + Unit.prototype.generateUnitListLazy = function() { + + if (this.isUnitListValid) { +// console.log('isUnitListValid == true, so returning early from function'); + return; + } + else { + console.log('Generating unit list!'); + } + + // Clear the current units list + this.units = []; // Search for a matching base var matchingBase; for(var key in UNIT_SYSTEMS.current) { if(this.hasBase(BASE_UNITS[key])) { - matchingBase = BASE_UNITS[key]; + matchingBase = key; break; } } @@ -897,49 +931,75 @@ function factory (type, config, load, typed) { var value; var str; if(matchingUnit) { - return matchingUnit; + console.log('Found matching unit in generateUnitListLazy: ' + matchingUnit.unit.name); + this.units.push({ + unit: matchingUnit.unit, + prefix: matchingUnit.prefix, + power: 1.0, + }); + this.isUnitListValid = true; + return; } else { // Multiple units or units with powers are formatted like this: // 5 kg m^2 / s^3 - - var str = ""; - var anyNegativePowers = false; - var anyPositivePowers = false; + // Build an representation from the base units of the current unit system for(var i=0; i<7; i++) { var baseDim = BASE_DIMENSIONS[i]; - if(this.dimensions[i] > 1e-12) { - anyPositivePowers = true; - console.log(baseDim); - console.log(UNIT_SYSTEMS.current[baseDim]); - str += " " + UNIT_SYSTEMS.current[baseDim].unit.name; - if(Math.abs(this.dimensions[i] - 1.0) > 1e-15) { - str += "^" + this.dimensions[i]; - } - } - else if (this.dimensions[i] < -1e-12) { - anyNegativePowers = true; + if(Math.abs(this.dimensions[i]) > 1e-12) { + this.units.push({ + unit: UNIT_SYSTEMS.current[baseDim].unit, + prefix: UNIT_SYSTEMS.current[baseDim].prefix, + power: this.dimensions[i] + }); } } - if(anyNegativePowers && anyPositivePowers) { - str += " /"; - } - for(var i=0; i<7; i++) { - var baseDim = BASE_DIMENSIONS[i]; - if(this.dimensions[i] < -1e-12) { - str += " " + UNIT_SYSTEMS.current[baseDim].unit.name; - if(anyPositivePowers) { - if(Math.abs(this.dimensions[i] + 1.0) > 1e-15) { - str += "^" + (-this.dimensions[i]); - } - } - else { - str += "^" + (this.dimensions[i]); - } - } - } - return str.substr(1); // Remove first ' ' from beginning of string + this.isUnitListValid = 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.generateUnitListLazy(); + + var str = ""; + var anyNegativePowers = false; + var anyPositivePowers = false; + for(var i=0; i 0) { + anyPositivePowers = true; + str += " " + this.units[i].prefix.name + this.units[i].unit.name; + if(Math.abs(this.units[i].power - 1.0) > 1e-15) { + str += "^" + this.units[i].power; + } + } + else if (this.units[i].power < 0) { + anyNegativePowers = true; + } + } + if(anyNegativePowers && anyPositivePowers) { + str += " /"; + } + for(var i=0; i 1e-15) { + str += "^" + (-this.units[i].power); + } + } + else { + str += "^" + (this.units[i].power); + } + } + } + return str.substr(1); // Remove first ' ' from beginning of string }; /** @@ -1230,15 +1290,15 @@ function factory (type, config, load, typed) { * */ - var BASE_DIMENSIONS = ["LENGTH", "MASS", "TIME", "CURRENT", "TEMPERATURE", "LUMINOUS_INTENSITY", "AMOUNT_OF_SUBSTANCE"]; + var BASE_DIMENSIONS = ["MASS", "LENGTH", "TIME", "CURRENT", "TEMPERATURE", "LUMINOUS_INTENSITY", "AMOUNT_OF_SUBSTANCE"]; var BASE_UNITS = { NONE: {}, - LENGTH: { + MASS: { dimensions: [1, 0, 0, 0, 0, 0, 0] }, - MASS: { + LENGTH: { dimensions: [0, 1, 0, 0, 0, 0, 0] }, TIME: { @@ -2138,17 +2198,17 @@ function factory (type, config, load, typed) { */ var UNIT_SYSTEMS = { def: { - LENGTH: {unit: UNITS.m, prefix: ''}, - MASS: {unit: UNITS.g, prefix: 'k'}, - TIME: {unit: UNITS.s, prefix: ''}, - CURRENT: {unit: UNITS.A, prefix: ''}, - TEMPERATURE: {unit: UNITS.K, prefix: ''}, - LUMINOUS_INTENSITY: {unit: UNITS.cd, prefix: ''}, - AMOUNT_OF_SUBSTANCE: {unit: UNITS.mol, prefix: ''}, + 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['']}, - FORCE: {unit: UNITS.N, prefix: ''}, - ANGLE: {unit: UNITS.rad, prefix: ''}, - BIT: {unit: UNITS.bit, prefix: ''}, + FORCE: {unit: UNITS.N, prefix: PREFIXES.SHORT['']}, + ANGLE: {unit: UNITS.rad, prefix: PREFIXES.SHORT['']}, + BIT: {unit: UNITS.bit, prefix: PREFIXES.SHORT['']}, } }; @@ -2156,8 +2216,8 @@ function factory (type, config, load, typed) { UNIT_SYSTEMS.si = JSON.parse(JSON.stringify(UNIT_SYSTEMS.def)); UNIT_SYSTEMS.cgs = JSON.parse(JSON.stringify(UNIT_SYSTEMS.def)); - UNIT_SYSTEMS.cgs.LENGTH = {unit: UNITS.m, prefix: 'c'}; - UNIT_SYSTEMS.cgs.MASS = {unit: UNITS.g, prefix: ''}; + UNIT_SYSTEMS.cgs.LENGTH = {unit: UNITS.m, prefix: PREFIXES.SHORT['c']}; + UNIT_SYSTEMS.cgs.MASS = {unit: UNITS.g, prefix: PREFIXES.SHORT['']}; // Add additional unit systems here. From 7ce39f10842fea43f367224b81b0763002b7876b Mon Sep 17 00:00:00 2001 From: Eric Date: Sat, 1 Aug 2015 18:35:46 +0000 Subject: [PATCH 06/10] Finished upgrades to unit type. Adjusted several unit tests. --- lib/type/unit/Unit.js | 512 ++++++++++++++++------ test/function/arithmetic/divide.test.js | 11 +- test/function/arithmetic/multiply.test.js | 10 +- test/function/units/to.test.js | 4 +- test/type/unit/Unit.test.js | 45 +- test/type/unit/function/unit.test.js | 2 +- 6 files changed, 431 insertions(+), 153 deletions(-) diff --git a/lib/type/unit/Unit.js b/lib/type/unit/Unit.js index acea252d8..6b015568b 100644 --- a/lib/type/unit/Unit.js +++ b/lib/type/unit/Unit.js @@ -3,7 +3,12 @@ var format = require('../../utils/number').format; var endsWith = require('../../utils/string').endsWith; + function factory (type, config, load, typed) { + + //console.log('Loading parse'); + //var parser = load(require('../../expression/function/parser'))(); + /** * @constructor Unit * @@ -44,6 +49,12 @@ function factory (type, config, load, typed) { if (name != undefined) { + var u = Unit.parse(name); + + this.units = u.units; + this.dimensions = u.dimensions; + this.isUnitListSimplified = true; +/* // Is name a single unit or a combination? var singleUnitPattern = /^([a-zA-Z][a-zA-Z0-9]*)$/; @@ -58,15 +69,18 @@ function factory (type, config, load, typed) { prefix: res.prefix, power: 1.0 }]; - this.isUnitListValid = true; + this.isUnitListSimplified = true; // Clone the built-in unit's dimensions array this.dimensions = res.unit.dimensions.slice(0); } else { // Not a single unit, so send it off to the parser + console.log('Sending to parser'); var u = math.eval(name); + console.log("Returned from parser. Result = "); + console.log(u); this.units = u.units; - this.isUnitListValid = true; + this.isUnitListSimplified = true; this.dimensions = u.dimensions; } @@ -139,11 +153,13 @@ function factory (type, config, load, typed) { power: 0 } ]; - this.isUnitListValid = true; - this.dimensions = [0, 0, 0, 0, 0, 0, 0]; + this.isUnitListSimplified = true; + this.dimensions = [0, 0, 0, 0, 0, 0, 0, 0, 0]; } - this.value = (value != undefined) ? this._normalize(value) : null; + this.value = (value != undefined) ? this._normalize(value) : null; + console.log("In Unit(" + (value == null ? "null" : value.toString()) + ", '" + name + "'), setting value to " + this._normalize(value).toString()); + 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 @@ -235,27 +251,36 @@ 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); + console.log('parseNumber returned ' + number.toString()); + return number; } - + + // We can now safely say that this is scientific notation. + number = number + tentativeNumber; while (isDigit(c)) { number += c; next(); } } + console.log('parseNumber returned ' + number.toString()); return number; } @@ -263,6 +288,7 @@ function factory (type, config, load, typed) { /** * Match a regex pattern for units such as kg m / s^2. Only matches the pattern u1[^ex1] [u2[^ex2] ...] [/ u3[^ex3] [u4[^ex4]]]; does not check for the existence or validity of units. */ + /* function parseUnit() { var unitsPattern = /^\s*(?:(?:(?:[a-zA-Z][a-zA-Z0-9]*)\s*(?:\^\s*(?:[-+]?[0-9]*\.?[0-9]+(?:[eE][-+]?[0-9]+)?))?)\s*\/?\s*)*\s*(?:(?:[a-zA-Z][a-zA-Z0-9]*)\s*(?:\^\s*(?:[-+]?[0-9]*\.?[0-9]+(?:[eE][-+]?[0-9]+)?))?)$/; // This regex is quite ugly...Alternatively, do we just want to return the entire string without testing and let the Unit constructor check for valid format? @@ -276,28 +302,159 @@ function factory (type, config, load, typed) { return null; } } + */ - /* - // The old parseUnit function did not accept units like "m / s" --ericman314 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); } + console.log('parseUnit returned ' + unitName); return unitName || null; } - */ + + + function parseCharacter(toFind) { + if (c === toFind) { + console.log('parseCharacter returned ' + 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) { + text = str; + index = -1; + 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(); + // Optional number at the start of the string + var valueStr = parseNumber(); + var value = null; + if(valueStr) { + value = parseFloat(valueStr); + } + skipWhitespace(); // Whitespace is not required here + + // Next, we read any number of unit[^number] + var powerMultiplier = 1.0; + var expectingDenominator = false; + while (true) { + skipWhitespace(); + var uStr = parseUnit(); + if(uStr == null) { + // No more units. + 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 1e-12) { nNonZero++; + if (Math.abs(this.dimensions[i]-1.0) > 1e-12) { + return false; + } } } return nNonZero > 1; - /* - return this.units.length > 1 || Math.abs(this.units[0].power - 1.0) > 1e-15; */ - } + if(this.units.length === 0) { + return false; + } + return this.units.length > 1 || Math.abs(this.units[0].power - 1.0) > 1e-15; + } + /** * Normalize a value, based on its currently set unit(s) @@ -401,7 +569,7 @@ function factory (type, config, load, typed) { * @private */ Unit.prototype._normalize = function (value) { - this.generateUnitListLazy(); + this.simplifyUnitListLazy(); if (this.units.length === 0) { return value; } @@ -428,7 +596,7 @@ function factory (type, config, load, typed) { * @private */ Unit.prototype._denormalize = function (value, prefixValue) { - this.generateUnitListLazy(); + this.simplifyUnitListLazy(); if (this.units.length === 0) { return value; } @@ -501,7 +669,7 @@ function factory (type, config, load, typed) { Unit.prototype.hasBase = function (base) { // All dimensions must be the same - for(var i=0; i<7; i++) { + for(var i=0; i 1e-12) { return false; } @@ -526,7 +694,7 @@ function factory (type, config, load, typed) { */ Unit.prototype.equalBase = function (other) { // All dimensions must be the same - for(var i=0; i<7; i++) { + for(var i=0; i 1e-12) { return false; } @@ -598,20 +766,32 @@ function factory (type, config, load, typed) { */ Unit.prototype.multiply = function (other) { - console.log(this.dimensions); var res = this.clone(); - console.log(this.dimensions); - console.log(other.dimensions); - for(var i=0; i<7; i++) { + for(var i=0; i 1e-12) { - this.units.push({ + proposedUnitList.push({ unit: UNIT_SYSTEMS.current[baseDim].unit, prefix: UNIT_SYSTEMS.current[baseDim].prefix, power: this.dimensions[i] }); } } - this.isUnitListValid = true; + + // 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; } } @@ -966,26 +1173,83 @@ function factory (type, config, load, typed) { Unit.prototype.formatUnits = function () { // Lazy evaluation of the unit list - this.generateUnitListLazy(); + this.simplifyUnitListLazy(); + + var strNum = ""; + var strDen = ""; + var nNum = 0; + var nDen = 0; - var str = ""; - var anyNegativePowers = false; - var anyPositivePowers = false; for(var i=0; i 0) { - anyPositivePowers = true; + 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; + +/* + for(var i=0; i 0) { + nPositivePowers++; str += " " + this.units[i].prefix.name + this.units[i].unit.name; if(Math.abs(this.units[i].power - 1.0) > 1e-15) { str += "^" + this.units[i].power; } } else if (this.units[i].power < 0) { - anyNegativePowers = true; + nNegativePowers++; } } - if(anyNegativePowers && anyPositivePowers) { + if(nPositivePowers > 1) { + str = "(" + str + ")"; + if(nNegativePowers > 0 && nPositivePowers > 0) { str += " /"; } + if(nNegativePowers > 0) { + str += " + for(var i=0; i 0 && str.length > 0) { str += " "; } - str += this.formatUnits(); + str += unitStr; + console.log('str="' + str + '"'); } else if (this.units.length === 1) { + console.log('Units length = 1'); if (this.value !== null && !this.fixPrefix) { var bestPrefix = this._bestPrefix(); @@ -1084,7 +1337,10 @@ function factory (type, config, load, typed) { str += this.units[0].prefix.name + this.units[0].unit.name; } } - */ + else if (this.units.length === 0) { + str = format(this.value, options); + } + return str; }; @@ -1285,58 +1541,64 @@ function factory (type, config, load, typed) { * 4 Temperature * 5 Luminous intensity * 6 Amount of substance - * - * 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]. The unit "1 cal / (gm °C)" can be written in terms of the 7 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]. + * 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"]; + 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] + dimensions: [1, 0, 0, 0, 0, 0, 0, 0, 0] }, LENGTH: { - dimensions: [0, 1, 0, 0, 0, 0, 0] + dimensions: [0, 1, 0, 0, 0, 0, 0, 0, 0] }, TIME: { - dimensions: [0, 0, 1, 0, 0, 0, 0] + dimensions: [0, 0, 1, 0, 0, 0, 0, 0, 0] }, CURRENT: { - dimensions: [0, 0, 0, 1, 0, 0, 0] + dimensions: [0, 0, 0, 1, 0, 0, 0, 0, 0] }, TEMPERATURE: { - dimensions: [0, 0, 0, 0, 1, 0, 0] + dimensions: [0, 0, 0, 0, 1, 0, 0, 0, 0] }, LUMINOUS_INTENSITY: { - dimensions: [0, 0, 0, 0, 0, 1, 0] + dimensions: [0, 0, 0, 0, 0, 1, 0, 0, 0] }, AMOUNT_OF_SUBSTANCE: { - dimensions: [0, 0, 0, 0, 0, 0, 1] + dimensions: [0, 0, 0, 0, 0, 0, 1, 0, 0] }, FORCE: { - dimensions: [1, 1, -2, 0, 0, 0, 0] + dimensions: [1, 1, -2, 0, 0, 0, 0, 0, 0] }, SURFACE: { - dimensions: [2, 0, 0, 0, 0, 0, 0] + dimensions: [0, 2, 0, 0, 0, 0, 0, 0, 0] }, VOLUME: { - dimensions: [3, 0, 0, 0, 0, 0, 0] + dimensions: [0, 3, 0, 0, 0, 0, 0, 0, 0] }, ANGLE: { - dimensions: [0, 0, 0, 0, 0, 0, 0] + dimensions: [0, 0, 0, 0, 0, 0, 0, 1, 0] }, BIT: { - dimensions: [0, 0, 0, 0, 0, 0, 0] + 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, dimensions: []}; + 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 @@ -2194,10 +2456,12 @@ function factory (type, config, load, typed) { /** * 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). - * Default unit system: If I figure out how (or even if it's possible) to make a "static" property of the Unit "class" (sorry, coming from C++/C#) then the default unit system can be updated on the fly anytime a unit is constructed. The unit string is parsed and the corresponding units in the default unit system are updated, so that answers are given in the same units the user supplies. + * 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 = { - def: { + 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['']}, @@ -2205,26 +2469,26 @@ function factory (type, config, load, typed) { TEMPERATURE: {unit: UNITS.K, prefix: PREFIXES.SHORT['']}, LUMINOUS_INTENSITY: {unit: UNITS.cd, prefix: PREFIXES.SHORT['']}, AMOUNT_OF_SUBSTANCE: {unit: UNITS.mol, prefix: PREFIXES.SHORT['']}, - - FORCE: {unit: UNITS.N, 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['']}, } }; - // Clone "default" to create the other unit systems - UNIT_SYSTEMS.si = JSON.parse(JSON.stringify(UNIT_SYSTEMS.def)); - - UNIT_SYSTEMS.cgs = JSON.parse(JSON.stringify(UNIT_SYSTEMS.def)); + // 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['']}; // 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 - UNIT_SYSTEMS.current = UNIT_SYSTEMS.def; + UNIT_SYSTEMS.current = UNIT_SYSTEMS.auto; // Add dimensions to each built-in unit for (var key in UNITS) { diff --git a/test/function/arithmetic/divide.test.js b/test/function/arithmetic/divide.test.js index 68d5a3d0c..c821998e0 100644 --- a/test/function/arithmetic/divide.test.js +++ b/test/function/arithmetic/divide.test.js @@ -124,19 +124,22 @@ describe('divide', function() { }); it('should divide a number by a unit', function() { - assert.equal(divide(20, math.unit('4 N s')).toString(), '5 s / kg m'); + 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/min').toString(), '0.03125 gal / min'); + assert.equal(divide(math.unit('75 mi/h'), math.unit('40 mi/gal')).to('gal/minute').toString(), '0.03125 gal / minute'); }); - it('should divide valueless units', function() { - assert.equal(divide(math.unit('gal'), math.unit('L')).format(4), '3.785'); + 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 diff --git a/test/function/arithmetic/multiply.test.js b/test/function/arithmetic/multiply.test.js index ea35ccc32..3e721d627 100644 --- a/test/function/arithmetic/multiply.test.js +++ b/test/function/arithmetic/multiply.test.js @@ -156,18 +156,18 @@ describe('multiply', function() { 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')).format(5), '0.74322 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(), '0.002 m^3 / s'); + 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), '0.37161 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(), '0.002 m^3 / s'); - assert.equal(multiply(unit('m/s'), unit('h/m')).toString(), '3600'); + 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 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 ad9b0f6dc..2427b471e 100644 --- a/test/type/unit/Unit.test.js +++ b/test/type/unit/Unit.test.js @@ -41,7 +41,7 @@ describe('unit', function() { 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; }); @@ -332,7 +332,7 @@ describe('unit', function() { 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, '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'); }); @@ -360,7 +360,7 @@ describe('unit', function() { 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, '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'); }); @@ -374,7 +374,7 @@ describe('unit', function() { 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}); + {'mathjs': 'Unit', value: 5000, unit: '(kg m) / s^2', fixPrefix: true}); }); it('fromJSON', function () { @@ -407,7 +407,7 @@ 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'); + assert.equal(new Unit(null, 'kg m/s').format(), '(kg m) / s'); }); it('should format a unit with fixed prefix and without value', function() { @@ -486,10 +486,10 @@ describe('unit', function() { assert.equal(unit1.units[0].unit.name, 'g'); assert.equal(unit1.units[0].prefix.name, 'm'); - unit1 = Unit.parse('300 kg/min'); + 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, 'min'); + assert.equal(unit1.units[1].unit.name, 'minute'); assert.equal(unit1.units[0].prefix.name, 'k'); unit1 = Unit.parse('981 cm/s^2'); @@ -515,19 +515,30 @@ describe('unit', function() { }); 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); - assert.equal(Unit.parse('meter.'), null); - assert.equal(Unit.parse('meter/'), null); - assert.equal(Unit.parse('/meter'), null); + // 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.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); }); }); 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() { From 0a1c8446a5082e7469622149cbaee36766f41fbc Mon Sep 17 00:00:00 2001 From: Eric Date: Sat, 1 Aug 2015 20:58:08 +0000 Subject: [PATCH 07/10] Added a few more unit tests --- lib/type/unit/Unit.js | 81 ++++++++++++++++++++++++++++++++----- test/type/unit/Unit.test.js | 57 ++++++++++++++++++++++++-- 2 files changed, 124 insertions(+), 14 deletions(-) diff --git a/lib/type/unit/Unit.js b/lib/type/unit/Unit.js index 6b015568b..1d2ed4bda 100644 --- a/lib/type/unit/Unit.js +++ b/lib/type/unit/Unit.js @@ -569,7 +569,7 @@ function factory (type, config, load, typed) { * @private */ Unit.prototype._normalize = function (value) { - this.simplifyUnitListLazy(); +// this.simplifyUnitListLazy(); if (this.units.length === 0) { return value; } @@ -596,7 +596,7 @@ function factory (type, config, load, typed) { * @private */ Unit.prototype._denormalize = function (value, prefixValue) { - this.simplifyUnitListLazy(); +// this.simplifyUnitListLazy(); if (this.units.length === 0) { return value; } @@ -1107,7 +1107,7 @@ function factory (type, config, load, typed) { // Search for a matching base var matchingBase; - for(var key in UNIT_SYSTEMS.current) { + for(var key in currentUnitSystem) { if(this.hasBase(BASE_UNITS[key])) { matchingBase = key; break; @@ -1123,8 +1123,8 @@ function factory (type, config, load, typed) { var matchingUnit; if(matchingBase) { // Does the unit system have a matching unit? - if(UNIT_SYSTEMS.current.hasOwnProperty(matchingBase)) { - matchingUnit = UNIT_SYSTEMS.current[matchingBase] + if(currentUnitSystem.hasOwnProperty(matchingBase)) { + matchingUnit = currentUnitSystem[matchingBase] } } @@ -1149,8 +1149,8 @@ function factory (type, config, load, typed) { var baseDim = BASE_DIMENSIONS[i]; if(Math.abs(this.dimensions[i]) > 1e-12) { proposedUnitList.push({ - unit: UNIT_SYSTEMS.current[baseDim].unit, - prefix: UNIT_SYSTEMS.current[baseDim].prefix, + unit: currentUnitSystem[baseDim].unit, + prefix: currentUnitSystem[baseDim].prefix, power: this.dimensions[i] }); } @@ -2355,6 +2355,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, @@ -2479,16 +2493,63 @@ function factory (type, config, load, typed) { // 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.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.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['']}; + // 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 - UNIT_SYSTEMS.current = UNIT_SYSTEMS.auto; + 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) { diff --git a/test/type/unit/Unit.test.js b/test/type/unit/Unit.test.js index 2427b471e..f27b38a9c 100644 --- a/test/type/unit/Unit.test.js +++ b/test/type/unit/Unit.test.js @@ -20,10 +20,11 @@ describe('unit', function() { assert.equal(unit1.value, null); assert.equal(unit1.units[0].unit.name, 'g'); - unit1 = new Unit(9.81, "m/s^2"); + unit1 = new Unit(9.81, "kg m/s^2"); assert.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[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() { @@ -59,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() { @@ -95,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); }); }); @@ -117,7 +129,7 @@ describe('unit', function() { 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(001, 'kg m / s')), false); + assert.equal(new Unit(100, 'N').equals(new Unit(100, 'kg m / s')), false); }); }); @@ -334,6 +346,8 @@ describe('unit', function() { 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() { @@ -351,6 +365,37 @@ describe('unit', function() { assert.equal(new Unit(1000 ,'m').toString(), '1 km'); }); + + }); + + describe('simplifyUnitListLazy', function() { + it('should simplify derived units according to the chosen unit system', 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"); + + Unit.setUnitSystem('us'); + unit1.isUnitListSimplified = false; + unit1.simplifyUnitListLazy(); + assert.equal(unit1.units[0].unit.name, "lbf"); + assert.equal(unit1.toString(), "2.248089430997105 lbf"); + + Unit.setUnitSystem('cgs'); + unit1.isUnitListSimplified = false; + unit1.simplifyUnitListLazy(); + assert.equal(unit1.units[0].unit.name, "dyn"); + assert.equal(unit1.format(2), "1 Mdyn"); + }); + + 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; + unit1.simplifyUnitListLazy(); + assert.equal(unit1.toString(), "300 lbf"); + }); + }); describe('valueOf', function() { @@ -512,6 +557,10 @@ describe('unit', function() { 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() { From f5d166a8091c15ff6b4204acee1931088aaec443 Mon Sep 17 00:00:00 2001 From: Eric Date: Tue, 4 Aug 2015 05:26:00 +0000 Subject: [PATCH 08/10] Made an attempt at derived units with simplification Updated unit tests Updated examples Updated docs --- docs/datatypes/units.md | 9 +- docs/expressions/syntax.md | 7 +- docs/reference/units.md | 7 +- examples/basic_usage.js | 1 - examples/units.js | 38 +- lib/type/unit/Unit.js | 1521 ++++++++++---------------- test/function/arithmetic/pow.test.js | 12 + test/type/unit/Unit.test.js | 122 ++- 8 files changed, 728 insertions(+), 989 deletions(-) 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..ea4716f89 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 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/basic_usage.js b/examples/basic_usage.js index 744efd818..e85f275fc 100644 --- a/examples/basic_usage.js +++ b/examples/basic_usage.js @@ -34,7 +34,6 @@ console.log(); console.log('mixed use of data types'); print(math.add(4, [5, 6])); // number + Array, [9, 10] print(math.multiply(math.unit('5 mm'), 3)); // Unit * number, 15 mm -print(math.multiply(math.unit('8 ft'), math.unit('20 lbf')).to('ft lbf')); // Unit * number, 15 mm print(math.subtract([2, 3, 4], 5)); // Array - number, [-3, -2, -1] print(math.add(math.matrix([2, 3]), [4, 5])); // Matrix + Array, [6, 8] console.log(); diff --git a/examples/units.js b/examples/units.js index 1870a4384..7c9eea95a 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 @@ -39,6 +43,7 @@ console.log(); 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 @@ -46,26 +51,35 @@ console.log(); print(math.eval('number(5 cm, mm)')); // number, 50 console.log(); -// derived units -console.log('multiply, divide, exponentiate units'); -print(math.multiply(a, b)); // 0.045 m^2 -print(math.divide(a, b)); // 4.5 -print(math.pow(a, 3)); // 91125 cm^3 +// 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(); -console.log('compute molar volume of ideal gas at STP in L/mol:'); +// 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 lbf/in^2'); -var T = math.unit('25 celsius'); +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(); console.log('P = '); print(P); -console.log(); console.log('T = '); print(T); -console.log(); 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/type/unit/Unit.js b/lib/type/unit/Unit.js index 1d2ed4bda..5850138dd 100644 --- a/lib/type/unit/Unit.js +++ b/lib/type/unit/Unit.js @@ -6,9 +6,6 @@ var endsWith = require('../../utils/string').endsWith; function factory (type, config, load, typed) { - //console.log('Loading parse'); - //var parser = load(require('../../expression/function/parser'))(); - /** * @constructor Unit * @@ -27,7 +24,6 @@ function factory (type, config, load, typed) { * @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) { - console.log("Entering Unit("+(value==null?"null":value).toString()+", '"+name+"')"); if (!(this instanceof Unit)) { throw new Error('Constructor must be called with the new operator'); } @@ -39,111 +35,10 @@ function factory (type, config, load, typed) { throw new TypeError('Second parameter in Unit constructor must be a string'); } - // Matches a unit with an optional exponent, such as: - // "kg" - // "m^2" - // "s ^ -3" - // The unit is contained in the first capturing group - // The exponent (without the "^") is contained in the second capturing group - - if (name != undefined) { - - var u = Unit.parse(name); - - this.units = u.units; - this.dimensions = u.dimensions; - this.isUnitListSimplified = true; -/* - // Is name a single unit or a combination? - - var singleUnitPattern = /^([a-zA-Z][a-zA-Z0-9]*)$/; - if(singleUnitPattern.test(name)) { - console.log(name + ' matches singleUnitPattern'); - var res = _findUnit(name); - if (!res) { - throw new SyntaxError('Unknown unit "' + name + '"'); - } - this.units = [{ - unit: res.unit, - prefix: res.prefix, - power: 1.0 - }]; - this.isUnitListSimplified = true; - // Clone the built-in unit's dimensions array - this.dimensions = res.unit.dimensions.slice(0); - } - else { - // Not a single unit, so send it off to the parser - console.log('Sending to parser'); - var u = math.eval(name); - console.log("Returned from parser. Result = "); - console.log(u); - this.units = u.units; - this.isUnitListSimplified = true; - this.dimensions = u.dimensions; - } - - - /* - - this.units = []; - - // Split the string by the '/' character - var parts = name.split('/'); - - var multiplier = 1; - for(var i=0; i0) { - // After the first pass through the loop the units will go in the denominator - multiplier = -1; - } - */ - //var unitWithExpPattern = /\s*([a-zA-Z0-9]+)\s*(?:\^\s*([-+]?[0-9]*\.?[0-9]+(?:[eE][-+]?[0-9]+)?))?\s*/g; - /* - var result; - var lastIndex = unitWithExpPattern.lastIndex; - var reachedEndOfString = false; - while ((result = unitWithExpPattern.exec(parts[i])) !== null) { - if(typeof(result[1]) === 'undefined') { - throw new SyntaxError('Error encountered while parsing unit: "' + name + '"'); - } - if(result.index != lastIndex) { - // Oops, there were some extra characters at the beginning of the string. - throw new SyntaxError('Stray characters encountered while parsing unit: "' + name + '"'); - } - lastIndex = unitWithExpPattern.lastIndex; - if(lastIndex === parts[i].length) - reachedEndOfString = true; - - var thisName = result[1]; - var thisPower; - if(typeof(result[2]) === 'undefined') { - thisPower = multiplier; - } - else { - var thisPower = parseFloat(result[2]) * multiplier; - } - - var res = _findUnit(thisName); - if (!res) { - throw new SyntaxError('Unknown unit "' + thisName + '"'); - } - - this.units.push({ - unit: res.unit, - prefix: res.prefix, - power: thisPower - }); - - } - // Ensure the entire string was consumed by the regex - if(!reachedEndOfString) { - throw new SyntaxError('Trailing characters encountered while parsing unit: "' + name + '"'); - } - } - */ - + var u = Unit.parse(name); + this.units = u.units; + this.dimensions = u.dimensions; } else { this.units = [ @@ -153,20 +48,19 @@ function factory (type, config, load, typed) { power: 0 } ]; - this.isUnitListSimplified = true; - this.dimensions = [0, 0, 0, 0, 0, 0, 0, 0, 0]; + this.dimensions = [0, 0, 0, 0, 0, 0, 0, 0, 0]; } - this.value = (value != undefined) ? this._normalize(value) : null; - console.log("In Unit(" + (value == null ? "null" : value.toString()) + ", '" + name + "'), setting value to " + this._normalize(value).toString()); + 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 - console.log("Finished constructing " + this.toString()); - console.log("The finished unit is: "); - console.log(this); + // 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; + } /** @@ -202,9 +96,6 @@ function factory (type, config, load, typed) { c = text.charAt(index); } - // Would this work better as a regex? - // For example, /[-+]?[0-9]*\.?[0-9]+([eE][-+]?[0-9]+)?/ - // --ericman314 function parseNumber() { var number = ''; var oldIndex; @@ -251,10 +142,10 @@ function factory (type, config, load, typed) { // check for exponential notation like "2.3e-4" or "1.23e50" if (c == 'E' || c == 'e') { - // 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" + // 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; + var tentativeNumber = ''; + var tentativeIndex = index; tentativeNumber += c; next(); @@ -268,69 +159,52 @@ function factory (type, config, load, typed) { if (!isDigit(c)) { // The e or E must belong to something else, so return the number without the e or E. revert(tentativeIndex); - console.log('parseNumber returned ' + number.toString()); return number; } - - // We can now safely say that this is scientific notation. - number = number + tentativeNumber; + + // We can now safely say that this is scientific notation. + number = number + tentativeNumber; while (isDigit(c)) { number += c; next(); } } - console.log('parseNumber returned ' + number.toString()); return number; } - - /** - * Match a regex pattern for units such as kg m / s^2. Only matches the pattern u1[^ex1] [u2[^ex2] ...] [/ u3[^ex3] [u4[^ex4]]]; does not check for the existence or validity of units. - */ - /* - function parseUnit() { - var unitsPattern = /^\s*(?:(?:(?:[a-zA-Z][a-zA-Z0-9]*)\s*(?:\^\s*(?:[-+]?[0-9]*\.?[0-9]+(?:[eE][-+]?[0-9]+)?))?)\s*\/?\s*)*\s*(?:(?:[a-zA-Z][a-zA-Z0-9]*)\s*(?:\^\s*(?:[-+]?[0-9]*\.?[0-9]+(?:[eE][-+]?[0-9]+)?))?)$/; - // This regex is quite ugly...Alternatively, do we just want to return the entire string without testing and let the Unit constructor check for valid format? - var result = unitsPattern.exec(text.substr(index)); - if(result) { - index = text.length; - c = text.charAt(index); - return result[0]; - } - else { - return null; - } - } - */ - function parseUnit() { var unitName = ''; - // Alphanumeric characters only; matches [a-zA-Z0-9] - var code = text.charCodeAt(index); + // 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)) { + (code >= 65 && code <= 90) || + (code >= 97 && code <= 122)) { unitName += c; next(); - var code = text.charCodeAt(index); + var code = text.charCodeAt(index); } - console.log('parseUnit returned ' + unitName); - 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) { - console.log('parseCharacter returned ' + toFind); - next(); - return toFind; - } - else { - return null; - } + if (c === toFind) { + next(); + return toFind; + } + else { + return null; + } } /** @@ -339,182 +213,124 @@ function factory (type, config, load, typed) { * @param {string} str A string like "5.2 inch", "4e2 cm/s^2" * @return {Unit | null} unit */ - - - Unit.parse = function (str) { - text = str; - index = -1; - 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(); - // Optional number at the start of the string - var valueStr = parseNumber(); - var value = null; - if(valueStr) { - value = parseFloat(valueStr); - } - skipWhitespace(); // Whitespace is not required here - - // Next, we read any number of unit[^number] - var powerMultiplier = 1.0; - var expectingDenominator = false; - while (true) { - skipWhitespace(); - var uStr = parseUnit(); - if(uStr == null) { - // No more units. - 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 1e-12) { - nNonZero++; - if (Math.abs(this.dimensions[i]-1.0) > 1e-12) { - return false; - } + Unit.prototype._isDerived = function() { + if(this.units.length === 0) { + return false; } - } - return nNonZero > 1; - */ - if(this.units.length === 0) { - return false; - } - return this.units.length > 1 || Math.abs(this.units[0].power - 1.0) > 1e-15; - } - + return this.units.length > 1 || Math.abs(this.units[0].power - 1.0) > 1e-15; + } /** * Normalize a value, based on its currently set unit(s) @@ -569,23 +377,22 @@ function factory (type, config, load, typed) { * @private */ Unit.prototype._normalize = function (value) { -// this.simplifyUnitListLazy(); if (this.units.length === 0) { - return value; + 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; - } + // 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; + } }; /** @@ -596,30 +403,29 @@ function factory (type, config, load, typed) { * @private */ Unit.prototype._denormalize = function (value, prefixValue) { -// this.simplifyUnitListLazy(); - 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; - - /* - if(this._isDerived()) { + for(var i=0; i 1e-12) { return false; + } } - else { - return (this.units[0].unit.base === base); - } - */ + return true; + }; /** @@ -694,60 +492,12 @@ function factory (type, config, load, typed) { */ Unit.prototype.equalBase = function (other) { // All dimensions must be the same - for(var i=0; i 1e-12) { - return false; - } - } - return true; - - /* - - // We might have multiple units, each with multiple dimensions, each with non-unity powers - // For example, newton^2 (although I have no idea what that could possibly represent physically) - var diffDimensions = []; - // Add the dimensions of the first value - for(var i=0; i 1e-12) { + return false; + } } - - // Subtract the dimensions of the second value - for(var i=0; i 1e-12) { - return false; - } - } - return true; - */ }; /** @@ -766,94 +516,31 @@ function factory (type, config, load, typed) { */ 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] - }); + var matchingUnit; + if(matchingBase) { + // Does the unit system have a matching unit? + if(currentUnitSystem.hasOwnProperty(matchingBase)) { + matchingUnit = currentUnitSystem[matchingBase] } } - // 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; + + var value; + var str; + if(matchingUnit) { + this.units = [{ + unit: matchingUnit.unit, + prefix: matchingUnit.prefix, + power: 1.0, + }]; + this.isUnitListSimplified = true; + return; + } + else { + // Multiple units or units with powers are formatted like this: + // 5 (kg m^2) / (s^3 mol) + // Build an representation from the base units of the current unit system + 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; } - this.isUnitListSimplified = true; - return; - } } /** * Get a string representation of the units of this Unit, without the value. * @return {string} */ - Unit.prototype.formatUnits = function () { + Unit.prototype.formatUnits = function () { - // Lazy evaluation of the unit list - this.simplifyUnitListLazy(); + // Lazy evaluation of the unit list + this.simplifyUnitListLazy(); - var strNum = ""; - var strDen = ""; - var nNum = 0; - var nDen = 0; + 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; + 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++; } } - 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); + 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); } } - 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); + // 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; - -/* - for(var i=0; i 0) { - nPositivePowers++; - str += " " + this.units[i].prefix.name + this.units[i].unit.name; - if(Math.abs(this.units[i].power - 1.0) > 1e-15) { - str += "^" + this.units[i].power; - } + // 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 + ")"; } - else if (this.units[i].power < 0) { - nNegativePowers++; + if(nDen > 1 && nNum > 0) { + strDen = "(" + strDen + ")"; } - } - if(nPositivePowers > 1) { - str = "(" + str + ")"; - if(nNegativePowers > 0 && nPositivePowers > 0) { - str += " /"; - } - if(nNegativePowers > 0) { - str += " - for(var i=0; i 1e-15) { - str += "^" + (-this.units[i].power); - } - } - else { - str += "^" + (this.units[i].power); - } + var str = strNum; + if(nNum > 0 && nDen > 0) { + str += " / "; } - } - return str.substr(1); // Remove first ' ' from beginning of string - */ + str += strDen; - - }; + return str; + }; /** * Get a string representation of the Unit, with optional formatting options. @@ -1277,83 +847,52 @@ function factory (type, config, load, typed) { * options. * @return {string} */ - Unit.prototype.format = function (options) { -/* - var value; - var str; - if(matchingUnit) { - // The current unit system has a matching unit - 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 { - // There is no matching unit in the unit system, so format a group of base units - 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 += this.formatUnits(); - - } -*/ + Unit.prototype.format = function (options) { + +// Simplfy the unit list, if necessary + this.simplifyUnitListLazy(); + + 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); + } - var value, - str; - if (this._isDerived()) { - value = this._denormalize(this.value); - str = (this.value !== null) ? (format(value, options)) : ''; - var unitStr = this.formatUnits(); - console.log('unitStr="' + unitStr + '"'); - if(unitStr.length > 0 && str.length > 0) { - str += " "; - } - str += unitStr; - console.log('str="' + str + '"'); - } - else if (this.units.length === 1) { - console.log('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; - }; + return str; + }; /** * Calculate the best prefix using current value. * @returns {Object} prefix * @private */ - 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!"; - } + 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, @@ -1526,6 +1065,10 @@ 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} } }; @@ -1541,8 +1084,8 @@ function factory (type, config, load, typed) { * 4 Temperature * 5 Luminous intensity * 6 Amount of substance - * 7 Angle - * 8 Bit (digital) + * 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]. * */ @@ -1551,49 +1094,58 @@ function factory (type, config, load, typed) { var BASE_UNITS = { NONE: { - dimensions: [0, 0, 0, 0, 0, 0, 0, 0, 0] - }, + dimensions: [0, 0, 0, 0, 0, 0, 0, 0, 0] + }, MASS: { - dimensions: [1, 0, 0, 0, 0, 0, 0, 0, 0] - }, + dimensions: [1, 0, 0, 0, 0, 0, 0, 0, 0] + }, LENGTH: { - dimensions: [0, 1, 0, 0, 0, 0, 0, 0, 0] - }, + dimensions: [0, 1, 0, 0, 0, 0, 0, 0, 0] + }, TIME: { - dimensions: [0, 0, 1, 0, 0, 0, 0, 0, 0] - }, + dimensions: [0, 0, 1, 0, 0, 0, 0, 0, 0] + }, CURRENT: { - dimensions: [0, 0, 0, 1, 0, 0, 0, 0, 0] - }, + dimensions: [0, 0, 0, 1, 0, 0, 0, 0, 0] + }, TEMPERATURE: { - dimensions: [0, 0, 0, 0, 1, 0, 0, 0, 0] - }, + dimensions: [0, 0, 0, 0, 1, 0, 0, 0, 0] + }, LUMINOUS_INTENSITY: { - dimensions: [0, 0, 0, 0, 0, 1, 0, 0, 0] - }, + dimensions: [0, 0, 0, 0, 0, 1, 0, 0, 0] + }, AMOUNT_OF_SUBSTANCE: { - dimensions: [0, 0, 0, 0, 0, 0, 1, 0, 0] + dimensions: [0, 0, 0, 0, 0, 0, 1, 0, 0] }, FORCE: { - dimensions: [1, 1, -2, 0, 0, 0, 0, 0, 0] - }, + dimensions: [1, 1, -2, 0, 0, 0, 0, 0, 0] + }, SURFACE: { - dimensions: [0, 2, 0, 0, 0, 0, 0, 0, 0] - }, + dimensions: [0, 2, 0, 0, 0, 0, 0, 0, 0] + }, VOLUME: { - dimensions: [0, 3, 0, 0, 0, 0, 0, 0, 0] - }, + 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] - }, + dimensions: [0, 0, 0, 0, 0, 0, 0, 1, 0] + }, BIT: { - dimensions: [0, 0, 0, 0, 0, 0, 0, 0, 1] - } + dimensions: [0, 0, 0, 0, 0, 0, 0, 0, 1] + } }; for(var key in BASE_UNITS) { - BASE_UNITS[key].key = key; + BASE_UNITS[key].key = key; } var BASE_UNIT_NONE = {}; @@ -2355,20 +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 - }, + 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, @@ -2383,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: { @@ -2474,21 +2094,24 @@ function factory (type, config, load, typed) { */ 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['']}, + // 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['']}, - } + // 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 @@ -2496,12 +2119,16 @@ function factory (type, config, load, typed) { 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. @@ -2517,14 +2144,14 @@ function factory (type, config, load, typed) { * 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(); + 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. @@ -2532,10 +2159,10 @@ function factory (type, config, load, typed) { */ Unit.listAvailableUnitSystems = function() { var mess = ""; - for(var key in UNIT_SYSTEMS) { - mess += " " + key; + for(var key in UNIT_SYSTEMS) { + mess += " " + key; } - return mess.substr(1); + return mess.substr(1); } /** @@ -2543,9 +2170,9 @@ function factory (type, config, load, typed) { * @return {string} The current unit system. */ Unit.getUnitSystem = function() { - for(var key in UNIT_SYSTEMS) { - if(UNIT_SYSTEMS[key] === currentUnitSystem) { - return key; + for(var key in UNIT_SYSTEMS) { + if(UNIT_SYSTEMS[key] === currentUnitSystem) { + return key; } } } @@ -2554,7 +2181,7 @@ function factory (type, config, load, typed) { // Add dimensions to each built-in unit for (var key in UNITS) { var unit = UNITS[key]; - unit.dimensions = unit.base.dimensions; + unit.dimensions = unit.base.dimensions; } for (var name in PLURALS) { diff --git a/test/function/arithmetic/pow.test.js b/test/function/arithmetic/pow.test.js index 6c6cc59af..c4405d486 100644 --- a/test/function/arithmetic/pow.test.js +++ b/test/function/arithmetic/pow.test.js @@ -159,6 +159,18 @@ describe('pow', function() { 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)}); diff --git a/test/type/unit/Unit.test.js b/test/type/unit/Unit.test.js index f27b38a9c..102553e2e 100644 --- a/test/type/unit/Unit.test.js +++ b/test/type/unit/Unit.test.js @@ -117,6 +117,7 @@ describe('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); }); }); @@ -369,32 +370,55 @@ describe('unit', function() { }); describe('simplifyUnitListLazy', function() { - it('should simplify derived units according to the chosen unit system', 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"); - - Unit.setUnitSystem('us'); - unit1.isUnitListSimplified = false; - unit1.simplifyUnitListLazy(); - assert.equal(unit1.units[0].unit.name, "lbf"); - assert.equal(unit1.toString(), "2.248089430997105 lbf"); + 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"); + }); - Unit.setUnitSystem('cgs'); - unit1.isUnitListSimplified = false; - unit1.simplifyUnitListLazy(); - assert.equal(unit1.units[0].unit.name, "dyn"); - assert.equal(unit1.format(2), "1 Mdyn"); - }); + 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"); + }); - 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; - unit1.simplifyUnitListLazy(); - assert.equal(unit1.toString(), "300 lbf"); - }); }); @@ -563,7 +587,7 @@ describe('unit', function() { assert.equal(unit1.units[0].unit.name, 'bytes'); }); - it('should return null when parsing an invalid unit', function() { + 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/); @@ -574,6 +598,7 @@ describe('unit', function() { 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); @@ -591,6 +616,53 @@ describe('unit', function() { }); }); + 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 () { From 377be29eb64b1bf664fa3fdd6c2cd60a1de0a99e Mon Sep 17 00:00:00 2001 From: Eric Mansfield Date: Mon, 3 Aug 2015 23:34:30 -0600 Subject: [PATCH 09/10] Fix my typos --- docs/expressions/syntax.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/expressions/syntax.md b/docs/expressions/syntax.md index ea4716f89..84fe42810 100644 --- a/docs/expressions/syntax.md +++ b/docs/expressions/syntax.md @@ -325,7 +325,7 @@ parser.eval('number(a)'); // Error: 2 + i is no valid number ### Units -math.js supports units. Units can be used the arithmetic operations +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 From a44ccd09bb2fc2bf98a44250c43eeabcd5aa86a7 Mon Sep 17 00:00:00 2001 From: Eric Mansfield Date: Mon, 3 Aug 2015 23:36:00 -0600 Subject: [PATCH 10/10] Removed debugging output --- lib/function/arithmetic/divideScalar.js | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/function/arithmetic/divideScalar.js b/lib/function/arithmetic/divideScalar.js index 1137ec024..cdb14b2df 100644 --- a/lib/function/arithmetic/divideScalar.js +++ b/lib/function/arithmetic/divideScalar.js @@ -37,7 +37,6 @@ function factory(type, config, load, typed) { 'number, Unit': function (x, y) { var xUnit = new type.Unit(x); - console.log(xUnit); return xUnit.divide(y); },