@itwin/core-quantity 4.8.0-dev.9 → 4.9.0-dev.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/CHANGELOG.md +36 -1
  2. package/lib/cjs/Constants.js.map +1 -1
  3. package/lib/cjs/Exception.js.map +1 -1
  4. package/lib/cjs/Formatter/Format.d.ts +3 -0
  5. package/lib/cjs/Formatter/Format.d.ts.map +1 -1
  6. package/lib/cjs/Formatter/Format.js +8 -0
  7. package/lib/cjs/Formatter/Format.js.map +1 -1
  8. package/lib/cjs/Formatter/FormatEnums.js.map +1 -1
  9. package/lib/cjs/Formatter/Formatter.js.map +1 -1
  10. package/lib/cjs/Formatter/FormatterSpec.js.map +1 -1
  11. package/lib/cjs/Formatter/Interfaces.d.ts +1 -0
  12. package/lib/cjs/Formatter/Interfaces.d.ts.map +1 -1
  13. package/lib/cjs/Formatter/Interfaces.js.map +1 -1
  14. package/lib/cjs/Interfaces.js.map +1 -1
  15. package/lib/cjs/Parser.d.ts +25 -4
  16. package/lib/cjs/Parser.d.ts.map +1 -1
  17. package/lib/cjs/Parser.js +184 -117
  18. package/lib/cjs/Parser.js.map +1 -1
  19. package/lib/cjs/ParserSpec.js.map +1 -1
  20. package/lib/cjs/Quantity.js.map +1 -1
  21. package/lib/cjs/Unit.js.map +1 -1
  22. package/lib/cjs/core-quantity.js.map +1 -1
  23. package/lib/esm/Constants.js.map +1 -1
  24. package/lib/esm/Exception.js.map +1 -1
  25. package/lib/esm/Formatter/Format.d.ts +3 -0
  26. package/lib/esm/Formatter/Format.d.ts.map +1 -1
  27. package/lib/esm/Formatter/Format.js +8 -0
  28. package/lib/esm/Formatter/Format.js.map +1 -1
  29. package/lib/esm/Formatter/FormatEnums.js.map +1 -1
  30. package/lib/esm/Formatter/Formatter.js.map +1 -1
  31. package/lib/esm/Formatter/FormatterSpec.js.map +1 -1
  32. package/lib/esm/Formatter/Interfaces.d.ts +1 -0
  33. package/lib/esm/Formatter/Interfaces.d.ts.map +1 -1
  34. package/lib/esm/Formatter/Interfaces.js.map +1 -1
  35. package/lib/esm/Interfaces.js.map +1 -1
  36. package/lib/esm/Parser.d.ts +25 -4
  37. package/lib/esm/Parser.d.ts.map +1 -1
  38. package/lib/esm/Parser.js +184 -117
  39. package/lib/esm/Parser.js.map +1 -1
  40. package/lib/esm/ParserSpec.js.map +1 -1
  41. package/lib/esm/Quantity.js.map +1 -1
  42. package/lib/esm/Unit.js.map +1 -1
  43. package/lib/esm/core-quantity.js.map +1 -1
  44. package/package.json +4 -4
@@ -13,7 +13,8 @@ export declare enum ParseError {
13
13
  UnitLabelSuppliedButNotMatched = 3,
14
14
  UnknownUnit = 4,
15
15
  UnableToConvertParseTokensToQuantity = 5,
16
- InvalidParserSpec = 6
16
+ InvalidParserSpec = 6,
17
+ MathematicOperationFoundButIsNotAllowed = 7
17
18
  }
18
19
  /** Parse error result from [[Parser.parseToQuantityValue]] or [[Parser.parseToQuantityValue]].
19
20
  * @beta
@@ -33,6 +34,10 @@ export interface ParsedQuantity {
33
34
  /** The magnitude of the parsed quantity. */
34
35
  value: number;
35
36
  }
