@flolegal-it/numbers 1.0.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 ADDED
@@ -0,0 +1,96 @@
1
+ # numbers
2
+
3
+ TypeScript utility library for number handling.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install @flo-legal-it/numbers
9
+ ```
10
+
11
+ ## Regex utilities
12
+
13
+ ```typescript
14
+ import { getNumberRegExp } from 'numbers';
15
+
16
+ // English format (1,000.25)
17
+ const enRegex = getNumberRegExp('en');
18
+ enRegex.test('1,000.25'); // true
19
+ enRegex.test('0.5'); // true
20
+
21
+ // Dutch/French format (1.000,25)
22
+ const nlRegex = getNumberRegExp('nl');
23
+ nlRegex.test('1.000,25'); // true
24
+ nlRegex.test('0,5'); // true
25
+
26
+ // Allow negative numbers
27
+ const negRegex = getNumberRegExp('en', true);
28
+ negRegex.test('-1,000.25'); // true
29
+
30
+ // Disallow empty string
31
+ const strictRegex = getNumberRegExp('en', false, false);
32
+ strictRegex.test(''); // false
33
+ ```
34
+
35
+ ## Formula utilities
36
+
37
+ ```typescript
38
+ import { validateFormula, getUsedVars, evalFormula } from '@flo-legal-it/numbers';
39
+
40
+ // Validate a formula against allowed variables
41
+ const errors = validateFormula('a + b * 2', ['a', 'b']);
42
+ // [] (no errors)
43
+
44
+ // Get variables used in a formula
45
+ const vars = getUsedVars('a + b * 2');
46
+ // ['a', 'b']
47
+
48
+ // Evaluate a formula with a variable scope
49
+ const result = evalFormula('a + b * 2', { a: 1, b: 3 });
50
+ // 7
51
+ ```
52
+
53
+ ## API
54
+
55
+ ### `getNumberRegExp(language, includeNegativeNumbers?, allowEmpty?)`
56
+
57
+ Returns a `RegExp` that validates number strings for the given locale.
58
+
59
+ | Parameter | Type | Default | Description |
60
+ |---|---|---|---|
61
+ | `language` | `'nl' \| 'en' \| 'fr'` | — | Locale for number formatting |
62
+ | `includeNegativeNumbers` | `boolean` | `false` | Allow negative numbers |
63
+ | `allowEmpty` | `boolean` | `true` | Allow empty string |
64
+
65
+ ### `validateFormula(expr, allowedVars)`
66
+
67
+ Returns an array of error messages. Empty array means the formula is valid.
68
+
69
+ | Parameter | Type | Description |
70
+ |---|---|---|
71
+ | `expr` | `string` | Math expression to validate |
72
+ | `allowedVars` | `string[]` | Variable names allowed in the expression |
73
+
74
+ ### `getUsedVars(expr)`
75
+
76
+ Returns an array of variable names found in the expression, or an empty array if parsing fails.
77
+
78
+ ### `evalFormula(expression, scope)`
79
+
80
+ Evaluates a math expression with the given variable scope and returns the numeric result. Validate the formula first with `validateFormula`.
81
+
82
+ ## Module formats
83
+
84
+ This package ships both CommonJS and ESM builds. The correct format is selected automatically based on your project's module system via the `exports` field in `package.json`.
85
+
86
+ ---
87
+
88
+ ## Notes for internal contributers
89
+ This repo is published as a *public* npm package. When publishing a new version, use `npm publish --access public`.
90
+ Don't forget to bump the version number!
91
+ ```
92
+ npm version patch # 1.0.0 → 1.0.1
93
+ npm version minor # 1.0.0 → 1.1.0
94
+ npm version major # 1.0.0 → 2.0.0
95
+
96
+ ```
@@ -0,0 +1,12 @@
1
+ export declare function validateFormula(expr: string, allowedVars: string[]): string[];
2
+ /**
3
+ * Returns list of currently used vars in formula, or null when formula cannot
4
+ * be parsed
5
+ * @param expr
6
+ */
7
+ export declare function getUsedVars(expr: string): string[];
8
+ /**
9
+ * Evaluate a math expression with a fixed variable scope
10
+ * nb. use validateFormula first
11
+ */
12
+ export declare function evalFormula(expression: string, scope: Record<string, number>): number;
@@ -0,0 +1,60 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.validateFormula = validateFormula;
4
+ exports.getUsedVars = getUsedVars;
5
+ exports.evalFormula = evalFormula;
6
+ const number_1 = require("mathjs/number");
7
+ // Create a minimal math.js instance
8
+ const math = (0, number_1.create)({
9
+ evaluateDependencies: number_1.evaluateDependencies,
10
+ addDependencies: number_1.addDependencies,
11
+ subtractDependencies: number_1.subtractDependencies,
12
+ multiplyDependencies: number_1.multiplyDependencies,
13
+ divideDependencies: number_1.divideDependencies,
14
+ });
15
+ function validateFormula(expr, allowedVars) {
16
+ const errorMessages = [];
17
+ try {
18
+ const vars = new Set(allowedVars);
19
+ const node = math.parse(expr);
20
+ node.traverse((n) => {
21
+ if (n.isSymbolNode && !vars.has(n.name)) {
22
+ errorMessages.push(`'${n.name}' is geen geldige variable`);
23
+ }
24
+ if (n.isOperatorNode && !["+", "-", "*", "/"].includes(n.op)) {
25
+ errorMessages.push(`'${n.op}' is geen geldige operator`);
26
+ }
27
+ });
28
+ }
29
+ catch (e) {
30
+ errorMessages.push(e.message);
31
+ }
32
+ return errorMessages;
33
+ }
34
+ /**
35
+ * Returns list of currently used vars in formula, or null when formula cannot
36
+ * be parsed
37
+ * @param expr
38
+ */
39
+ function getUsedVars(expr) {
40
+ try {
41
+ const node = math.parse(expr);
42
+ const vars = [];
43
+ node.traverse((n) => {
44
+ if (n.isSymbolNode) {
45
+ vars.push(n.name);
46
+ }
47
+ });
48
+ return vars;
49
+ }
50
+ catch (e) {
51
+ return [];
52
+ }
53
+ }
54
+ /**
55
+ * Evaluate a math expression with a fixed variable scope
56
+ * nb. use validateFormula first
57
+ */
58
+ function evalFormula(expression, scope) {
59
+ return math.evaluate(expression, scope);
60
+ }
@@ -0,0 +1,3 @@
1
+ export { getNumberRegExp } from './reg-exp';
2
+ export type { NumberRegExpOptions } from './reg-exp';
3
+ export { validateFormula, getUsedVars, evalFormula } from './formula';
@@ -0,0 +1,9 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.evalFormula = exports.getUsedVars = exports.validateFormula = exports.getNumberRegExp = void 0;
4
+ var reg_exp_1 = require("./reg-exp");
5
+ Object.defineProperty(exports, "getNumberRegExp", { enumerable: true, get: function () { return reg_exp_1.getNumberRegExp; } });
6
+ var formula_1 = require("./formula");
7
+ Object.defineProperty(exports, "validateFormula", { enumerable: true, get: function () { return formula_1.validateFormula; } });
8
+ Object.defineProperty(exports, "getUsedVars", { enumerable: true, get: function () { return formula_1.getUsedVars; } });
9
+ Object.defineProperty(exports, "evalFormula", { enumerable: true, get: function () { return formula_1.evalFormula; } });
@@ -0,0 +1 @@
1
+ {"type":"commonjs"}
@@ -0,0 +1,25 @@
1
+ export interface NumberRegExpOptions {
2
+ useThousandsSeparator: boolean;
3
+ allowDecimalSeparator: boolean;
4
+ thousandsSeparator?: ',' | '.' | ' ';
5
+ decimalSeparator?: '.' | ',';
6
+ }
7
+ /**
8
+ * Returns a regex that accepts any possible string representation of a positive floating
9
+ * point number for the given language including '0'.
10
+ * The regex does not accept power notations.
11
+ * Negative numbers are optional.
12
+ *
13
+ * Examples NL:
14
+ * 0
15
+ * 0,5
16
+ * 1.000
17
+ * 1.000,25
18
+ *
19
+ * Examples EN:
20
+ * 0
21
+ * 0.5
22
+ * 1,000
23
+ * 1,000.25
24
+ */
25
+ export declare function getNumberRegExp(language: 'nl' | 'en' | 'fr', includeNegativeNumbers?: boolean, allowEmpty?: boolean): RegExp;
@@ -0,0 +1,108 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.getNumberRegExp = getNumberRegExp;
4
+ /**
5
+ * Returns a regex that accepts any possible string representation of a positive floating
6
+ * point number for the given language including '0'.
7
+ * The regex does not accept power notations.
8
+ * Negative numbers are optional.
9
+ *
10
+ * Examples NL:
11
+ * 0
12
+ * 0,5
13
+ * 1.000
14
+ * 1.000,25
15
+ *
16
+ * Examples EN:
17
+ * 0
18
+ * 0.5
19
+ * 1,000
20
+ * 1,000.25
21
+ */
22
+ function getNumberRegExp(language, includeNegativeNumbers = false, allowEmpty = true) {
23
+ const variants = [];
24
+ const baseOptions = {
25
+ useThousandsSeparator: true,
26
+ allowDecimalSeparator: true
27
+ };
28
+ if (language === 'en') {
29
+ // English formatting (1,000.50)
30
+ variants.push(buildNumberPattern({ ...baseOptions, thousandsSeparator: ',', decimalSeparator: '.' }));
31
+ // International formatting with space (1 000.50)
32
+ variants.push(buildNumberPattern({ ...baseOptions, thousandsSeparator: ' ', decimalSeparator: '.' }));
33
+ // Without thousands separator (1000.50)
34
+ variants.push(buildNumberPattern({ ...baseOptions, useThousandsSeparator: false, decimalSeparator: '.' }));
35
+ }
36
+ else {
37
+ // Dutch / French formatting (1.000,50)
38
+ variants.push(buildNumberPattern({ ...baseOptions, thousandsSeparator: '.', decimalSeparator: ',' }));
39
+ // International formatting with space (1 000,50)
40
+ variants.push(buildNumberPattern({ ...baseOptions, thousandsSeparator: ' ', decimalSeparator: ',' }));
41
+ // Without thousands separator (1000,50)
42
+ variants.push(buildNumberPattern({ ...baseOptions, useThousandsSeparator: false, decimalSeparator: ',' }));
43
+ }
44
+ // Combine all allowed variants
45
+ let pattern = variants.join('|');
46
+ // Optionally allow empty string
47
+ if (allowEmpty) {
48
+ pattern += '|';
49
+ }
50
+ // Optionally allow negative numbers
51
+ if (includeNegativeNumbers) {
52
+ pattern = `-?(?:${pattern})`;
53
+ }
54
+ // Add start and end anchors
55
+ return new RegExp(`^(?:${pattern})$`);
56
+ }
57
+ /**
58
+ * Builds a regex fragment for a single number formatting configuration.
59
+ *
60
+ * Integer part:
61
+ * - Allows '0'
62
+ * - Prevents leading zeros like '01'
63
+ * - Allows properly grouped thousands if enabled
64
+ *
65
+ * Fraction part:
66
+ * - Optional
67
+ * - Decimal separator followed by one or more digits
68
+ */
69
+ function buildNumberPattern(options) {
70
+ let integerPart = '';
71
+ if (options.useThousandsSeparator && options.thousandsSeparator) {
72
+ // first digit is 1-9 (unless number is exactly 0)
73
+ // then maximum two digits (first group)
74
+ // followed by one or more groups of 3 digits
75
+ // preceded by the thousands separator
76
+ switch (options.thousandsSeparator) {
77
+ case '.':
78
+ integerPart = '(0|[1-9]\\d{0,2}(?:\\.\\d{3})+|[1-9]\\d*)';
79
+ break;
80
+ case ',':
81
+ integerPart = '(0|[1-9]\\d{0,2}(?:,\\d{3})+|[1-9]\\d*)';
82
+ break;
83
+ case ' ':
84
+ integerPart = '(0|[1-9]\\d{0,2}(?:\\s\\d{3})+|[1-9]\\d*)';
85
+ break;
86
+ }
87
+ }
88
+ else {
89
+ // no thousands separator
90
+ // first position cannot be 0 unless number is exactly 0
91
+ // remaining positions can be any digit
92
+ integerPart = '(0|[1-9]\\d*)';
93
+ }
94
+ let fractionPart = '';
95
+ if (options.allowDecimalSeparator && options.decimalSeparator) {
96
+ // optional fraction group
97
+ // decimal separator followed by one or more digits
98
+ switch (options.decimalSeparator) {
99
+ case '.':
100
+ fractionPart = '(?:\\.\\d+)?';
101
+ break;
102
+ case ',':
103
+ fractionPart = '(?:,\\d+)?';
104
+ break;
105
+ }
106
+ }
107
+ return integerPart + fractionPart;
108
+ }
@@ -0,0 +1,12 @@
1
+ export declare function validateFormula(expr: string, allowedVars: string[]): string[];
2
+ /**
3
+ * Returns list of currently used vars in formula, or null when formula cannot
4
+ * be parsed
5
+ * @param expr
6
+ */
7
+ export declare function getUsedVars(expr: string): string[];
8
+ /**
9
+ * Evaluate a math expression with a fixed variable scope
10
+ * nb. use validateFormula first
11
+ */
12
+ export declare function evalFormula(expression: string, scope: Record<string, number>): number;
@@ -0,0 +1,57 @@
1
+ import { create, evaluateDependencies, addDependencies, subtractDependencies, multiplyDependencies, divideDependencies,
2
+ // @ts-ignore
3
+ } from 'mathjs/number';
4
+ // Create a minimal math.js instance
5
+ const math = create({
6
+ evaluateDependencies,
7
+ addDependencies,
8
+ subtractDependencies,
9
+ multiplyDependencies,
10
+ divideDependencies,
11
+ });
12
+ export function validateFormula(expr, allowedVars) {
13
+ const errorMessages = [];
14
+ try {
15
+ const vars = new Set(allowedVars);
16
+ const node = math.parse(expr);
17
+ node.traverse((n) => {
18
+ if (n.isSymbolNode && !vars.has(n.name)) {
19
+ errorMessages.push(`'${n.name}' is geen geldige variable`);
20
+ }
21
+ if (n.isOperatorNode && !["+", "-", "*", "/"].includes(n.op)) {
22
+ errorMessages.push(`'${n.op}' is geen geldige operator`);
23
+ }
24
+ });
25
+ }
26
+ catch (e) {
27
+ errorMessages.push(e.message);
28
+ }
29
+ return errorMessages;
30
+ }
31
+ /**
32
+ * Returns list of currently used vars in formula, or null when formula cannot
33
+ * be parsed
34
+ * @param expr
35
+ */
36
+ export function getUsedVars(expr) {
37
+ try {
38
+ const node = math.parse(expr);
39
+ const vars = [];
40
+ node.traverse((n) => {
41
+ if (n.isSymbolNode) {
42
+ vars.push(n.name);
43
+ }
44
+ });
45
+ return vars;
46
+ }
47
+ catch (e) {
48
+ return [];
49
+ }
50
+ }
51
+ /**
52
+ * Evaluate a math expression with a fixed variable scope
53
+ * nb. use validateFormula first
54
+ */
55
+ export function evalFormula(expression, scope) {
56
+ return math.evaluate(expression, scope);
57
+ }
@@ -0,0 +1,3 @@
1
+ export { getNumberRegExp } from './reg-exp';
2
+ export type { NumberRegExpOptions } from './reg-exp';
3
+ export { validateFormula, getUsedVars, evalFormula } from './formula';
@@ -0,0 +1,2 @@
1
+ export { getNumberRegExp } from './reg-exp';
2
+ export { validateFormula, getUsedVars, evalFormula } from './formula';
@@ -0,0 +1 @@
1
+ {"type":"module"}
@@ -0,0 +1,25 @@
1
+ export interface NumberRegExpOptions {
2
+ useThousandsSeparator: boolean;
3
+ allowDecimalSeparator: boolean;
4
+ thousandsSeparator?: ',' | '.' | ' ';
5
+ decimalSeparator?: '.' | ',';
6
+ }
7
+ /**
8
+ * Returns a regex that accepts any possible string representation of a positive floating
9
+ * point number for the given language including '0'.
10
+ * The regex does not accept power notations.
11
+ * Negative numbers are optional.
12
+ *
13
+ * Examples NL:
14
+ * 0
15
+ * 0,5
16
+ * 1.000
17
+ * 1.000,25
18
+ *
19
+ * Examples EN:
20
+ * 0
21
+ * 0.5
22
+ * 1,000
23
+ * 1,000.25
24
+ */
25
+ export declare function getNumberRegExp(language: 'nl' | 'en' | 'fr', includeNegativeNumbers?: boolean, allowEmpty?: boolean): RegExp;
@@ -0,0 +1,105 @@
1
+ /**
2
+ * Returns a regex that accepts any possible string representation of a positive floating
3
+ * point number for the given language including '0'.
4
+ * The regex does not accept power notations.
5
+ * Negative numbers are optional.
6
+ *
7
+ * Examples NL:
8
+ * 0
9
+ * 0,5
10
+ * 1.000
11
+ * 1.000,25
12
+ *
13
+ * Examples EN:
14
+ * 0
15
+ * 0.5
16
+ * 1,000
17
+ * 1,000.25
18
+ */
19
+ export function getNumberRegExp(language, includeNegativeNumbers = false, allowEmpty = true) {
20
+ const variants = [];
21
+ const baseOptions = {
22
+ useThousandsSeparator: true,
23
+ allowDecimalSeparator: true
24
+ };
25
+ if (language === 'en') {
26
+ // English formatting (1,000.50)
27
+ variants.push(buildNumberPattern({ ...baseOptions, thousandsSeparator: ',', decimalSeparator: '.' }));
28
+ // International formatting with space (1 000.50)
29
+ variants.push(buildNumberPattern({ ...baseOptions, thousandsSeparator: ' ', decimalSeparator: '.' }));
30
+ // Without thousands separator (1000.50)
31
+ variants.push(buildNumberPattern({ ...baseOptions, useThousandsSeparator: false, decimalSeparator: '.' }));
32
+ }
33
+ else {
34
+ // Dutch / French formatting (1.000,50)
35
+ variants.push(buildNumberPattern({ ...baseOptions, thousandsSeparator: '.', decimalSeparator: ',' }));
36
+ // International formatting with space (1 000,50)
37
+ variants.push(buildNumberPattern({ ...baseOptions, thousandsSeparator: ' ', decimalSeparator: ',' }));
38
+ // Without thousands separator (1000,50)
39
+ variants.push(buildNumberPattern({ ...baseOptions, useThousandsSeparator: false, decimalSeparator: ',' }));
40
+ }
41
+ // Combine all allowed variants
42
+ let pattern = variants.join('|');
43
+ // Optionally allow empty string
44
+ if (allowEmpty) {
45
+ pattern += '|';
46
+ }
47
+ // Optionally allow negative numbers
48
+ if (includeNegativeNumbers) {
49
+ pattern = `-?(?:${pattern})`;
50
+ }
51
+ // Add start and end anchors
52
+ return new RegExp(`^(?:${pattern})$`);
53
+ }
54
+ /**
55
+ * Builds a regex fragment for a single number formatting configuration.
56
+ *
57
+ * Integer part:
58
+ * - Allows '0'
59
+ * - Prevents leading zeros like '01'
60
+ * - Allows properly grouped thousands if enabled
61
+ *
62
+ * Fraction part:
63
+ * - Optional
64
+ * - Decimal separator followed by one or more digits
65
+ */
66
+ function buildNumberPattern(options) {
67
+ let integerPart = '';
68
+ if (options.useThousandsSeparator && options.thousandsSeparator) {
69
+ // first digit is 1-9 (unless number is exactly 0)
70
+ // then maximum two digits (first group)
71
+ // followed by one or more groups of 3 digits
72
+ // preceded by the thousands separator
73
+ switch (options.thousandsSeparator) {
74
+ case '.':
75
+ integerPart = '(0|[1-9]\\d{0,2}(?:\\.\\d{3})+|[1-9]\\d*)';
76
+ break;
77
+ case ',':
78
+ integerPart = '(0|[1-9]\\d{0,2}(?:,\\d{3})+|[1-9]\\d*)';
79
+ break;
80
+ case ' ':
81
+ integerPart = '(0|[1-9]\\d{0,2}(?:\\s\\d{3})+|[1-9]\\d*)';
82
+ break;
83
+ }
84
+ }
85
+ else {
86
+ // no thousands separator
87
+ // first position cannot be 0 unless number is exactly 0
88
+ // remaining positions can be any digit
89
+ integerPart = '(0|[1-9]\\d*)';
90
+ }
91
+ let fractionPart = '';
92
+ if (options.allowDecimalSeparator && options.decimalSeparator) {
93
+ // optional fraction group
94
+ // decimal separator followed by one or more digits
95
+ switch (options.decimalSeparator) {
96
+ case '.':
97
+ fractionPart = '(?:\\.\\d+)?';
98
+ break;
99
+ case ',':
100
+ fractionPart = '(?:,\\d+)?';
101
+ break;
102
+ }
103
+ }
104
+ return integerPart + fractionPart;
105
+ }
package/package.json ADDED
@@ -0,0 +1,42 @@
1
+ {
2
+ "name": "@flolegal-it/numbers",
3
+ "version": "1.0.0",
4
+ "description": "Contains utility functions for working with numbers",
5
+ "repository": {
6
+ "type": "git",
7
+ "url": "git+https://github.com/FLO-Legal-IT/numbers.git"
8
+ },
9
+ "keywords": [
10
+ "number",
11
+ "regex"
12
+ ],
13
+ "author": "FloLegal IT",
14
+ "license": "ISC",
15
+ "main": "lib/cjs/index.js",
16
+ "module": "lib/esm/index.js",
17
+ "types": "lib/cjs/index.d.ts",
18
+ "exports": {
19
+ ".": {
20
+ "types": "./lib/cjs/index.d.ts",
21
+ "import": "./lib/esm/index.js",
22
+ "require": "./lib/cjs/index.js"
23
+ }
24
+ },
25
+ "files": [
26
+ "lib"
27
+ ],
28
+ "scripts": {
29
+ "build": "rm -rf lib && tsc -p tsconfig.cjs.json && tsc -p tsconfig.esm.json && echo '{\"type\":\"commonjs\"}' > lib/cjs/package.json && echo '{\"type\":\"module\"}' > lib/esm/package.json",
30
+ "test": "ts-node node_modules/.bin/tape 'src/**/*.test.ts'",
31
+ "prepublishOnly": "npm run build"
32
+ },
33
+ "dependencies": {
34
+ "mathjs": "^15.1.1"
35
+ },
36
+ "devDependencies": {
37
+ "@types/tape": "^5.8.1",
38
+ "tape": "^5.9.0",
39
+ "ts-node": "^10.9.2",
40
+ "typescript": "^5.9.3"
41
+ }
42
+ }