-
Notifications
You must be signed in to change notification settings - Fork 103
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add support to define custom units #112
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -367,6 +367,59 @@ Conversions are probably better done like this... | |
Qty('0 tempC').add('100 degC') // => 100 tempC | ||
``` | ||
|
||
### Custom units and prefixes | ||
|
||
You can define your own custom units and prefixes by using the `Qty.defineUnit` | ||
function. | ||
|
||
```javascript | ||
Qty.defineUnit( unitName, unitDefinition[, isBase] ); | ||
``` | ||
|
||
`unitName` is the official name of the unit wrapped in `<` and `>`, | ||
e.g. `<meter>` | ||
|
||
`unitDefinition` is an array of definition values. For units it is: | ||
- `aliases` is an array of aliases for the unit, e.g. `["m","meter","meters","metre","metres"]` | ||
- `conversion` is a decimal number that can be multiplied to the number to | ||
convert to the base unit | ||
- `kind` is the kind of the unit. If you specify an unknown kind here, be | ||
sure to include a unit with `isBase` set to true for doing conversions | ||
- `numerator` is an array containing the set of units that can be specified as a numerator for this unit | ||
- `denominator` is an array containing the set of units that can be specified as a denominator for this unit | ||
|
||
`unitDefinition` for prefixes is sligly different. | ||
- `aliases` is an array of aliases for the unit, e.g. `["m","meter","meters","metre","metres"]` | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't think "meter" is a good example for a prefix. |
||
- `conversion` is a decimal number that can be multiplied to the number to | ||
convert to the base non-prefixed unit | ||
- `kind` must be `prefix` | ||
|
||
`isBase` indicates whether this is a base unit for the specified kind. You | ||
must include one base unit for each kind. | ||
|
||
Example new unit: | ||
```javascript | ||
Qty.defineUnit("<CanadianDollar>", [["CAD","CanadianDollar"], 0.78, "currency", ["<dollar>"]]); | ||
var qty = Qty('1 CAD'); | ||
qty.to('USD').toString(); // => '0.78 USD' | ||
``` | ||
|
||
Example new base unit: | ||
```javascript | ||
Qty.defineUnit("<myNewUnit>", [["MYU","myNewUnit"], 1.0, "myNewKind", ["<myNewUnit>"]], true); | ||
var qty = Qty(`1 myNewUnit`); | ||
``` | ||
|
||
Example new prefix: | ||
```javascript | ||
Qty.defineUnit("<fooPrefix>", [["foo"], 1e5, "prefix"]); | ||
var qty = Qty(`3 foometers`); | ||
qty.to('meters').toString(); // => '300000 meters' | ||
``` | ||
|
||
Defining new units should be done carefully and tested thoroughly as it can | ||
introduce conflicts while parsing other units. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why not throw an error if you find a collision? |
||
|
||
### Errors | ||
|
||
Every error thrown by JS-quantities is an instance of `Qty.Error`. | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -510,9 +510,11 @@ SOFTWARE. | |
var UNIT_VALUES = {}; | ||
var UNIT_MAP = {}; | ||
var OUTPUT_MAP = {}; | ||
for (var unitDef in UNITS) { | ||
if (UNITS.hasOwnProperty(unitDef)) { | ||
var definition = UNITS[unitDef]; | ||
|
||
function defineUnit(unitDef, definition, isBase) { | ||
let oldDef = UNITS[unitDef]; | ||
try { | ||
UNITS[unitDef] = definition; | ||
if (definition[2] === "prefix") { | ||
PREFIX_VALUES[unitDef] = definition[1]; | ||
for (var i = 0; i < definition[0].length; i++) { | ||
|
@@ -529,9 +531,25 @@ SOFTWARE. | |
for (var j = 0; j < definition[0].length; j++) { | ||
UNIT_MAP[definition[0][j]] = unitDef; | ||
} | ||
if (isBase) { | ||
if (BASE_UNITS.indexOf(unitDef) === -1) { | ||
BASE_UNITS.push(unitDef); | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. else throw error? silent failures are difficult to debug. PS - What happens if |
||
} | ||
} | ||
OUTPUT_MAP[unitDef] = definition[0][0]; | ||
} | ||
catch (e) { | ||
UNITS[unitDef] = oldDef; | ||
throw e; | ||
} | ||
} | ||
|
||
for (var unitDef in UNITS) { | ||
if (UNITS.hasOwnProperty(unitDef)) { | ||
var definition = UNITS[unitDef]; | ||
defineUnit(unitDef, definition); | ||
} | ||
} | ||
|
||
/** | ||
|
@@ -667,6 +685,30 @@ SOFTWARE. | |
var TOP_REGEX = new RegExp ("([^ \\*\\d]+?)(?:" + POWER_OP + ")?(-?" + SAFE_POWER + "(?![a-zA-Z]))"); | ||
var BOTTOM_REGEX = new RegExp("([^ \\*\\d]+?)(?:" + POWER_OP + ")?(" + SAFE_POWER + "(?![a-zA-Z]))"); | ||
|
||
function getRegexes() { | ||
var PREFIX_REGEX = Object.keys(PREFIX_MAP).sort(function(a, b) { | ||
return b.length - a.length; | ||
}).join("|"); | ||
var UNIT_REGEX = Object.keys(UNIT_MAP).sort(function(a, b) { | ||
return b.length - a.length; | ||
}).join("|"); | ||
|
||
/* | ||
* Minimal boundary regex to support units with Unicode characters | ||
* \b only works for ASCII | ||
*/ | ||
var BOUNDARY_REGEX = "\\b|$"; | ||
var UNIT_MATCH = "(" + PREFIX_REGEX + ")??(" + | ||
UNIT_REGEX + | ||
")(?:" + BOUNDARY_REGEX + ")"; | ||
var UNIT_TEST_REGEX = new RegExp("^\\s*(" + UNIT_MATCH + "[\\s\\*]*)+$"); | ||
var UNIT_MATCH_REGEX = new RegExp(UNIT_MATCH, "g"); // g flag for multiple occurences | ||
|
||
return { | ||
UNIT_TEST_REGEX, | ||
UNIT_MATCH_REGEX | ||
}; | ||
} | ||
/* parse a string into a unit object. | ||
* Typical formats like : | ||
* "5.6 kg*m/s^2" | ||
|
@@ -702,6 +744,8 @@ SOFTWARE. | |
var top = result[2]; | ||
var bottom = result[3]; | ||
|
||
var regexes = getRegexes(); | ||
|
||
var n, x, nx; | ||
// TODO DRY me | ||
while ((result = TOP_REGEX.exec(top))) { | ||
|
@@ -711,7 +755,7 @@ SOFTWARE. | |
throw new QtyError("Unit exponent is not a number"); | ||
} | ||
// Disallow unrecognized unit even if exponent is 0 | ||
if (n === 0 && !UNIT_TEST_REGEX.test(result[1])) { | ||
if (n === 0 && !regexes.UNIT_TEST_REGEX.test(result[1])) { | ||
throw new QtyError("Unit not recognized"); | ||
} | ||
x = result[1] + " "; | ||
|
@@ -735,7 +779,7 @@ SOFTWARE. | |
throw new QtyError("Unit exponent is not a number"); | ||
} | ||
// Disallow unrecognized unit even if exponent is 0 | ||
if (n === 0 && !UNIT_TEST_REGEX.test(result[1])) { | ||
if (n === 0 && !regexes.UNIT_TEST_REGEX.test(result[1])) { | ||
throw new QtyError("Unit not recognized"); | ||
} | ||
x = result[1] + " "; | ||
|
@@ -755,22 +799,6 @@ SOFTWARE. | |
} | ||
} | ||
|
||
var PREFIX_REGEX = Object.keys(PREFIX_MAP).sort(function(a, b) { | ||
return b.length - a.length; | ||
}).join("|"); | ||
var UNIT_REGEX = Object.keys(UNIT_MAP).sort(function(a, b) { | ||
return b.length - a.length; | ||
}).join("|"); | ||
/* | ||
* Minimal boundary regex to support units with Unicode characters | ||
* \b only works for ASCII | ||
*/ | ||
var BOUNDARY_REGEX = "\\b|$"; | ||
var UNIT_MATCH = "(" + PREFIX_REGEX + ")??(" + | ||
UNIT_REGEX + | ||
")(?:" + BOUNDARY_REGEX + ")"; | ||
var UNIT_TEST_REGEX = new RegExp("^\\s*(" + UNIT_MATCH + "[\\s\\*]*)+$"); | ||
var UNIT_MATCH_REGEX = new RegExp(UNIT_MATCH, "g"); // g flag for multiple occurences | ||
var parsedUnitsCache = {}; | ||
/** | ||
* Parses and converts units string to normalized unit array. | ||
|
@@ -792,12 +820,13 @@ SOFTWARE. | |
|
||
var unitMatch, normalizedUnits = []; | ||
|
||
var regexes = getRegexes(); | ||
// Scan | ||
if (!UNIT_TEST_REGEX.test(units)) { | ||
if (!regexes.UNIT_TEST_REGEX.test(units)) { | ||
throw new QtyError("Unit not recognized"); | ||
} | ||
|
||
while ((unitMatch = UNIT_MATCH_REGEX.exec(units))) { | ||
while ((unitMatch = regexes.UNIT_MATCH_REGEX.exec(units))) { | ||
normalizedUnits.push(unitMatch.slice(1)); | ||
} | ||
|
||
|
@@ -1421,6 +1450,7 @@ SOFTWARE. | |
|
||
Qty.parse = globalParse; | ||
|
||
Qty.defineUnit = defineUnit; | ||
Qty.getUnits = getUnits; | ||
Qty.getAliases = getAliases; | ||
|
||
|
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -1476,6 +1476,27 @@ describe("js-quantities", function() { | |
}); | ||
}); | ||
|
||
describe("Qty.defineUnit", function() { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. unit/alias collisions throwing errors, and tests for that would be nice maybe also a way to remove an existing unit |
||
it("should define a new unit with existing kind", function() { | ||
Qty.defineUnit("<CanadianDollar>", [["CAD","CanadianDollar"], 0.78, "currency", ["<dollar>"]]); | ||
expect(Qty.getUnits("currency")).toContain("CanadianDollar"); | ||
expect(Qty("1 CAD").eq(Qty("0.78 USD"))); | ||
}); | ||
it("should validate the unit conversion factor", function() { | ||
expect(function() { | ||
Qty.defineUnit("<CanadianDollar>", [["CAD","CanadianDollar"], "invalid", "currency", ["<dollar>"]]); | ||
}).toThrowError("<CanadianDollar>: Invalid unit definition. 'scalar' must be a number"); | ||
}); | ||
it("should define a new prefix", function() { | ||
Qty.defineUnit("<fooPrefix>", [["foo"], 1e5, "prefix"]); | ||
expect(Qty("3 foometers").eq(Qty("300000 m"))).toBe(true); | ||
}); | ||
it("should define a new unit with new kind", function() { | ||
Qty.defineUnit("<myNewUnit>", [["FUBAR","myNewUnit"], 1.0, "myNewKind", ["<myNewUnit>"]], true); | ||
expect(Qty("300 FUBAR").eq(Qty("3 hectomyNewUnit"))).toBe(true); | ||
}); | ||
}); | ||
|
||
describe("information", function() { | ||
describe("bits and bytes", function() { | ||
it("should have 'information' as kind", function() { | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why an array? Why not build the array yourself internally out of an object that's passed in? Less chance of accidentally swapping order that way.