fig-tree-evaluator 2.9.0 → 2.10.0

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/README.md CHANGED
@@ -776,14 +776,21 @@ Example using "data" passed in dynamically as part of expression:
776
776
  ----
777
777
  ### STRING_SUBSTITUTION
778
778
 
779
- *Replace values in a string using simple parameter substitution*
779
+ *Replace values in a string using simple parameter (positional or named properties) substitution*
780
780
 
781
781
  Aliases: `stringSubstitution`, `substitute`, `stringSub`, `replace`
782
782
 
783
783
  #### Properties
784
784
 
785
785
  - `string`<sup>*</sup>: (string) -- a parameterized (`%1`, `%2`) string, where the parameters are to be replaced by dynamic values. E.g. `"My name is %1 (age %2)"`
786
- - `substitutions` (or `replacements`)<sup>*</sup>: (array) -- the values to be substituted into `string`
786
+ - `substitutions` (or `replacements`, `values`)<sup>*</sup>: (array | object) -- the values to be substituted into `string`. Will be either an array or object depending on whether you're using positional replacements or named properties (see [below](#positional-replacement)).
787
+ - `trimWhiteSpace` (or `trimWhitespace`, `trim`): (boolean, default `true`) -- strips whitespace from the beginning or end of the substitution values
788
+ - `substitutionCharacter` (or `subCharacter`, `subChar`): (`"%"` or `"$"`) -- by default, when using positional replacement, it looks for the `%` token (i.e `%1, %2, etc`), but this can be changed to `$` (i.e. `$1, $2, $3, etc`) by setting this property to `$`.
789
+ - `numberMapping` (or `numMap`, `numberMap`, `pluralisation`, `pluralization`, `plurals`): (object) -- when replacing with named properties and you have replacement values that are numbers, it's possible to map values or ranges to specific string outputs. This can be used to produce correct pluralisation, for example. [See below](#named-property-replacement) for more details.
790
+
791
+ Substitution can be done using either **positional** replacement, or with **named properties**:
792
+
793
+ #### Positional replacement
787
794
 
788
795
  The values in the `substitutions` array are replaced in the original `string` by matching their order to the numerical order of the parameters.
789
796
 
@@ -804,7 +811,9 @@ e.g.
804
811
 
