@itwin/core-quantity 4.8.0-dev.4 → 4.8.0-dev.40

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.
package/lib/cjs/Parser.js CHANGED
@@ -22,18 +22,35 @@ var ParseError;
22
22
  ParseError[ParseError["UnknownUnit"] = 4] = "UnknownUnit";
23
23
  ParseError[ParseError["UnableToConvertParseTokensToQuantity"] = 5] = "UnableToConvertParseTokensToQuantity";
24
24
  ParseError[ParseError["InvalidParserSpec"] = 6] = "InvalidParserSpec";
25
+ ParseError[ParseError["MathematicOperationFoundButIsNotAllowed"] = 7] = "MathematicOperationFoundButIsNotAllowed";
25
26
  })(ParseError || (exports.ParseError = ParseError = {}));
27
+ var Operator;
28
+ (function (Operator) {
29
+ Operator["addition"] = "+";
30
+ Operator["subtraction"] = "-";
31
+ })(Operator || (Operator = {}));
32
+ function isOperator(char) {
33
+ if (typeof char === "number") {
34
+ // Convert the charcode to string.
35
+ char = String.fromCharCode(char);
36
+ }
37
+ return Object.values(Operator).includes(char);
38
+ }
26
39
  /** A ParseToken holds either a numeric or string token extracted from a string that represents a quantity value.
27
40
  * @beta
28
41
  */
29
42
  class ParseToken {
30
43
  constructor(value) {
31
- if (typeof value === "string")
44
+ this.isOperator = false;
45
+ if (typeof value === "string") {
32
46
  this.value = value.trim();
33
- else
47
+ this.isOperator = isOperator(this.value);
48
+ }
49
+ else {
34
50
  this.value = value;
51
+ }
35
52
  }
36
- get isString() { return typeof this.value === "string"; }
53
+ get isString() { return !this.isOperator && typeof this.value === "string"; }
37
54
  get isNumber() { return typeof this.value === "number"; }
38
55
  }
