Skip to content

Commit

Permalink
Support for custom types (based on: gentooboontoo#112)
Browse files Browse the repository at this point in the history
  • Loading branch information
philippspohn committed Mar 23, 2023
1 parent 4226c6a commit a55472f
Show file tree
Hide file tree
Showing 7 changed files with 266 additions and 91 deletions.
53 changes: 53 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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"]`
- `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.

### Errors

Every error thrown by JS-quantities is an instance of `Qty.Error`.
Expand Down
99 changes: 67 additions & 32 deletions build/quantities.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ SOFTWARE.
typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
typeof define === 'function' && define.amd ? define(factory) :
(global = typeof globalThis !== 'undefined' ? globalThis : global || self, global.Qty = factory());
}(this, (function () { 'use strict';
})(this, (function () { 'use strict';

/**
* Tests if a value is a string
Expand Down Expand Up @@ -383,13 +383,14 @@ SOFTWARE.
"<oersted>" : [["Oe","oersted","oersteds"], 250.0 / Math.PI, "magnetism", ["<ampere>"], ["<meter>"]],

/* energy */
"<joule>" : [["J","joule","Joule","joules"], 1.0, "energy", ["<meter>","<meter>","<kilogram>"], ["<second>","<second>"]],
"<joule>" : [["J","joule","Joule","joules","Joules"], 1.0, "energy", ["<meter>","<meter>","<kilogram>"], ["<second>","<second>"]],
"<erg>" : [["erg","ergs"], 1e-7, "energy", ["<meter>","<meter>","<kilogram>"], ["<second>","<second>"]],
"<btu>" : [["BTU","btu","BTUs"], 1055.056, "energy", ["<meter>","<meter>","<kilogram>"], ["<second>","<second>"]],
"<calorie>" : [["cal","calorie","calories"], 4.18400, "energy",["<meter>","<meter>","<kilogram>"], ["<second>","<second>"]],
"<Calorie>" : [["Cal","Calorie","Calories"], 4184.00, "energy",["<meter>","<meter>","<kilogram>"], ["<second>","<second>"]],
"<therm-US>" : [["th","therm","therms","Therm","therm-US"], 105480400, "energy",["<meter>","<meter>","<kilogram>"], ["<second>","<second>"]],
"<Wh>" : [["Wh"], 3600, "energy",["<meter>","<meter>","<kilogram>"], ["<second>","<second>"]],
"<electronvolt>" : [["eV", "electronvolt", "electronvolts"], 1.602176634E-19, "energy", ["<meter>","<meter>","<kilogram>"], ["<second>","<second>"]],

/* force */
"<newton>" : [["N","Newton","newton"], 1.0, "force", ["<kilogram>","<meter>"], ["<second>","<second>"]],
Expand All @@ -402,6 +403,8 @@ SOFTWARE.
/* angle */
"<radian>" :[["rad","radian","radians"], 1.0, "angle", ["<radian>"]],
"<degree>" :[["deg","degree","degrees"], Math.PI / 180.0, "angle", ["<radian>"]],
"<arcminute>" :[["arcmin","arcminute","arcminutes"], Math.PI / 10800.0, "angle", ["<radian>"]],
"<arcsecond>" :[["arcsec","arcsecond","arcseconds"], Math.PI / 648000.0, "angle", ["<radian>"]],
"<gradian>" :[["gon","grad","gradian","grads"], Math.PI / 200.0, "angle", ["<radian>"]],
"<steradian>" : [["sr","steradian","steradians"], 1.0, "solid_angle", ["<steradian>"]],

Expand Down Expand Up @@ -460,7 +463,9 @@ SOFTWARE.
"<dozen>" : [["doz","dz","dozen"],12.0,"prefix_only", ["<each>"]],
"<percent>": [["%","percent"], 0.01, "prefix_only", ["<1>"]],
"<ppm>" : [["ppm"],1e-6, "prefix_only", ["<1>"]],
"<ppt>" : [["ppt"],1e-9, "prefix_only", ["<1>"]],
"<ppb>" : [["ppb"],1e-9, "prefix_only", ["<1>"]],
"<ppt>" : [["ppt"],1e-12, "prefix_only", ["<1>"]],
"<ppq>" : [["ppq"],1e-15, "prefix_only", ["<1>"]],
"<gross>" : [["gr","gross"],144.0, "prefix_only", ["<dozen>","<dozen>"]],
"<decibel>" : [["dB","decibel","decibels"], 1.0, "logarithmic", ["<decibel>"]]
};
Expand All @@ -487,20 +492,20 @@ SOFTWARE.
var denominator = definition[4] || [];
if (!isNumber(scalar)) {
throw new QtyError(unitDef + ": Invalid unit definition. " +
"'scalar' must be a number");
"'scalar' must be a number");
}

numerator.forEach(function(unit) {
if (UNITS[unit] === undefined) {
throw new QtyError(unitDef + ": Invalid unit definition. " +
"Unit " + unit + " in 'numerator' is not recognized");
"Unit " + unit + " in 'numerator' is not recognized");
}
});

denominator.forEach(function(unit) {
if (UNITS[unit] === undefined) {
throw new QtyError(unitDef + ": Invalid unit definition. " +
"Unit " + unit + " in 'denominator' is not recognized");
"Unit " + unit + " in 'denominator' is not recognized");
}
});
}
Expand All @@ -510,9 +515,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++) {
Expand All @@ -529,9 +536,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);
}
}
}
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);
}
}

/**
Expand Down Expand Up @@ -652,8 +675,8 @@ SOFTWARE.
var SIGNED_INTEGER = SIGN + "?" + INTEGER;
var FRACTION = "\\." + INTEGER;
var FLOAT = "(?:" + INTEGER + "(?:" + FRACTION + ")?" + ")" +
"|" +
"(?:" + FRACTION + ")";
"|" +
"(?:" + FRACTION + ")";
var EXPONENT = "[Ee]" + SIGNED_INTEGER;
var SCI_NUMBER = "(?:" + FLOAT + ")(?:" + EXPONENT + ")?";
var SIGNED_NUMBER = SIGN + "?\\s*" + SCI_NUMBER;
Expand All @@ -667,6 +690,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("|").replace("$", "\\$");

/*
* 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"
Expand Down Expand Up @@ -702,6 +749,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))) {
Expand All @@ -711,7 +760,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] + " ";
Expand All @@ -735,7 +784,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] + " ";
Expand All @@ -755,22 +804,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.
Expand All @@ -792,12 +825,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));
}

Expand Down Expand Up @@ -1421,6 +1455,7 @@ SOFTWARE.

Qty.parse = globalParse;

Qty.defineUnit = defineUnit;
Qty.getUnits = getUnits;
Qty.getAliases = getAliases;

Expand Down Expand Up @@ -2011,4 +2046,4 @@ SOFTWARE.

return Qty;

})));
}));
Loading

0 comments on commit a55472f

Please sign in to comment.