37
+ declare enum Operator {
38
+ addition = "+",
39
+ subtraction = "-"
40
+ }
36
41
  /**
37
42
  * Defines Results of parsing a string input by a user into its desired value type
38
43
  * @beta
@@ -42,8 +47,9 @@ export type QuantityParseResult = ParsedQuantity | ParseQuantityError;
42
47
  * @beta
43
48
  */
44
49
  declare class ParseToken {
45
- value: number | string;
46
- constructor(value: string | number);
50
+ value: number | string | Operator;
51
+ isOperator: boolean;
52
+ constructor(value: string | number | Operator);
47
53
  get isString(): boolean;
48
54
  get isNumber(): boolean;
49
55
  }
@@ -62,7 +68,15 @@ export declare class Parser {
62
68
  * @param quantitySpecification The quantity string to ba parsed.
63
69
  */
64
70
  static parseQuantitySpecification(quantitySpecification: string, format: Format): ParseToken[];
71
+ private static isMathematicOperation;
65
72
  private static lookupUnitByLabel;
73
+ /**
74
+ * Get the output unit and all the conversion specs required to parse a given list of tokens.
75
+ */
76
+ private static getRequiredUnitsConversionsToParseTokens;
77
+ /**
78
+ * Get the units information asynchronously, then convert the tokens into quantity using the synchronous tokens -> value.
79
+ */
66
80
  private static createQuantityFromParseTokens;
67
81
  /** Async method to generate a Quantity given a string that represents a quantity value and likely a unit label.
68
82
  * @param inString A string that contains text represent a quantity.
@@ -72,11 +86,18 @@ export declare class Parser {
72
86
  static parseIntoQuantity(inString: string, format: Format, unitsProvider: UnitsProvider, altUnitLabelsProvider?: AlternateUnitLabelsProvider): Promise<QuantityProps>;
73
87
  /** method to get the Unit Conversion given a unit label */
74
88
  private static tryFindUnitConversion;
89
+ /**
90
+ * Get what the unit conversion is for a unitless value.
91
+ */
92
+ private static getDefaultUnitConversion;
93
+ private static getNextTokenPair;
94
+ /**
95
+ * Accumulate the given list of tokens into a single quantity value. Formatting the tokens along the way.
96
+ */
75
97
  private static getQuantityValueFromParseTokens;
76
98
  /** Method to generate a Quantity given a string that represents a quantity value.
77
99
  * @param inString A string that contains text represent a quantity.
78
100
  * @param parserSpec unit label if not explicitly defined by user. Must have matching entry in supplied array of unitsConversions.
79
- * @param defaultValue default value to return if parsing is un successful
80
101
  */
81
102
  static parseQuantityString(inString: string, parserSpec: ParserSpec): QuantityParseResult;
82
103
  /** Method to generate a Quantity given a string that represents a quantity value and likely a unit label.
@@ -1 +1 @@
1
- {"version":3,"file":"Parser.d.ts","sourceRoot":"","sources":["../../src/Parser.ts"],"names":[],"mappings":"AAIA;;GAEG;AAGH,OAAO,EAAE,MAAM,EAAE,MAAM,oBAAoB,CAAC;AAE5C,OAAO,EAAE,2BAA2B,EAAE,kBAAkB,EAAE,aAAa,EAAuB,kBAAkB,EAAE,SAAS,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AACjK,OAAO,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAG1C;;GAEG;AACH,oBAAY,UAAU;IACpB,2BAA2B,IAAI;IAC/B,0BAA0B,IAAA;IAC1B,8BAA8B,IAAA;IAC9B,WAAW,IAAA;IACX,oCAAoC,IAAA;IACpC,iBAAiB,IAAA;CAClB;AAED;;GAEG;AACH,MAAM,WAAW,kBAAkB;IACjC,uDAAuD;IACvD,EAAE,EAAE,KAAK,CAAC;IACV,uDAAuD;IACvD,KAAK,EAAE,UAAU,CAAC;CACnB;AAED;;GAEG;AACH,MAAM,WAAW,cAAc;IAC7B,uDAAuD;IACvD,EAAE,EAAE,IAAI,CAAC;IACT,4CAA4C;IAC5C,KAAK,EAAE,MAAM,CAAC;CACf;AAED;;;GAGG;AACH,MAAM,MAAM,mBAAmB,GAAG,cAAc,GAAG,kBAAkB,CAAC;AAEtE;;GAEG;AACH,cAAM,UAAU;IACP,KAAK,EAAE,MAAM,GAAG,MAAM,CAAC;gBAElB,KAAK,EAAE,MAAM,GAAG,MAAM;IAOlC,IAAW,QAAQ,IAAI,OAAO,CAA2C;IACzE,IAAW,QAAQ,IAAI,OAAO,CAA2C;CAC1E;AA+BD;;GAEG;AACH,qBAAa,MAAM;IACjB,OAAO,CAAC,MAAM,CAAC,IAAI,CAAS;WAEd,gBAAgB,CAAC,IAAI,EAAE,mBAAmB,GAAG,IAAI,IAAI,cAAc;WAInE,YAAY,CAAC,IAAI,EAAE,mBAAmB,GAAG,IAAI,IAAI,kBAAkB;IAIjF,OAAO,CAAC,MAAM,CAAC,0BAA0B;IAoBzC,OAAO,CAAC,MAAM,CAAC,iBAAiB;IAyChC,OAAO,CAAC,MAAM,CAAC,OAAO;IAItB,OAAO,CAAC,MAAM,CAAC,yBAAyB;IAIxC;;OAEG;WACW,0BAA0B,CAAC,qBAAqB,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,UAAU,EAAE;mBA6IhF,iBAAiB;mBA6BjB,6BAA6B;IAkElD;;;;OAIG;WACiB,iBAAiB,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,aAAa,EAAE,aAAa,EAAE,qBAAqB,CAAC,EAAE,2BAA2B,GAAG,OAAO,CAAC,aAAa,CAAC;IAQlL,2DAA2D;IAC3D,OAAO,CAAC,MAAM,CAAC,qBAAqB;IAyCpC,OAAO,CAAC,MAAM,CAAC,+BAA+B;IAmE9C;;;;OAIG;WACW,mBAAmB,CAAC,QAAQ,EAAE,MAAM,EAAE,UAAU,EAAE,UAAU,GAAG,mBAAmB;IAIhG;;;;OAIG;WACW,oBAAoB,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,gBAAgB,EAAE,kBAAkB,EAAE,GAAG,mBAAmB;IA+BjI,+HAA+H;WAC3G,gCAAgC,CAAC,aAAa,EAAE,aAAa,EAAE,OAAO,EAAE,SAAS,EAAE,qBAAqB,CAAC,EAAE,2BAA2B,GAAG,OAAO,CAAC,kBAAkB,EAAE,CAAC;IA4B1L,+HAA+H;WAC3G,yBAAyB,CAAC,aAAa,EAAE,aAAa,EAAE,WAAW,EAAE,MAAM,EAAE,mBAAmB,EAAE,kBAAkB,EAAE,EAAE,qBAAqB,CAAC,EAAE,2BAA2B,GAAG,OAAO,CAAC,kBAAkB,EAAE,CAAC;CAgDhO"}
1
+ {"version":3,"file":"Parser.d.ts","sourceRoot":"","sources":["../../src/Parser.ts"],"names":[],"mappings":"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"}
package/lib/esm/Parser.js CHANGED
@@ -19,18 +19,35 @@ export var ParseError;
19
19
  ParseError[ParseError["UnknownUnit"] = 4] = "UnknownUnit";
20
20
  ParseError[ParseError["UnableToConvertParseTokensToQuantity"] = 5] = "UnableToConvertParseTokensToQuantity";
21
21
  ParseError[ParseError["InvalidParserSpec"] = 6] = "InvalidParserSpec";
22
+ ParseError[ParseError["MathematicOperationFoundButIsNotAllowed"] = 7] = "MathematicOperationFoundButIsNotAllowed";
22
23
  })(ParseError || (ParseError = {}));
24
+ var Operator;
25
+ (function (Operator) {
26
+ Operator["addition"] = "+";
27
+ Operator["subtraction"] = "-";
28
+ })(Operator || (Operator = {}));
29
+ function isOperator(char) {
30
+ if (typeof char === "number") {
31
+ // Convert the charcode to string.
32
+ char = String.fromCharCode(char);
33
+ }
34
+ return Object.values(Operator).includes(char);
35
+ }
23
36
  /** A ParseToken holds either a numeric or string token extracted from a string that represents a quantity value.
24
37
  * @beta
25
38
  */
26
39
  class ParseToken {
27
40
  constructor(value) {
28
- if (typeof value === "string")
41
+ this.isOperator = false;
42
+ if (typeof value === "string") {
29
43
  this.value = value.trim();
30
- else
44
+ this.isOperator = isOperator(this.value);
45
+ }
46
+ else {
31
47
  this.value = value;
48
+ }
32
49
  }
33
- get isString() { return typeof this.value === "string"; }
50
+ get isString() { return !this.isOperator && typeof this.value === "string"; }
34
51
  get isNumber() { return typeof this.value === "number"; }
35
52
  }
36
53
  /** A ScientificToken holds an index and string representing the exponent.
@@ -137,6 +154,7 @@ export class Parser {
137
154
  let processingNumber = false;
138
155
  let wipToken = "";
139
156
  let signToken = "";
157
+ let isStationSeparatorAdded = false;
140
158
  let uomSeparatorToIgnore = 0;
141
159
  let fractionDashCode = 0;
142
160
  const skipCodes = [format.thousandSeparator.charCodeAt(0)];
@@ -222,9 +240,17 @@ export class Parser {
222
240
  }
223
241
  }
224
242
  }
225
- // ignore any codes in skipCodes
226
- if (skipCodes.findIndex((ref) => ref === charCode) !== -1)
243
+ if (format.type === FormatType.Station && charCode === format.stationSeparator.charCodeAt(0)) {
244
+ if (!isStationSeparatorAdded) {
245
+ isStationSeparatorAdded = true;
246
+ continue;
247
+ }
248
+ isStationSeparatorAdded = false;
249
+ }
250
+ else if (skipCodes.findIndex((ref) => ref === charCode) !== -1) {
251
+ // ignore any codes in skipCodes
227
252
  continue;
253
+ }
228
254
  if (signToken.length > 0) {
229
255
  wipToken = signToken + wipToken;
230
256
  signToken = "";
@@ -232,12 +258,24 @@ export class Parser {
232
258
  tokens.push(new ParseToken(parseFloat(wipToken)));
233
259
  wipToken = (i < str.length) ? str[i] : "";
234
260
  processingNumber = false;
261
+ if (wipToken.length === 1 && isOperator(wipToken)) {
262
+ tokens.push(new ParseToken(wipToken)); // Push operator token.
263
+ wipToken = "";
264
+ }
235
265
  }
236
266
  else {
237
267
  // not processing a number
238
- if ((charCode === QuantityConstants.CHAR_PLUS || charCode === QuantityConstants.CHAR_MINUS)) {
239
- if (0 === tokens.length) // sign token only needed for left most value
240
- signToken = str[i];
268
+ if (isOperator(charCode)) {
269
+ if (wipToken.length > 0) {
270
+ // There is a token is progress, process it now, before adding the new operator token.
271
+ tokens.push(new ParseToken(wipToken));
272
+ wipToken = "";
273
+ }
274
+ tokens.push(new ParseToken(str[i])); // Push an Operator Token in the list.
275
+ continue;
276
+ }
277
+ if (wipToken.length === 0 && charCode === QuantityConstants.CHAR_SPACE) {
278
+ // Dont add space when the wip token is empty.
241
279
  continue;
242
280
  }
243
281
  wipToken = wipToken.concat(str[i]);
@@ -258,6 +296,17 @@ export class Parser {
258
296
  }
259
297
  return tokens;
260
298
  }
299
+ static isMathematicOperation(tokens) {
300
+ if (tokens.length > 1) {
301
+ // The loop starts at one because the first token can be a operator without it being maths. Ex: "-5FT"
302
+ for (let i = 1; i < tokens.length; i++) {
303
+ if (tokens[i].isOperator)
304
+ // Operator found, it's a math operation.
305
+ return true;
306
+ }
307
+ }
308
+ return false;
309
+ }
261
310
  static async lookupUnitByLabel(unitLabel, format, unitsProvider, altUnitLabelsProvider) {
262
311
  const defaultUnit = format.units && format.units.length > 0 ? format.units[0][0] : undefined;
263
312
  const labelToFind = unitLabel.toLowerCase();
@@ -282,69 +331,53 @@ export class Parser {
282
331
  foundUnit = await unitsProvider.findUnit(unitLabel, defaultUnit ? defaultUnit.phenomenon : undefined);
283
332
  return foundUnit;
284
333
  }
285
- static async createQuantityFromParseTokens(tokens, format, unitsProvider, altUnitLabelsProvider) {
286
- let defaultUnit = format.units && format.units.length > 0 ? format.units[0][0] : undefined;
287
- // common case where single value is supplied
288
- if (tokens.length === 1) {
289
- if (tokens[0].isNumber) {
290
- return new Quantity(defaultUnit, tokens[0].value);
334
+ /**
335
+ * Get the output unit and all the conversion specs required to parse a given list of tokens.
336
+ */
337
+ static async getRequiredUnitsConversionsToParseTokens(tokens, format, unitsProvider, altUnitLabelsProvider) {
338
+ let outUnit = (format.units && format.units.length > 0 ? format.units[0][0] : undefined);
339
+ const unitConversions = [];
340
+ const uniqueUnitLabels = [...new Set(tokens.filter((token) => token.isString).map((token) => token.value))];
341
+ for (const label of uniqueUnitLabels) {
342
+ const unitProps = await this.lookupUnitByLabel(label, format, unitsProvider, altUnitLabelsProvider);
343
+ if (!outUnit) {
344
+ // No default unit, assume that the first unit found is the desired output unit.
345
+ outUnit = unitProps;
291
346
  }
292
- else {
293
- const unit = await this.lookupUnitByLabel(tokens[0].value, format, unitsProvider, altUnitLabelsProvider);
294
- return new Quantity(unit);
347
+ let spec = unitConversions.find((specB) => specB.name === unitProps.name);
348
+ if (spec) {
349
+ // Already in the list, just add the label.
350
+ spec.parseLabels?.push(label.toLocaleLowerCase());
295
351
  }
296
- }
297
- // common case where single value and single label are supplied
298
- if (tokens.length === 2) {
299
- // unit specification comes before value (like currency)
300
- if (tokens[1].isNumber && tokens[0].isString) {
301
- tokens = [tokens[1], tokens[0]];
302
- }
303
- if (tokens[0].isNumber && tokens[1].isString) {
304
- const unit = await this.lookupUnitByLabel(tokens[1].value, format, unitsProvider, altUnitLabelsProvider);
305
- if (undefined === defaultUnit)
306
- defaultUnit = unit;
307
- if (defaultUnit && defaultUnit.name === unit.name) {
308
- return new Quantity(defaultUnit, tokens[0].value);
309
- }
310
- else if (defaultUnit) {
311
- const conversion = await unitsProvider.getConversion(unit, defaultUnit);
312
- const mag = ((tokens[0].value * conversion.factor)) + conversion.offset;
313
- return new Quantity(defaultUnit, mag);
352
+ else {
353
+ // Add new conversion to the list.
354
+ const conversion = await unitsProvider.getConversion(unitProps, outUnit);
355
+ if (conversion) {
356
+ spec = {
357
+ conversion,
358
+ label: unitProps.label,
359
+ system: unitProps.system,
360
+ name: unitProps.name,
361
+ parseLabels: [label.toLocaleLowerCase()],
362
+ };
363
+ unitConversions.push(spec);
314
364
  }
315
365
  }
316
366
  }
317
- // common case where there are multiple value/label pairs
318
- if (tokens.length % 2 === 0) {
319
- let mag = 0.0;
320
- for (let i = 0; i < tokens.length; i = i + 2) {
321
- if (tokens[i].isNumber && tokens[i + 1].isString) {
322
- const value = tokens[i].value;
323
- const unit = await this.lookupUnitByLabel(tokens[i + 1].value, format, unitsProvider, altUnitLabelsProvider);
324
- if (undefined === defaultUnit)
325
- defaultUnit = unit;
326
- if (0 === i) {
327
- if (defaultUnit.name === unit.name)
328
- mag = value;
329
- else {
330
- const conversion = await unitsProvider.getConversion(unit, defaultUnit);
331
- mag = ((value * conversion.factor)) + conversion.offset;
332
- }
333
- }
334
- else {
335
- if (defaultUnit) {
336
- const conversion = await unitsProvider.getConversion(unit, defaultUnit);
337
- if (mag < 0.0)
338
- mag = mag - ((value * conversion.factor)) + conversion.offset;
339
- else
340
- mag = mag + ((value * conversion.factor)) + conversion.offset;
341
- }
342
- }
343
- }
367
+ return { outUnit, specs: unitConversions };
368
+ }
369
+ /**
370
+ * Get the units information asynchronously, then convert the tokens into quantity using the synchronous tokens -> value.
371
+ */
372
+ static async createQuantityFromParseTokens(tokens, format, unitsProvider, altUnitLabelsProvider) {
373
+ const unitConversionInfos = await this.getRequiredUnitsConversionsToParseTokens(tokens, format, unitsProvider, altUnitLabelsProvider);
374
+ if (unitConversionInfos.outUnit) {
375
+ const value = Parser.getQuantityValueFromParseTokens(tokens, format, unitConversionInfos.specs, await unitsProvider.getConversion(unitConversionInfos.outUnit, unitConversionInfos.outUnit));
376
+ if (value.ok) {
377
+ return new Quantity(unitConversionInfos.outUnit, value.value);
344
378
  }
345
- return new Quantity(defaultUnit, mag);
346
379
  }
347
- return new Quantity(defaultUnit);
380
+ return new Quantity();
348
381
  }
349
382
  /** Async method to generate a Quantity given a string that represents a quantity value and likely a unit label.
350
383
  * @param inString A string that contains text represent a quantity.
@@ -353,7 +386,7 @@ export class Parser {
353
386
  */
354
387
  static async parseIntoQuantity(inString, format, unitsProvider, altUnitLabelsProvider) {
355
388
  const tokens = Parser.parseQuantitySpecification(inString, format);
356
- if (tokens.length === 0)
389
+ if (tokens.length === 0 || (!format.allowMathematicOperations && Parser.isMathematicOperation(tokens)))
357
390
  return new Quantity();
358
391
  return Parser.createQuantityFromParseTokens(tokens, format, unitsProvider, altUnitLabelsProvider);
359
392
  }
@@ -396,75 +429,106 @@ export class Parser {
396
429
  }
397
430
  return undefined;
398
431
  }
399
- static getQuantityValueFromParseTokens(tokens, format, unitsConversions) {
400
- const defaultUnit = format.units && format.units.length > 0 ? format.units[0][0] : undefined;
401
- // common case where single value is supplied
402
- if (tokens.length === 1) {
403
- if (tokens[0].isNumber) {
404
- if (defaultUnit) {
405
- const conversion = Parser.tryFindUnitConversion(defaultUnit.label, unitsConversions, defaultUnit);
406
- if (conversion) {
407
- const value = tokens[0].value * conversion.factor + conversion.offset;
408
- return { ok: true, value };
409
- }
410
- }
411
- else {
412
- // if no conversion or no defaultUnit, just return parsed number
413
- return { ok: true, value: tokens[0].value };
414
- }
432
+ /**
433
+ * Get what the unit conversion is for a unitless value.
434
+ */
435
+ static getDefaultUnitConversion(tokens, unitsConversions, defaultUnit) {
436
+ let unitConversion = defaultUnit ? Parser.tryFindUnitConversion(defaultUnit.label, unitsConversions, defaultUnit) : undefined;
437
+ if (!unitConversion) {
438
+ // No default unit conversion, take the first valid unit.
439
+ const uniqueUnitLabels = [...new Set(tokens.filter((token) => token.isString).map((token) => token.value))];
440
+ for (const label of uniqueUnitLabels) {
441
+ unitConversion = Parser.tryFindUnitConversion(label, unitsConversions, defaultUnit);
442
+ if (unitConversion !== undefined)
443
+ return unitConversion;
444
+ }
445
+ }
446
+ return unitConversion;
447
+ }
448
+ // Get the next token pair to parse into a quantity.
449
+ static getNextTokenPair(index, tokens) {
450
+ if (index >= tokens.length)
451
+ return;
452
+ // 6 possible combination of token pair.
453
+ // Stringified to ease comparison later.
454
+ const validCombinations = [
455
+ JSON.stringify(["string"]), // ['FT']
456
+ JSON.stringify(["string", "number"]), // ['$', 5] unit specification comes before value (like currency)
457
+ JSON.stringify(["number"]), // [5]
458
+ JSON.stringify(["number", "string"]), // [5, 'FT']
459
+ JSON.stringify(["operator", "number"]), // ['-', 5]
460
+ JSON.stringify(["operator", "number", "string"]), // ['-', 5, 'FT']
461
+ ];
462
+ // Push up to 3 tokens in the list, if the length allows it.
463
+ const maxNbrTokensInThePair = Math.min(tokens.length - index, 3);
464
+ const tokenPair = tokens.slice(index, index + maxNbrTokensInThePair);
465
+ const currentCombination = tokenPair.map((token) => token.isOperator ? "operator" : (token.isNumber ? "number" : "string"));
466
+ // Check if the token pair is valid. If not, try again by removing the last token util empty.
467
+ // Ex: ['5', 'FT', '7'] invalid => ['5', 'FT'] valid returned
468
+ for (let i = currentCombination.length - 1; i >= 0; i--) {
469
+ if (validCombinations.includes(JSON.stringify(currentCombination))) {
470
+ break;
415
471
  }
416
472
  else {
417
- // only the unit label was specified so assume magnitude of 1
418
- const conversion = Parser.tryFindUnitConversion(tokens[0].value, unitsConversions, defaultUnit);
419
- if (undefined !== conversion)
420
- return { ok: true, value: conversion.factor + conversion.offset };
421
- else
422
- return { ok: false, error: ParseError.NoValueOrUnitFoundInString };
473
+ currentCombination.pop();
474
+ tokenPair.pop();
423
475
  }
424
476
  }
425
- // common case where single value and single label are supplied
426
- if (tokens.length === 2) {
477
+ return tokenPair.length > 0 ? tokenPair : undefined;
478
+ }
479
+ /**
480
+ * Accumulate the given list of tokens into a single quantity value. Formatting the tokens along the way.
481
+ */
482
+ static getQuantityValueFromParseTokens(tokens, format, unitsConversions, defaultUnitConversion) {
483
+ const defaultUnit = format.units && format.units.length > 0 ? format.units[0][0] : undefined;
484
+ defaultUnitConversion = defaultUnitConversion ? defaultUnitConversion : Parser.getDefaultUnitConversion(tokens, unitsConversions, defaultUnit);
485
+ let tokenPair;
486
+ let increment = 1;
487
+ let mag = 0.0;
488
+ // The sign is saved outside from the loop for cases like this. '-1m 50cm 10mm + 2m 30cm 40mm' => -1.51m + 2.34m
489
+ let sign = 1;
490
+ for (let i = 0; i < tokens.length; i = i + increment) {
491
+ tokenPair = this.getNextTokenPair(i, tokens);
492
+ if (!tokenPair || tokenPair.length === 0) {
493
+ return { ok: false, error: ParseError.UnableToConvertParseTokensToQuantity };
494
+ }
495
+ increment = tokenPair.length;
496
+ // Keep the sign so its applied to the next tokens.
497
+ if (tokenPair[0].isOperator) {
498
+ sign = tokenPair[0].value === Operator.addition ? 1 : -1;
499
+ tokenPair.shift();
500
+ }
427
501
  // unit specification comes before value (like currency)
428
- if (tokens[1].isNumber && tokens[0].isString) {
429
- tokens = [tokens[1], tokens[0]];
502
+ if (tokenPair.length === 2 && tokenPair[0].isString) {
503
+ // Invert it so the currency sign comes second.
504
+ tokenPair = [tokenPair[1], tokenPair[0]];
430
505
  }
431
- if (tokens[0].isNumber && tokens[1].isString) {
432
- let conversion = Parser.tryFindUnitConversion(tokens[1].value, unitsConversions, defaultUnit);
433
- // if no conversion, ignore value in second token. If we have defaultUnit, use it.
434
- if (!conversion && defaultUnit) {
435
- conversion = Parser.tryFindUnitConversion(defaultUnit.label, unitsConversions, defaultUnit);
506
+ if (tokenPair[0].isNumber) {
507
+ let value = sign * tokenPair[0].value;
508
+ let conversion;
509
+ if (tokenPair.length === 2 && tokenPair[1].isString) {
510
+ conversion = Parser.tryFindUnitConversion(tokenPair[1].value, unitsConversions, defaultUnit);
436
511
  }
512
+ conversion = conversion ? conversion : defaultUnitConversion;
437
513
  if (conversion) {
438
- const value = tokens[0].value * conversion.factor + conversion.offset;
439
- return { ok: true, value };
514
+ value = (value * conversion.factor) + conversion.offset;
440
515
  }
441
- // if no conversion, just return parsed number and ignore value in second token
442
- return { ok: true, value: tokens[0].value };
516
+ mag = mag + value;
443
517
  }
444
- }
445
- // common case where there are multiple value/label pairs
446
- if (tokens.length % 2 === 0) {
447
- let mag = 0.0;
448
- for (let i = 0; i < tokens.length; i = i + 2) {
449
- if (tokens[i].isNumber && tokens[i + 1].isString) {
450
- const value = tokens[i].value;
451
- const conversion = Parser.tryFindUnitConversion(tokens[i + 1].value, unitsConversions, defaultUnit);
452
- if (conversion) {
453
- if (mag < 0.0)
454
- mag = mag - ((value * conversion.factor)) + conversion.offset;
455
- else
456
- mag = mag + ((value * conversion.factor)) + conversion.offset;
457
- }
518
+ else {
519
+ // only the unit label was specified so assume magnitude of 0
520
+ const conversion = Parser.tryFindUnitConversion(tokenPair[0].value, unitsConversions, defaultUnit);
521
+ if (conversion === undefined) {
522
+ // Unknown unit label
523
+ return { ok: false, error: ParseError.NoValueOrUnitFoundInString };
458
524
  }
459
525
  }
460
- return { ok: true, value: mag };
461
526
  }
462
- return { ok: false, error: ParseError.UnableToConvertParseTokensToQuantity };
527
+ return { ok: true, value: mag };
463
528
  }
464
529
  /** Method to generate a Quantity given a string that represents a quantity value.
465
530
  * @param inString A string that contains text represent a quantity.
466
531
  * @param parserSpec unit label if not explicitly defined by user. Must have matching entry in supplied array of unitsConversions.
467
- * @param defaultValue default value to return if parsing is un successful
468
532
  */
469
533
  static parseQuantityString(inString, parserSpec) {
470
534
  return Parser.parseToQuantityValue(inString, parserSpec.format, parserSpec.unitConversions);
@@ -490,6 +554,9 @@ export class Parser {
490
554
  const tokens = Parser.parseQuantitySpecification(inString, format);
491
555
  if (tokens.length === 0)
492
556
  return { ok: false, error: ParseError.UnableToGenerateParseTokens };
557
+ if (!format.allowMathematicOperations && Parser.isMathematicOperation(tokens)) {
558
+ return { ok: false, error: ParseError.MathematicOperationFoundButIsNotAllowed };
559
+ }
493
560
  if (Parser._log) {
494
561
  // eslint-disable-next-line no-console
495
562
  console.log(`Parse tokens`);