39
56
  /** A ScientificToken holds an index and string representing the exponent.
@@ -140,6 +157,7 @@ class Parser {
140
157
  let processingNumber = false;
141
158
  let wipToken = "";
142
159
  let signToken = "";
160
+ let isStationSeparatorAdded = false;
143
161
  let uomSeparatorToIgnore = 0;
144
162
  let fractionDashCode = 0;
145
163
  const skipCodes = [format.thousandSeparator.charCodeAt(0)];
@@ -225,9 +243,17 @@ class Parser {
225
243
  }
226
244
  }
227
245
  }
228
- // ignore any codes in skipCodes
229
- if (skipCodes.findIndex((ref) => ref === charCode) !== -1)
246
+ if (format.type === FormatEnums_1.FormatType.Station && charCode === format.stationSeparator.charCodeAt(0)) {
247
+ if (!isStationSeparatorAdded) {
248
+ isStationSeparatorAdded = true;
249
+ continue;
250
+ }
251
+ isStationSeparatorAdded = false;
252
+ }
253
+ else if (skipCodes.findIndex((ref) => ref === charCode) !== -1) {
254
+ // ignore any codes in skipCodes
230
255
  continue;
256
+ }
231
257
  if (signToken.length > 0) {
232
258
  wipToken = signToken + wipToken;
233
259
  signToken = "";
@@ -235,12 +261,24 @@ class Parser {
235
261
  tokens.push(new ParseToken(parseFloat(wipToken)));
236
262
  wipToken = (i < str.length) ? str[i] : "";
237
263
  processingNumber = false;
264
+ if (wipToken.length === 1 && isOperator(wipToken)) {
265
+ tokens.push(new ParseToken(wipToken)); // Push operator token.
266
+ wipToken = "";
267
+ }
238
268
  }
239
269
  else {
240
270
  // not processing a number
241
- if ((charCode === Constants_1.QuantityConstants.CHAR_PLUS || charCode === Constants_1.QuantityConstants.CHAR_MINUS)) {
242
- if (0 === tokens.length) // sign token only needed for left most value
243
- signToken = str[i];
271
+ if (isOperator(charCode)) {
272
+ if (wipToken.length > 0) {
273
+ // There is a token is progress, process it now, before adding the new operator token.
274
+ tokens.push(new ParseToken(wipToken));
275
+ wipToken = "";
276
+ }
277
+ tokens.push(new ParseToken(str[i])); // Push an Operator Token in the list.
278
+ continue;
279
+ }
280
+ if (wipToken.length === 0 && charCode === Constants_1.QuantityConstants.CHAR_SPACE) {
281
+ // Dont add space when the wip token is empty.
244
282
  continue;
245
283
  }
246
284
  wipToken = wipToken.concat(str[i]);
@@ -261,6 +299,17 @@ class Parser {
261
299
  }
262
300
  return tokens;
263
301
  }
302
+ static isMathematicOperation(tokens) {
303
+ if (tokens.length > 1) {
304
+ // The loop starts at one because the first token can be a operator without it being maths. Ex: "-5FT"
305
+ for (let i = 1; i < tokens.length; i++) {
306
+ if (tokens[i].isOperator)
307
+ // Operator found, it's a math operation.
308
+ return true;
309
+ }
310
+ }
311
+ return false;
312
+ }
264
313
  static async lookupUnitByLabel(unitLabel, format, unitsProvider, altUnitLabelsProvider) {
265
314
  const defaultUnit = format.units && format.units.length > 0 ? format.units[0][0] : undefined;
266
315
  const labelToFind = unitLabel.toLowerCase();
@@ -285,69 +334,53 @@ class Parser {
285
334
  foundUnit = await unitsProvider.findUnit(unitLabel, defaultUnit ? defaultUnit.phenomenon : undefined);
286
335
  return foundUnit;
287
336
  }
288
- static async createQuantityFromParseTokens(tokens, format, unitsProvider, altUnitLabelsProvider) {
289
- let defaultUnit = format.units && format.units.length > 0 ? format.units[0][0] : undefined;
290
- // common case where single value is supplied
291
- if (tokens.length === 1) {
292
- if (tokens[0].isNumber) {
293
- return new Quantity_1.Quantity(defaultUnit, tokens[0].value);
337
+ /**
338
+ * Get the output unit and all the conversion specs required to parse a given list of tokens.
339
+ */
340
+ static async getRequiredUnitsConversionsToParseTokens(tokens, format, unitsProvider, altUnitLabelsProvider) {
341
+ let outUnit = (format.units && format.units.length > 0 ? format.units[0][0] : undefined);
342
+ const unitConversions = [];
343
+ const uniqueUnitLabels = [...new Set(tokens.filter((token) => token.isString).map((token) => token.value))];
344
+ for (const label of uniqueUnitLabels) {
345
+ const unitProps = await this.lookupUnitByLabel(label, format, unitsProvider, altUnitLabelsProvider);
346
+ if (!outUnit) {
347
+ // No default unit, assume that the first unit found is the desired output unit.
348
+ outUnit = unitProps;
294
349
  }
295
- else {
296
- const unit = await this.lookupUnitByLabel(tokens[0].value, format, unitsProvider, altUnitLabelsProvider);
297
- return new Quantity_1.Quantity(unit);
350
+ let spec = unitConversions.find((specB) => specB.name === unitProps.name);
351
+ if (spec) {
352
+ // Already in the list, just add the label.
353
+ spec.parseLabels?.push(label.toLocaleLowerCase());
298
354
  }
299
- }
300
- // common case where single value and single label are supplied
301
- if (tokens.length === 2) {
302
- // unit specification comes before value (like currency)
303
- if (tokens[1].isNumber && tokens[0].isString) {
304
- tokens = [tokens[1], tokens[0]];
305
- }
306
- if (tokens[0].isNumber && tokens[1].isString) {
307
- const unit = await this.lookupUnitByLabel(tokens[1].value, format, unitsProvider, altUnitLabelsProvider);
308
- if (undefined === defaultUnit)
309
- defaultUnit = unit;
310
- if (defaultUnit && defaultUnit.name === unit.name) {
311
- return new Quantity_1.Quantity(defaultUnit, tokens[0].value);
312
- }
313
- else if (defaultUnit) {
314
- const conversion = await unitsProvider.getConversion(unit, defaultUnit);
315
- const mag = ((tokens[0].value * conversion.factor)) + conversion.offset;
316
- return new Quantity_1.Quantity(defaultUnit, mag);
355
+ else {
356
+ // Add new conversion to the list.
357
+ const conversion = await unitsProvider.getConversion(unitProps, outUnit);
358
+ if (conversion) {
359
+ spec = {
360
+ conversion,
361
+ label: unitProps.label,
362
+ system: unitProps.system,
363
+ name: unitProps.name,
364
+ parseLabels: [label.toLocaleLowerCase()],
365
+ };
366
+ unitConversions.push(spec);
317
367
  }
318
368
  }
319
369
  }
320
- // common case where there are multiple value/label pairs
321
- if (tokens.length % 2 === 0) {
322
- let mag = 0.0;
323
- for (let i = 0; i < tokens.length; i = i + 2) {
324
- if (tokens[i].isNumber && tokens[i + 1].isString) {
325
- const value = tokens[i].value;
326
- const unit = await this.lookupUnitByLabel(tokens[i + 1].value, format, unitsProvider, altUnitLabelsProvider);
327
- if (undefined === defaultUnit)
328
- defaultUnit = unit;
329
- if (0 === i) {
330
- if (defaultUnit.name === unit.name)
331
- mag = value;
332
- else {
333
- const conversion = await unitsProvider.getConversion(unit, defaultUnit);
334
- mag = ((value * conversion.factor)) + conversion.offset;
335
- }
336
- }
337
- else {
338
- if (defaultUnit) {
339
- const conversion = await unitsProvider.getConversion(unit, defaultUnit);
340
- if (mag < 0.0)
341
- mag = mag - ((value * conversion.factor)) + conversion.offset;
342
- else
343
- mag = mag + ((value * conversion.factor)) + conversion.offset;
344
- }
345
- }
346
- }
370
+ return { outUnit, specs: unitConversions };
371
+ }
372
+ /**
373
+ * Get the units information asynchronously, then convert the tokens into quantity using the synchronous tokens -> value.
374
+ */
375
+ static async createQuantityFromParseTokens(tokens, format, unitsProvider, altUnitLabelsProvider) {
376
+ const unitConversionInfos = await this.getRequiredUnitsConversionsToParseTokens(tokens, format, unitsProvider, altUnitLabelsProvider);
377
+ if (unitConversionInfos.outUnit) {
378
+ const value = Parser.getQuantityValueFromParseTokens(tokens, format, unitConversionInfos.specs, await unitsProvider.getConversion(unitConversionInfos.outUnit, unitConversionInfos.outUnit));
379
+ if (value.ok) {
380
+ return new Quantity_1.Quantity(unitConversionInfos.outUnit, value.value);
347
381
  }
348
- return new Quantity_1.Quantity(defaultUnit, mag);
349
382
  }
350
- return new Quantity_1.Quantity(defaultUnit);
383
+ return new Quantity_1.Quantity();
351
384
  }
352
385
  /** Async method to generate a Quantity given a string that represents a quantity value and likely a unit label.
353
386
  * @param inString A string that contains text represent a quantity.
@@ -356,7 +389,7 @@ class Parser {
356
389
  */
357
390
  static async parseIntoQuantity(inString, format, unitsProvider, altUnitLabelsProvider) {
358
391
  const tokens = Parser.parseQuantitySpecification(inString, format);
359
- if (tokens.length === 0)
392
+ if (tokens.length === 0 || (!format.allowMathematicOperations && Parser.isMathematicOperation(tokens)))
360
393
  return new Quantity_1.Quantity();
361
394
  return Parser.createQuantityFromParseTokens(tokens, format, unitsProvider, altUnitLabelsProvider);
362
395
  }
@@ -399,75 +432,106 @@ class Parser {
399
432
  }
400
433
  return undefined;
401
434
  }
402
- static getQuantityValueFromParseTokens(tokens, format, unitsConversions) {
403
- const defaultUnit = format.units && format.units.length > 0 ? format.units[0][0] : undefined;
404
- // common case where single value is supplied
405
- if (tokens.length === 1) {
406
- if (tokens[0].isNumber) {
407
- if (defaultUnit) {
408
- const conversion = Parser.tryFindUnitConversion(defaultUnit.label, unitsConversions, defaultUnit);
409
- if (conversion) {
410
- const value = tokens[0].value * conversion.factor + conversion.offset;
411
- return { ok: true, value };
412
- }
413
- }
414
- else {
415
- // if no conversion or no defaultUnit, just return parsed number
416
- return { ok: true, value: tokens[0].value };
417
- }
435
+ /**
436
+ * Get what the unit conversion is for a unitless value.
437
+ */
438
+ static getDefaultUnitConversion(tokens, unitsConversions, defaultUnit) {
439
+ let unitConversion = defaultUnit ? Parser.tryFindUnitConversion(defaultUnit.label, unitsConversions, defaultUnit) : undefined;
440
+ if (!unitConversion) {
441
+ // No default unit conversion, take the first valid unit.
442
+ const uniqueUnitLabels = [...new Set(tokens.filter((token) => token.isString).map((token) => token.value))];
443
+ for (const label of uniqueUnitLabels) {
444
+ unitConversion = Parser.tryFindUnitConversion(label, unitsConversions, defaultUnit);
445
+ if (unitConversion !== undefined)
446
+ return unitConversion;
447
+ }
448
+ }
449
+ return unitConversion;
450
+ }
451
+ // Get the next token pair to parse into a quantity.
452
+ static getNextTokenPair(index, tokens) {
453
+ if (index >= tokens.length)
454
+ return;
455
+ // 6 possible combination of token pair.
456
+ // Stringified to ease comparison later.
457
+ const validCombinations = [
458
+ JSON.stringify(["string"]), // ['FT']
459
+ JSON.stringify(["string", "number"]), // ['$', 5] unit specification comes before value (like currency)
460
+ JSON.stringify(["number"]), // [5]
461
+ JSON.stringify(["number", "string"]), // [5, 'FT']
462
+ JSON.stringify(["operator", "number"]), // ['-', 5]
463
+ JSON.stringify(["operator", "number", "string"]), // ['-', 5, 'FT']
464
+ ];
465
+ // Push up to 3 tokens in the list, if the length allows it.
466
+ const maxNbrTokensInThePair = Math.min(tokens.length - index, 3);
467
+ const tokenPair = tokens.slice(index, index + maxNbrTokensInThePair);
468
+ const currentCombination = tokenPair.map((token) => token.isOperator ? "operator" : (token.isNumber ? "number" : "string"));
469
+ // Check if the token pair is valid. If not, try again by removing the last token util empty.
470
+ // Ex: ['5', 'FT', '7'] invalid => ['5', 'FT'] valid returned
471
+ for (let i = currentCombination.length - 1; i >= 0; i--) {
472
+ if (validCombinations.includes(JSON.stringify(currentCombination))) {
473
+ break;
418
474
  }
419
475
  else {
420
- // only the unit label was specified so assume magnitude of 1
421
- const conversion = Parser.tryFindUnitConversion(tokens[0].value, unitsConversions, defaultUnit);
422
- if (undefined !== conversion)
423
- return { ok: true, value: conversion.factor + conversion.offset };
424
- else
425
- return { ok: false, error: ParseError.NoValueOrUnitFoundInString };
476
+ currentCombination.pop();
477
+ tokenPair.pop();
426
478
  }
427
479
  }
428
- // common case where single value and single label are supplied
429
- if (tokens.length === 2) {
480
+ return tokenPair.length > 0 ? tokenPair : undefined;
481
+ }
482
+ /**
483
+ * Accumulate the given list of tokens into a single quantity value. Formatting the tokens along the way.
484
+ */
485
+ static getQuantityValueFromParseTokens(tokens, format, unitsConversions, defaultUnitConversion) {
486
+ const defaultUnit = format.units && format.units.length > 0 ? format.units[0][0] : undefined;
487
+ defaultUnitConversion = defaultUnitConversion ? defaultUnitConversion : Parser.getDefaultUnitConversion(tokens, unitsConversions, defaultUnit);
488
+ let tokenPair;
489
+ let increment = 1;
490
+ let mag = 0.0;
491
+ // The sign is saved outside from the loop for cases like this. '-1m 50cm 10mm + 2m 30cm 40mm' => -1.51m + 2.34m
492
+ let sign = 1;
493
+ for (let i = 0; i < tokens.length; i = i + increment) {
494
+ tokenPair = this.getNextTokenPair(i, tokens);
495
+ if (!tokenPair || tokenPair.length === 0) {
496
+ return { ok: false, error: ParseError.UnableToConvertParseTokensToQuantity };
497
+ }
498
+ increment = tokenPair.length;
499
+ // Keep the sign so its applied to the next tokens.
500
+ if (tokenPair[0].isOperator) {
501
+ sign = tokenPair[0].value === Operator.addition ? 1 : -1;
502
+ tokenPair.shift();
503
+ }
430
504
  // unit specification comes before value (like currency)
431
- if (tokens[1].isNumber && tokens[0].isString) {
432
- tokens = [tokens[1], tokens[0]];
505
+ if (tokenPair.length === 2 && tokenPair[0].isString) {
506
+ // Invert it so the currency sign comes second.
507
+ tokenPair = [tokenPair[1], tokenPair[0]];
433
508
  }
434
- if (tokens[0].isNumber && tokens[1].isString) {
435
- let conversion = Parser.tryFindUnitConversion(tokens[1].value, unitsConversions, defaultUnit);
436
- // if no conversion, ignore value in second token. If we have defaultUnit, use it.
437
- if (!conversion && defaultUnit) {
438
- conversion = Parser.tryFindUnitConversion(defaultUnit.label, unitsConversions, defaultUnit);
509
+ if (tokenPair[0].isNumber) {
510
+ let value = sign * tokenPair[0].value;
511
+ let conversion;
512
+ if (tokenPair.length === 2 && tokenPair[1].isString) {
513
+ conversion = Parser.tryFindUnitConversion(tokenPair[1].value, unitsConversions, defaultUnit);
439
514
  }
515
+ conversion = conversion ? conversion : defaultUnitConversion;
440
516
  if (conversion) {
441
- const value = tokens[0].value * conversion.factor + conversion.offset;
442
- return { ok: true, value };
517
+ value = (value * conversion.factor) + conversion.offset;
443
518
  }
444
- // if no conversion, just return parsed number and ignore value in second token
445
- return { ok: true, value: tokens[0].value };
519
+ mag = mag + value;
446
520
  }
447
- }
448
- // common case where there are multiple value/label pairs
449
- if (tokens.length % 2 === 0) {
450
- let mag = 0.0;
451
- for (let i = 0; i < tokens.length; i = i + 2) {
452
- if (tokens[i].isNumber && tokens[i + 1].isString) {
453
- const value = tokens[i].value;
454
- const conversion = Parser.tryFindUnitConversion(tokens[i + 1].value, unitsConversions, defaultUnit);
455
- if (conversion) {
456
- if (mag < 0.0)
457
- mag = mag - ((value * conversion.factor)) + conversion.offset;
458
- else
459
- mag = mag + ((value * conversion.factor)) + conversion.offset;
460
- }
521
+ else {
522
+ // only the unit label was specified so assume magnitude of 0
523
+ const conversion = Parser.tryFindUnitConversion(tokenPair[0].value, unitsConversions, defaultUnit);
524
+ if (conversion === undefined) {
525
+ // Unknown unit label
526
+ return { ok: false, error: ParseError.NoValueOrUnitFoundInString };
461
527
  }
462
528
  }
463
- return { ok: true, value: mag };
464
529
  }
465
- return { ok: false, error: ParseError.UnableToConvertParseTokensToQuantity };
530
+ return { ok: true, value: mag };
466
531
  }
467
532
  /** Method to generate a Quantity given a string that represents a quantity value.
468
533
  * @param inString A string that contains text represent a quantity.
469
534
  * @param parserSpec unit label if not explicitly defined by user. Must have matching entry in supplied array of unitsConversions.
470
- * @param defaultValue default value to return if parsing is un successful
471
535
  */
472
536
  static parseQuantityString(inString, parserSpec) {
473
537
  return Parser.parseToQuantityValue(inString, parserSpec.format, parserSpec.unitConversions);
@@ -493,6 +557,9 @@ class Parser {
493
557
  const tokens = Parser.parseQuantitySpecification(inString, format);
494
558
  if (tokens.length === 0)
495
559
  return { ok: false, error: ParseError.UnableToGenerateParseTokens };
560
+ if (!format.allowMathematicOperations && Parser.isMathematicOperation(tokens)) {
561
+ return { ok: false, error: ParseError.MathematicOperationFoundButIsNotAllowed };
562
+ }
496
563
  if (Parser._log) {
497
564
  // eslint-disable-next-line no-console
498
565
  console.log(`Parse tokens`);