@neutrium/quantity
Version:
The NeutriumJS unit conversion module.
243 lines (198 loc) • 7 kB
text/typescript
import { Decimal } from "@neutrium/math";
import { Parser } from './Parser.js';
import { QuantityDefinition } from "../QuantityDefinition.js";
import { UnitTokenManager } from '../UnitTokenManager.js'
export class RegexQtyParser implements Parser<QuantityDefinition>
{
private static parsedUnitsCache = {};
private tokenMapper: UnitTokenManager;
private static UNITY = "<1>";
// REGEX - Numbers
private static readonly SIGN = "[+-]";
private static readonly INTEGER = "\\d+";
private static readonly SIGNED_INTEGER = RegexQtyParser.SIGN + "?" +RegexQtyParser.INTEGER;
private static readonly FRACTION = "\\." + RegexQtyParser.INTEGER;
private static readonly FLOAT = "(?:" + RegexQtyParser.INTEGER + "(?:" + RegexQtyParser.FRACTION + ")?" + ")" + "|" + "(?:" + RegexQtyParser.FRACTION + ")";
private static readonly EXPONENT = "[Ee]" + RegexQtyParser.SIGNED_INTEGER;
private static readonly SCI_NUMBER = "(?:" + RegexQtyParser.FLOAT + ")(?:" + RegexQtyParser.EXPONENT + ")?";
// Note below could be replaced by "[-+]?[0-9]*\.?[0-9]+([eE][-+]?[0-9]+)?"
private static readonly SIGNED_NUMBER = RegexQtyParser.SIGN + "?\\s*" + RegexQtyParser.SCI_NUMBER;
// REGEX - Quantity strings e.g. "2.5 m/s^2"
private static readonly QTY_STRING = "(" + RegexQtyParser.SIGNED_NUMBER + ")?" + "\\s*([^/]*)(?:\/(.+))?";
private static readonly QTY_STRING_REGEX = new RegExp("^" + RegexQtyParser.QTY_STRING + "$");
private static readonly POWER_OP = "\\^|\\*{2}";
private static readonly TOP_REGEX = new RegExp("([^ \\*.]+?)(?:" + RegexQtyParser.POWER_OP + ")?(-?\\d+)(?![A-z])");
private static readonly BOTTOM_REGEX = new RegExp("([^ \\*.]+?)(?:" + RegexQtyParser.POWER_OP + ")?(\\d+)");
private static readonly BOUNDARY_REGEX = "\\b|\\s|$";
// REGEX - defined during module initalisation
private static PREFIX_REGEX: string;
private static UNIT_REGEX: string;
private static UNIT_MATCH: string;
private static UNIT_MATCH_REGEX: RegExp;
private static UNIT_TEST_REGEX: RegExp;
constructor()
{
this.tokenMapper = UnitTokenManager.instance;
this.initialize();
}
initialize()
{
// Look at preprocessing the below
// Each prefix definition ordered by length eg E|EI|...
RegexQtyParser.PREFIX_REGEX = Object.keys(this.tokenMapper.getMap('prefix')).sort(function (a, b) {
return b.length - a.length;
}).join("|");
RegexQtyParser.UNIT_REGEX = Object.keys(this.tokenMapper.getMap('unit')).sort(function (a, b) {
return b.length - a.length;
}).join("|").replace(/(\(|\))/g, '\\$1');
// Minimal boundary regex to support units with Unicode characters. \b only works for ASCII
RegexQtyParser.UNIT_MATCH = "(" + RegexQtyParser.PREFIX_REGEX + ")??(" + RegexQtyParser.UNIT_REGEX + ")(?:" + RegexQtyParser.BOUNDARY_REGEX + ")";
RegexQtyParser.UNIT_MATCH_REGEX = new RegExp(RegexQtyParser.UNIT_MATCH, "g"); // g flag for multiple occurences
RegexQtyParser.UNIT_TEST_REGEX = new RegExp("^\\s*(" + RegexQtyParser.UNIT_MATCH + "\\s*(\\.?|\\*?)\\s*)+$"); // Try also to get . as in kg.s as kg*s
}
// Parse a string into a unit object.
// Typical formats like :
// "5.6 kg*m/s^2"
// "5.6 kg*m*s^-2"
// "5.6 kilogram*meter*second^-2"
// "2.2 kPa"
// "37 degC"
// "1" -- creates a unitless constant with value 1
// "GPa" -- creates a unit with scalar 1 with units 'GPa'
// 6'4" -- recognized as 6 feet + 4 inches
// 8 lbs 8 oz -- recognized as 8 lbs + 8 ounces
//
public parse(val: string) : QuantityDefinition
{
let output: QuantityDefinition = {
scalar: new Decimal(1),
numerator: [RegexQtyParser.UNITY],
denominator: [RegexQtyParser.UNITY]
}
val = (val + '').trim();
if (val.length === 0)
{
throw new Error("Unit not recognized");
}
let result: (string[] | null) = RegexQtyParser.QTY_STRING_REGEX.exec(val);
if (!result)
{
throw new Error(val + ": Quantity not recognized");
}
let scalarMatch = result[1];
if (scalarMatch)
{
// Allow whitespaces between sign and scalar for loose parsing
scalarMatch = scalarMatch.replace(/\s/g, "");
output.scalar = new Decimal(scalarMatch);
}
let top = result[2],
bottom: string = result[3],
n, x, nx: string;
while ((result = RegexQtyParser.TOP_REGEX.exec(top)))
{
n = parseFloat(result[2]);
if (isNaN(n))
{
// Prevents infinite loops
throw new Error("Unit exponent is not a number");
}
// Disallow unrecognized unit even if exponent is 0
if (n === 0 && !RegexQtyParser.UNIT_TEST_REGEX.test(result[1]))
{
throw new Error("Unit not recognized");
}
x = result[1] + " ";
nx = "";
for (let i = 0; i < Math.abs(n); i++)
{
nx += x;
}
if (n >= 0)
{
top = top.replace(result[0], nx);
}
else
{
bottom = bottom ? bottom + nx : nx;
top = top.replace(result[0], "");
}
}
while ((result = RegexQtyParser.BOTTOM_REGEX.exec(bottom)))
{
n = parseFloat(result[2]);
if (isNaN(n))
{
// Prevents infinite loops
throw new Error("Unit exponent is not a number");
}
// Disallow unrecognized unit even if exponent is 0
if (n === 0 && !RegexQtyParser.UNIT_TEST_REGEX.test(result[1]))
{
throw new Error("Unit not recognized");
}
x = result[1] + " ";
nx = "";
for (let j = 0; j < n; j++)
{
nx += x;
}
bottom = bottom.replace(result[0], nx);
}
if (top)
{
output.numerator = this.parseUnits(top.trim());
}
if (bottom)
{
output.denominator = this.parseUnits(bottom.trim());
}
return output;
}
//
// Parses and converts units string to normalized array of unit tokens.
// Result is cached to speed up next calls.
//
// @param {string} units Units string
// @returns {string[]} Array of normalized units
//
// @example
// // Returns ["<second>", "<meter>", "<second>"]
// parseUnits("s m s");
//
private parseUnits(units: string) : string[]
{
let cacheKey = units,
cached = RegexQtyParser.parsedUnitsCache[units],
unitMatch,
normalizedUnits : (string[][] | string[]) = [];
if (cached)
{
return cached;
}
// Strip out
units = units.replace(/(\.|\*)/g, ' ');
// Scan
if (!RegexQtyParser.UNIT_TEST_REGEX.test(units))
{
throw new Error("Unit not recognized");
}
while ((unitMatch = RegexQtyParser.UNIT_MATCH_REGEX.exec(units)))
{
normalizedUnits.push(unitMatch.slice(1));
}
const parser : RegexQtyParser = this;
normalizedUnits = <string[][]>normalizedUnits.map(function (item) : string[] {
// Convert multiple forms of a given unit to mutliple occurances of the same token
const result = [parser.tokenMapper.getPrefixToken(item[0]), parser.tokenMapper.getUnitToken(item[1])];
// Filter null values and empty strings
return result.filter((x: any) : x is string => x)
});
// Flatten and remove null elements
normalizedUnits = normalizedUnits.reduce(function (a, b) {
return a.concat(b);
}, []);
RegexQtyParser.parsedUnitsCache[cacheKey] = normalizedUnits;
return normalizedUnits;
}
}