805
812
  {
806
813
  operator: 'replace',
807
- string: '%1 is actually %2 %3',
814
+ // Using $1, $2 instead of %1, %2 this time:
815
+ string: '$1 is actually $2 $3',
816
+ substitutionCharacter: "$",
808
817
  substitutions: [
809
818
  // Using the 'user' object from above (OBJECT_PROPERTIES operator)
810
819
  {
@@ -830,9 +839,19 @@ e.g.
830
839
  substitutions: ['bird', 'Tweet!'],
831
840
  }
832
841
  // => 'A bird says: "Tweet! Tweet! Tweet!"'
842
+
843
+ // Replacement tokens can be escaped using the standard "\" escape character:
844
+ {
845
+ operator: 'stringSubstitution',
846
+ string: 'The price of $1 is \$5',
847
+ subChar: '$',
848
+ substitutions: [ 'a coffee', 'not used' ],
849
+ }
850
+ // => The price of a coffee is $5
833
851
  ```
834
852
 
835
- `children` array: `[string, ...substitutions]`
853
+ `children` array: `[string, ...substitutions]`
854
+ (`trimWhiteSpace` and `substitutionCharacter` not available, since `substitutions` can be an arbitrary number of items)
836
855
 
837
856
  e.g.
838
857
  ```js
@@ -843,6 +862,77 @@ e.g.
843
862
  // => "I am Iron Man"
844
863
  ```
845
864
 
865
+ #### Named property replacement
866
+
867
+ Replacement tokens can be indicated in the main string with a named value, using `{{<name>}}` syntax, e.g. `"Your name is {{firstName}} {{lastName}}"`. Then the `substitutions` property must be an object with those property names:
868
+
869
+ ```js
870
+ {
871
+ operator: 'stringSubstitution',
872
+ string: 'Your name is {{firstName}} {{lastName}}',
873
+ substitutions: {
874
+ firstName: 'Steve',
875
+ lastName: "Rogers"
876
+ }
877
+ }
878
+ // => "Your name is Steve Rogers"
879
+ ```
880
+
881
+ If the replacement values are numbers, we can extend this functionality with a special `numberMapping` object, which allows for different replacements depending on the value, which is handy for pluralisation, for example.
882
+
883
+ The syntax for the `numberMapping` property is:
884
+ ```js
885
+ {
886
+ propertyName1: {
887
+ 1: "Output if value is {}",
888
+ 2: "Output if value is 2",
889
+ ">5": "Output if value is greater than 5",
890
+ "<0": "Output if value is less than 0"
891
+ "other": "Fallback output if none of the others match: {} count"
892
+ // {} is a replacement for the numerical value itself
893
+ },
894
+ propertyName2: { ...etc }
895
+ }
896
+ ```
897
+ The number map can have as few or as many match options as desired -- if no match is found (or if no `numberMapping` property at all), the number will be returned as-is.
898
+
899
+ e.g.
900
+ ```js
901
+ {
902
+ operator: 'stringSubstitution',
903
+ string: 'Hi {{name}}, we have {{count}} attending this event.',
904
+ values: {
905
+ name: "Tatiana",
906
+ count: {operator: "getData", property: "numOfPeople" }
907
+ },
908
+ numberMap: {
909
+ count: {
910
+ 0: "no one",
911
+ 1: "just one person",
912
+ ">10": "too many people",
913
+ "other": "{} people"
914
+ }
915
+ }
916
+ }
917
+ // Output with varying values for "numOfPeople" passed into evaluation
918
+ // "data" object:
919
+
920
+ // { numOfPeople: 5 }
921
+ // => "Hi Tatiana, we have 5 people attending this event."
922
+
923
+ // { numOfPeople: 0 }
924
+ // => "Hi Tatiana, we have no one attending this event."
925
+
926
+ // { numOfPeople: 100 }
927
+ // => "Hi Tatiana, we have too many people attending this event."
928
+
929
+ // { numOfPeople: 1 }
930
+ // => "Hi Tatiana, we have just one person attending this event.
931
+ ```
932
+
933
+ **Note**: `children` array not available for named properties
934
+
935
+
846
936
  ----
847
937
 
848
938
  ### SPLIT
@@ -1703,14 +1793,18 @@ Please open an issue: https://github.com/CarlosNZ/fig-tree-evaluator/issues
1703
1793
 
1704
1794
  *Trivial upgrades (e.g. documentation, small re-factors, types, etc.) not included*
1705
1795
 
1796
+ - **v2.10.0**: Extended stringSubstitution functionality to included named
1797
+ property substitution, trim whitespace option, and pluralisation (#97)
1706
1798
  - **v2.9.0**: Added ability to invalidate cache by time (#94)
1707
1799
  - **v2.8.6**: Small bug fix where `options` object would be mutated instead of replaced
1708
1800
  - **v2.8.5**: Small bug fix in [COUNT](#count) operator
1709
1801
  - **v2.8.4**: Refactor types, better compliance with [ESLint](https://eslint.org/) rules, add more tests
1710
1802
  - **v2.8.0**:
1711
1803
  - **[Shorthand syntax](#shorthand-syntax)** (#80)
1712
- - **Methods to retrieve [metadata](#metadata)** about operators, fragments and functions (#82)
1713
- - **v2.7.0**: **Add `excludeOperators` option** to allow certain operators to be prohibited (e.g. database lookups) (#54)
1804
+ - **Methods to retrieve [metadata](#metadata)** about operators, fragments and
1805
+ functions (#82)
1806
+ - **v2.7.0**: **Add `excludeOperators` option** to allow certain operators to be
1807
+ prohibited (e.g. database lookups) (#54)
1714
1808
  - **v2.6.0**: Resolve alias nodes that are not part of an Operator node when `evaluateFullObject` is enabled (#78)
1715
1809
  - **v2.5.0**:
1716
1810
  - Bug fixes for edge cases (mainly related to backwards compatibility)
@@ -1732,7 +1826,8 @@ Please open an issue: https://github.com/CarlosNZ/fig-tree-evaluator/issues
1732
1826
  - **v2.0.0**: Re-write as stand-alone package. Major improvements include:
1733
1827
  - more [operators](#operator-reference)
1734
1828
  - operator (and property) [aliases](#operator--property-aliases)
1735
- - more appropriately-named properties associated with each operator (as opposed to a single `children` array)
1829
+ - more appropriately-named properties associated with each operator (as
1830
+ opposed to a single `children` array)
1736
1831
  - class-based Evaluator instances
1737
1832
  - runtime type-checking
1738
1833
  - better error handling and error reporting
@@ -15,9 +15,30 @@ const parameters = [
15
15
  {
16
16
  name: 'substitutions',
17
17
  description: 'An array of substitution values for the parameterised string',
18
- aliases: ['replacements'],
18
+ aliases: ['replacements', 'values'],
19
19
  required: true,
20
- type: 'array',
20
+ type: ['array', 'object'],
21
+ },
22
+ {
23
+ name: 'trimWhiteSpace',
24
+ description: 'Whether or not to trim white space from either end of the substituted strings (default: true)',
25
+ aliases: ['trim', 'trimWhitespace'],
26
+ required: false,
27
+ type: 'boolean',
28
+ },
29
+ {
30
+ name: 'substitutionCharacter',
31
+ description: 'Which character to search for in original string for replacement -- can be "%" or "$" (default: "%")',
32
+ aliases: ['subCharacter', 'subChar'],
33
+ required: false,
34
+ type: 'string',
35
+ },
36
+ {
37
+ name: 'numberMapping',
38
+ description: 'Rules for mapping number values to text strings, such as pluralisation.',
39
+ aliases: ['numMap', 'numberMap', 'pluralisation', 'pluralization', 'plurals'],
40
+ required: false,
41
+ type: 'object',
21
42
  },
22
43
  ];
23
44
  exports.propertyAliases = (0, _operatorUtils_1.getPropertyAliases)(parameters);
@@ -36,15 +36,42 @@ exports.STRING_SUBSTITUTION = void 0;
36
36
  const _operatorUtils_1 = require("../_operatorUtils");
37
37
  const data_1 = __importStar(require("./data"));
38
38
  const evaluate = (expression, config) => __awaiter(void 0, void 0, void 0, function* () {
39
- const [string, substitutions] = (yield (0, _operatorUtils_1.evaluateArray)([expression.string, expression.substitutions], config));
40
- config.typeChecker((0, _operatorUtils_1.getTypeCheckInput)(data_1.default.parameters, { string, substitutions }));
41
- const parameterPattern = /(%[\d]+)/g;
42
- const parameters = (string.match(parameterPattern) || []).sort((a, b) => Number(a.slice(1)) - Number(b.slice(1)));
43
- const uniqueParameters = new Set(parameters);
44
- const replacementsObj = (0, _operatorUtils_1.zipArraysToObject)(Array.from(uniqueParameters), substitutions);
39
+ const [string, substitutions, trimWhiteSpace = true, substitutionCharacter = '%', numberMapping = {},] = (yield (0, _operatorUtils_1.evaluateArray)([
40
+ expression.string,
41
+ expression.substitutions,
42
+ expression.trimWhiteSpace,
43
+ expression.substitutionCharacter,
44
+ expression.numberMapping,
45
+ ], config));
46
+ config.typeChecker((0, _operatorUtils_1.getTypeCheckInput)(data_1.default.parameters, {
47
+ string,
48
+ substitutions,
49
+ trimWhiteSpace,
50
+ substitutionCharacter,
51
+ numberMapping,
52
+ }));
53
+ if (Array.isArray(substitutions)) {
54
+ const subChar = substitutionCharacter === '$' ? '$' : '%';
55
+ const patternString = `(?<!\\\\)(${subChar === '%' ? '%' : '\\$'}[\\d]+)`;
56
+ const parameterPattern = new RegExp(patternString, 'g');
57
+ const parameters = (string.match(parameterPattern) || []).sort((a, b) => Number(a.slice(1)) - Number(b.slice(1)));
58
+ const uniqueParameters = new Set(parameters);
59
+ const replacementsObj = (0, _operatorUtils_1.zipArraysToObject)(Array.from(uniqueParameters), substitutions.map((sub) => (trimWhiteSpace ? String(sub).trim() : sub)));
60
+ return (string
61
+ .split(parameterPattern)
62
+ .map((fragment) => (fragment in replacementsObj ? replacementsObj[fragment] : fragment))
63
+ .join('')
64
+ .replace(`\\${subChar}`, subChar));
65
+ }
66
+ const parameterPattern = /(?<!\\)({{[A-z0-9_]+}})/g;
45
67
  return string
46
68
  .split(parameterPattern)
47
- .map((fragment) => (fragment in replacementsObj ? replacementsObj[fragment] : fragment))
69
+ .map((fragment) => {
70
+ if (!/(?<!\\){{(.+)}}/.exec(fragment))
71
+ return fragment.replace('\\{{', '{{');
72
+ const replacement = getReplacement(fragment, substitutions, numberMapping);
73
+ return trimWhiteSpace ? String(replacement).trim() : replacement;
74
+ })
48
75
  .join('');
49
76
  });
50
77
  const parseChildren = (expression) => {
@@ -57,3 +84,29 @@ exports.STRING_SUBSTITUTION = {
57
84
  evaluate,
58
85
  parseChildren,
59
86
  };
87
+ const getReplacement = (fragment, replacements, numberMaps) => {
88
+ var _a, _b;
89
+ const key = fragment.replace(/{{(.+)}}/, '$1');
90
+ const value = (_a = replacements === null || replacements === void 0 ? void 0 : replacements[key]) !== null && _a !== void 0 ? _a : '';
91
+ if (typeof value !== 'number')
92
+ return (_b = replacements === null || replacements === void 0 ? void 0 : replacements[key]) !== null && _b !== void 0 ? _b : '';
93
+ if (!(key in numberMaps))
94
+ return value;
95
+ const numMap = numberMaps[key];
96
+ if (value in numMap)
97
+ return numMap[value].replace('{}', String(value));
98
+ const numberKeys = Object.keys(numberMaps[key]);
99
+ const greaterThanKey = numberKeys.find((key) => key.startsWith('>'));
100
+ if (greaterThanKey) {
101
+ const num = Number(greaterThanKey.slice(1));
102
+ if (value > num)
103
+ return numMap === null || numMap === void 0 ? void 0 : numMap[greaterThanKey];
104
+ }
105
+ const lessThanKey = numberKeys.find((key) => key.startsWith('<'));
106
+ if (lessThanKey) {
107
+ const num = Number(lessThanKey.slice(1));
108
+ if (value < num)
109
+ return numMap === null || numMap === void 0 ? void 0 : numMap[lessThanKey];
110
+ }
111
+ return numMap.other ? numMap.other.replace('{}', String(value)) : value;
112
+ };
@@ -1 +1 @@
1
- export declare const version = "2.9.0";
1
+ export declare const version = "2.10.0";
package/build/version.js CHANGED
@@ -1,4 +1,4 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.version = void 0;
4
- exports.version = '2.9.0';
4
+ exports.version = '2.10.0';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fig-tree-evaluator",
3
- "version": "2.9.0",
3
+ "version": "2.10.0",
4
4
  "description": "Module to evaluate JSON-structured expression trees",
5
5
  "main": "build/index.js",
6
6
  "types": "build/index.d.ts",