Merge pull request #425 from ericman314/unit-parse-parentheses

Added support for parentheses to Unit.parse
This commit is contained in:
Jos de Jong 2015-08-09 23:12:09 +02:00
commit af0b0960c0
3 changed files with 125 additions and 40 deletions

View File

@ -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

View File

@ -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<BASE_DIMENSIONS.length; i++) {
unit.dimensions[i] += res.unit.dimensions[i] * power;
}
// Is there a forward slash? If so, all remaining units are in the denominator.
expectingUnit = false;
// Check for and consume closing parentheses, popping from the stack.
// A ')' will always follow a unit.
skipWhitespace();
while (c === ')') {
if(powerMultiplierStack.length === 0) {
throw new SyntaxError('Unmatched ")" in "' + text + '" at index ' + index.toString());
}
powerMultiplierStackProduct /= powerMultiplierStack.pop();
next();
skipWhitespace();
}
// "*" and "/" should mean we are expecting something to come next.
// Is there a forward slash? If so, negate powerMultiplierCurrent. The next unit or paren group is in the denominator.
expectingUnit = false;
if (parseCharacter('*')) {
// explicit multiplication
powerMultiplier = 1;
powerMultiplierCurrent = 1;
expectingUnit = true;
}
else if (parseCharacter('/')) {
// division
powerMultiplier = -1;
powerMultiplierCurrent = -1;
expectingUnit = true;
}
else {
// implicit multiplication
powerMultiplier = 1;
powerMultiplierCurrent = 1;
}
// Replace the unit into the auto unit system
@ -329,6 +364,12 @@ function factory (type, config, load, typed) {
throw new SyntaxError('Trailing characters: "' + str + '"');
}
// Is the parentheses stack empty?
if(powerMultiplierStack.length !== 0) {
throw new SyntaxError('Unmatched "(" in "' + text + '"');
}
// Are there any units at all?
if(unit.units.length == 0) {
throw new SyntaxError('"' + str + '" contains no units');
}

View File

@ -364,7 +364,7 @@ describe('unit', function() {
assert.equal(new Unit(500 ,'m').toString(), '500 m');
assert.equal(new Unit(600 ,'m').toString(), '0.6 km');
assert.equal(new Unit(1000 ,'m').toString(), '1 km');
assert.equal(new Unit(1000 ,'ohm').toString(), '1 kohm');
assert.equal(new Unit(1000 ,'ohm').toString(), '1 kohm');
});
});
@ -463,6 +463,12 @@ describe('unit', function() {
assert.deepEqual(u5, u6);
});
it('toJSON -> 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);
});
});