diff --git a/docs/datatypes/units.md b/docs/datatypes/units.md index 32337679d..1a0325be6 100644 --- a/docs/datatypes/units.md +++ b/docs/datatypes/units.md @@ -23,10 +23,11 @@ math.unit(unit: Unit) : Unit Example usage: ```js -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 +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 +var e = math.unit('101325 kg/(m s^2)'); // Unit 101325 kg / (m s^2) ``` A `Unit` contains the following functions: @@ -70,6 +71,19 @@ c.equalBase(b); // false d.toString(); // String "5.08 cm" ``` +Use care when creating a unit with multiple terms in the denominator. Implicit multiplication has the same operator precedence as explicit multiplication and division, which means these three expressions are identical: + +```js +// These three are identical +var correct1 = math.unit('8.314 m^3 Pa / mol / K'); // Unit 8.314 (m^3 Pa) / (mol K) +var correct2 = math.unit('8.314 (m^3 Pa) / (mol K)'); // Unit 8.314 (m^3 Pa) / (mol K) +var correct3 = math.unit('8.314 (m^3 * Pa) / (mol * K)'); // Unit 8.314 (m^3 Pa) / (mol K) +``` +But this expression, which omits the second `/` between `mol` and `K`, results in the wrong value: +```js +// Missing the second '/' between 'mol' and 'K' +var incorrect = math.unit('8.314 m^3 Pa / mol K'); // Unit 8.314 (m^3 Pa K) / mol +``` ## Calculations @@ -87,7 +101,7 @@ 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 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 diff --git a/lib/type/unit/Unit.js b/lib/type/unit/Unit.js index da7c272fa..6b6ad73a9 100644 --- a/lib/type/unit/Unit.js +++ b/lib/type/unit/Unit.js @@ -208,10 +208,10 @@ function factory (type, config, load, typed) { } /** - * Parse a string into a unit. Returns null if the provided string does not - * contain a valid unit. + * Parse a string into a unit. Throws an exception if the provided string does not + * contain a valid unit or cannot be parsed. * @param {string} str A string like "5.2 inch", "4e2 cm/s^2" - * @return {Unit | null} unit + * @return {Unit} unit */ Unit.parse = function (str) { text = str; @@ -249,28 +249,49 @@ function factory (type, config, load, typed) { skipWhitespace(); // Whitespace is not required here // Next, we read any number of unit[^number] - var powerMultiplier = 1; + var powerMultiplierCurrent = 1; var expectingUnit = false; + + // Stack to keep track of powerMultipliers applied to each parentheses group + var powerMultiplierStack = []; + + // Running product of all elements in powerMultiplierStack + var powerMultiplierStackProduct = 1; + while (true) { skipWhitespace(); + + // Check for and consume opening parentheses, pushing powerMultiplierCurrent to the stack + // A '(' will always appear directly before a unit. + while (c === '(') { + powerMultiplierStack.push(powerMultiplierCurrent); + powerMultiplierStackProduct *= powerMultiplierCurrent; + powerMultiplierCurrent = 1; + next(); + skipWhitespace(); + } + + // Is there something here? if(c) { + var oldC = c; var uStr = parseUnit(); if(uStr == null) { - // No more units. - throw new SyntaxError('Could not parse'); + throw new SyntaxError('Unexpected "' + oldC + '" in "' + text + '" at index ' + index.toString()); } } else { // End of input. break; } + + // Verify the unit exists and get the prefix (if any) var res = _findUnit(uStr); if(res == null) { // Unit not found. - //return null; throw new SyntaxError('Unit "' + uStr + '" not found.'); } - var power = powerMultiplier; + + var power = powerMultiplierCurrent * powerMultiplierStackProduct; // Is there a "^ number"? skipWhitespace(); if (parseCharacter('^')) { @@ -280,9 +301,10 @@ function factory (type, config, load, typed) { // No valid number found for the power! throw new SyntaxError('In "' + str + '", "^" must be followed by a floating-point number'); } - power = p * powerMultiplier; + power *= p; } - // Add the unit + + // Add the unit to the list unit.units.push( { unit: res.unit, prefix: res.prefix, @@ -291,23 +313,36 @@ function factory (type, config, load, typed) { for(var i=0; i fromJSON should recover an "equal" unit', function() { + var unit1 = Unit.parse('1.23(m/(s/(kg mol)/(lbm/h)K))'); + var unit2 = Unit.fromJSON(unit1.toJSON()); + assert.equal(unit1.equals(unit2), true); + }); + }); describe('format', function () { @@ -594,8 +600,43 @@ describe('unit', function() { assert.equal(unit1.units[0].unit.name, 'bytes'); }); + it('should parse expressions with nested parentheses correctly', function() { + unit1 = Unit.parse('8.314 kg (m^2 / (s^2 / (K^-1 / mol)))'); + approx.equal(unit1.value, 8.314); + assert.equal(unit1.units[0].unit.name, 'g'); + assert.equal(unit1.units[1].unit.name, 'm'); + assert.equal(unit1.units[2].unit.name, 's'); + assert.equal(unit1.units[3].unit.name, 'K'); + assert.equal(unit1.units[4].unit.name, 'mol'); + assert.equal(unit1.units[0].power, 1); + assert.equal(unit1.units[1].power, 2); + assert.equal(unit1.units[2].power, -2); + assert.equal(unit1.units[3].power, -1); + assert.equal(unit1.units[4].power, -1); + assert.equal(unit1.units[0].prefix.name, 'k'); + + unit1 = Unit.parse('1 (m / ( s / ( kg mol ) / ( lbm / h ) K ) )'); + assert.equal(unit1.units[0].unit.name, 'm'); + assert.equal(unit1.units[1].unit.name, 's'); + assert.equal(unit1.units[2].unit.name, 'g'); + assert.equal(unit1.units[3].unit.name, 'mol'); + assert.equal(unit1.units[4].unit.name, 'lbm'); + assert.equal(unit1.units[5].unit.name, 'h'); + assert.equal(unit1.units[6].unit.name, 'K'); + assert.equal(unit1.units[0].power, 1); + assert.equal(unit1.units[1].power, -1); + assert.equal(unit1.units[2].power, 1); + assert.equal(unit1.units[3].power, 1); + assert.equal(unit1.units[4].power, 1); + assert.equal(unit1.units[5].power, -1); + assert.equal(unit1.units[6].power, -1); + + unit2 = Unit.parse('1(m/(s/(kg mol)/(lbm/h)K))'); + assert.deepEqual(unit1, unit2); + }); + it('should parse units with correct precedence', function() { - var unit1 = Unit.parse('1 m^3 / kg s^2'); // implicit multiplication + var unit1 = Unit.parse('1 m^3 / kg s^2'); // implicit multiplication approx.equal(unit1.value, 1); assert.equal(unit1.units[0].unit.name, 'm'); @@ -607,32 +648,21 @@ describe('unit', function() { assert.equal(unit1.units[0].prefix.name, ''); }); - 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/); + it('should throw an exception when parsing an invalid unit', function() { + assert.throws(function () {Unit.parse('.meter')}, /Unexpected "\."/); 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('5e1.3')}, /Unexpected "\."/); 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.')}, /Unexpected "\."/); assert.throws(function () {Unit.parse('meter/')}, /Trailing characters/); - assert.throws(function () {Unit.parse('/meter')}, /Could not parse/); - assert.throws(function () {Unit.parse('45 kg 34 m')}, /Could not parse/); -// assert.equal(Unit.parse('.meter'), null); -// assert.equal(Unit.parse('5e'), null); -// assert.equal(Unit.parse('5e. meter'), null); -// assert.equal(Unit.parse('5e1.3 meter'), null); -// assert.equal(Unit.parse('5'), null); -// assert.equal(Unit.parse(''), null); -// assert.equal(Unit.parse('meter.'), null); -// assert.equal(Unit.parse('meter/'), null); -// assert.equal(Unit.parse('/meter'), null); + assert.throws(function () {Unit.parse('/meter')}, /Unexpected "\/"/); + assert.throws(function () {Unit.parse('45 kg 34 m')}, /Unexpected "3"/); }); - it('should return null when parsing an invalid type of argument', function() { + it('should throw an exception when parsing an invalid type of argument', function() { assert.throws(function () {Unit.parse(123)}, /Invalid argument in Unit.parse. Valid types are/); -// assert.equal(Unit.parse(123), null); }); });