@jsonspecs/decimal 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/CHANGELOG.md +9 -0
- package/COMPATIBILITY.md +28 -0
- package/LICENSE +21 -0
- package/README.md +94 -0
- package/SPEC_RU.md +94 -0
- package/index.d.ts +4 -0
- package/index.js +118 -0
- package/package.json +56 -0
- package/src/check/decimal_between.js +5 -0
- package/src/check/decimal_equals.js +5 -0
- package/src/check/decimal_field_equals_field.js +5 -0
- package/src/check/decimal_field_greater_than_field.js +5 -0
- package/src/check/decimal_greater_than.js +5 -0
- package/src/check/decimal_less_than.js +5 -0
- package/src/check/decimal_precision.js +5 -0
- package/src/check/decimal_scale.js +5 -0
- package/src/check/decimal_valid.js +5 -0
- package/src/parser/decimal.js +244 -0
- package/src/predicate/decimal_between.js +5 -0
- package/src/predicate/decimal_equals.js +5 -0
- package/src/predicate/decimal_field_equals_field.js +5 -0
- package/src/predicate/decimal_field_greater_than_field.js +5 -0
- package/src/predicate/decimal_greater_than.js +5 -0
- package/src/predicate/decimal_less_than.js +5 -0
- package/src/predicate/decimal_precision.js +5 -0
- package/src/predicate/decimal_scale.js +5 -0
- package/src/predicate/decimal_valid.js +5 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## [1.0.0] - 2026-07-20
|
|
4
|
+
|
|
5
|
+
- Added decimal grammar parser for string-only decimal values.
|
|
6
|
+
- Added check and predicate operators for validation, scale, precision, numeric
|
|
7
|
+
equality, comparisons, inclusive ranges, and field-to-field comparisons.
|
|
8
|
+
- Added contract, profile, pack API, integration, and package smoke tests.
|
|
9
|
+
- Added trusted publishing release workflow.
|
package/COMPATIBILITY.md
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# Compatibility Policy
|
|
2
|
+
|
|
3
|
+
`@jsonspecs/decimal` follows SemVer.
|
|
4
|
+
|
|
5
|
+
## Breaking Changes
|
|
6
|
+
|
|
7
|
+
A major version is required for:
|
|
8
|
+
|
|
9
|
+
- changing accepted decimal grammar;
|
|
10
|
+
- accepting JavaScript numbers as decimal values;
|
|
11
|
+
- changing any operator name or required rule parameter;
|
|
12
|
+
- changing `value_field` naming for field-to-field operators;
|
|
13
|
+
- changing check or predicate status semantics for missing, invalid, or
|
|
14
|
+
misconfigured inputs;
|
|
15
|
+
- adding rounding or coercion to existing operators;
|
|
16
|
+
- raising the minimum supported `jsonspecs` peer range.
|
|
17
|
+
|
|
18
|
+
## Non-Breaking Changes
|
|
19
|
+
|
|
20
|
+
A minor version may add new operators or optional parameters that preserve v1
|
|
21
|
+
behavior. A patch version may fix implementation bugs, metadata, tests,
|
|
22
|
+
packaging, or documentation without changing documented semantics.
|
|
23
|
+
|
|
24
|
+
## Runtime Contract
|
|
25
|
+
|
|
26
|
+
The package intentionally has no runtime dependencies. Source files under
|
|
27
|
+
`src/**` must stay deterministic, synchronous, and portable under the package
|
|
28
|
+
profile test.
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 jsonspecs contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
# @jsonspecs/decimal
|
|
2
|
+
|
|
3
|
+
[](https://github.com/jsonspecs/decimal/actions/workflows/release.yml)
|
|
4
|
+
[](https://www.npmjs.com/package/@jsonspecs/decimal)
|
|
5
|
+
[](LICENSE)
|
|
6
|
+
[](https://nodejs.org/)
|
|
7
|
+
|
|
8
|
+
Decimal string operator pack for `jsonspecs` financial validations.
|
|
9
|
+
|
|
10
|
+
The pack validates and compares decimal values represented as strings. It never
|
|
11
|
+
accepts JavaScript numbers as decimal input because a number may already have
|
|
12
|
+
lost scale or precision before an operator sees it.
|
|
13
|
+
|
|
14
|
+
## Installation
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
npm install @jsonspecs/decimal jsonspecs
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Usage
|
|
21
|
+
|
|
22
|
+
```js
|
|
23
|
+
const { createEngine, Operators } = require("jsonspecs");
|
|
24
|
+
const decimal = require("@jsonspecs/decimal");
|
|
25
|
+
|
|
26
|
+
const engine = createEngine({
|
|
27
|
+
operators: {
|
|
28
|
+
check: { ...Operators.check, ...decimal.check },
|
|
29
|
+
predicate: { ...Operators.predicate, ...decimal.predicate },
|
|
30
|
+
},
|
|
31
|
+
});
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Decimal Grammar
|
|
35
|
+
|
|
36
|
+
Accepted decimal values are strings matching:
|
|
37
|
+
|
|
38
|
+
```text
|
|
39
|
+
^-?\d+(\.\d+)?$
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Allowed: `"12500.50"`, `"-0.01"`, `"007"`.
|
|
43
|
+
|
|
44
|
+
Rejected: `"+5"`, `"1e3"`, `".5"`, `"5."`, `"12 500"`, `"Infinity"`,
|
|
45
|
+
`""`, and every JavaScript number value.
|
|
46
|
+
|
|
47
|
+
## Operators
|
|
48
|
+
|
|
49
|
+
All operators are available as both checks and predicates.
|
|
50
|
+
|
|
51
|
+
| Operator | Parameters | Semantics |
|
|
52
|
+
| --- | --- | --- |
|
|
53
|
+
| `decimal_valid` | `field` | Field value is a string matching the decimal grammar. |
|
|
54
|
+
| `decimal_scale` | `field`, one of `max` or `exact` | Fractional digit count is `<= max` or `== exact`; `"10.5"` and `"10.50"` differ. |
|
|
55
|
+
| `decimal_precision` | `field`, `max` | BigDecimal-style unscaled precision without leading zeroes. |
|
|
56
|
+
| `decimal_equals` | `field`, `value` | Numeric equality; `"10.50"` equals `"10.5"`. |
|
|
57
|
+
| `decimal_greater_than` | `field`, `value` | Strict numeric greater-than. |
|
|
58
|
+
| `decimal_less_than` | `field`, `value` | Strict numeric less-than. |
|
|
59
|
+
| `decimal_between` | `field`, `min`, `max` | Inclusive numeric range. |
|
|
60
|
+
| `decimal_field_greater_than_field` | `field`, `value_field` | Field is numerically greater than another field. |
|
|
61
|
+
| `decimal_field_equals_field` | `field`, `value_field` | Field is numerically equal to another field. |
|
|
62
|
+
|
|
63
|
+
Rule parameter misconfiguration returns `EXCEPTION`. Invalid data returns
|
|
64
|
+
`FAIL` for checks and `FALSE` for predicates. A missing target field returns
|
|
65
|
+
`FAIL` for checks and `UNDEFINED` for predicates.
|
|
66
|
+
|
|
67
|
+
## Currency Dictionaries
|
|
68
|
+
|
|
69
|
+
Use core `in_dictionary` for currency allow-lists instead of a custom operator:
|
|
70
|
+
|
|
71
|
+
```json
|
|
72
|
+
{
|
|
73
|
+
"id": "currency_codes",
|
|
74
|
+
"type": "dictionary",
|
|
75
|
+
"entries": ["RUB", "USD", "EUR"]
|
|
76
|
+
}
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
## Roadmap
|
|
80
|
+
|
|
81
|
+
Not included in v1:
|
|
82
|
+
|
|
83
|
+
- `money_sum_equals`: requires wildcard materialization semantics outside this
|
|
84
|
+
primitive pack.
|
|
85
|
+
- `decimal_multiple_of`: no confirmed demand for v1.
|
|
86
|
+
- `currency_allowed`: use core `in_dictionary`.
|
|
87
|
+
- `money_valid`: compose from `decimal_valid`, `decimal_scale`, and dictionary
|
|
88
|
+
checks.
|
|
89
|
+
|
|
90
|
+
## Release
|
|
91
|
+
|
|
92
|
+
Publishing is handled by `.github/workflows/release.yml` on tags matching `v*`.
|
|
93
|
+
Before pushing the first release tag, configure npm trusted publishing for
|
|
94
|
+
`@jsonspecs/decimal` in this GitHub repository.
|
package/SPEC_RU.md
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
# @jsonspecs/decimal: спецификация операторов
|
|
2
|
+
|
|
3
|
+
Пак содержит универсальные финансовые decimal-примитивы для `jsonspecs`.
|
|
4
|
+
|
|
5
|
+
## 1. Decimal-строка
|
|
6
|
+
|
|
7
|
+
Единственная валидная форма decimal-значения:
|
|
8
|
+
|
|
9
|
+
```text
|
|
10
|
+
^-?\d+(\.\d+)?$
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Знак `+` не допускается. Экспонента не допускается. Формы `.5` и `5.` не
|
|
14
|
+
допускаются. Ведущие нули допускаются: `"007.50"` валидно.
|
|
15
|
+
|
|
16
|
+
Пак принимает только строки. Значение поля типа `number` возвращает `FAIL` в
|
|
17
|
+
check-операторах и `FALSE` в predicate-операторах. Причина: `number` мог
|
|
18
|
+
потерять scale и точность до запуска правил, а принятие такого значения
|
|
19
|
+
легализовало бы проблему, которую пакет устраняет.
|
|
20
|
+
|
|
21
|
+
Внутренний parser: `src/parser/decimal.js`. Сравнение выполняется через
|
|
22
|
+
выравнивание scale добавлением нулей справа и сравнение `BigInt`. Округления в
|
|
23
|
+
паке нет.
|
|
24
|
+
|
|
25
|
+
## 2. Общая граница ошибок
|
|
26
|
+
|
|
27
|
+
Мисконфигурация правила является ошибкой автора правила и возвращает
|
|
28
|
+
`EXCEPTION`: отсутствующий обязательный параметр, параметр неверного типа,
|
|
29
|
+
невалидная decimal-строка в параметре правила, одновременно заданные `max` и
|
|
30
|
+
`exact`, а также `min > max` в `decimal_between`.
|
|
31
|
+
|
|
32
|
+
Невалидные данные возвращают `FAIL` для check и `FALSE` для predicate.
|
|
33
|
+
Отсутствующее поле данных возвращает `FAIL` для check и `UNDEFINED` для
|
|
34
|
+
predicate.
|
|
35
|
+
|
|
36
|
+
## 3. Операторы
|
|
37
|
+
|
|
38
|
+
### `decimal_valid`
|
|
39
|
+
|
|
40
|
+
Параметры: `field`.
|
|
41
|
+
|
|
42
|
+
Значение поля должно быть строкой по грамматике раздела 1.
|
|
43
|
+
|
|
44
|
+
### `decimal_scale`
|
|
45
|
+
|
|
46
|
+
Параметры: `field` и ровно один из `max` или `exact`.
|
|
47
|
+
|
|
48
|
+
`max` и `exact` должны быть неотрицательными целыми числами. Scale равен числу
|
|
49
|
+
знаков после точки в исходной строке. `"10.5"` имеет scale 1, `"10.50"` имеет
|
|
50
|
+
scale 2.
|
|
51
|
+
|
|
52
|
+
### `decimal_precision`
|
|
53
|
+
|
|
54
|
+
Параметры: `field`, `max`.
|
|
55
|
+
|
|
56
|
+
`max` должен быть положительным целым числом. Precision считается по семантике
|
|
57
|
+
BigDecimal: число цифр unscaled value без ведущих нулей. Если все цифры равны
|
|
58
|
+
нулю, precision равен 1. Примеры: `"0.50"` -> 2, `"12500.50"` -> 7.
|
|
59
|
+
|
|
60
|
+
### `decimal_equals`
|
|
61
|
+
|
|
62
|
+
Параметры: `field`, `value`.
|
|
63
|
+
|
|
64
|
+
`value` должен быть decimal-строкой. Сравнение численное: `"10.50"` равно
|
|
65
|
+
`"10.5"`.
|
|
66
|
+
|
|
67
|
+
### `decimal_greater_than` / `decimal_less_than`
|
|
68
|
+
|
|
69
|
+
Параметры: `field`, `value`.
|
|
70
|
+
|
|
71
|
+
Строгие численные сравнения с decimal-строкой из `value`.
|
|
72
|
+
|
|
73
|
+
### `decimal_between`
|
|
74
|
+
|
|
75
|
+
Параметры: `field`, `min`, `max`.
|
|
76
|
+
|
|
77
|
+
Обе границы включены. `min` и `max` должны быть decimal-строками. Если
|
|
78
|
+
`min > max`, оператор возвращает `EXCEPTION`.
|
|
79
|
+
|
|
80
|
+
### `decimal_field_greater_than_field` / `decimal_field_equals_field`
|
|
81
|
+
|
|
82
|
+
Параметры: `field`, `value_field`.
|
|
83
|
+
|
|
84
|
+
Имя второго поля зеркалит контракт ядровых `field_*_field` операторов:
|
|
85
|
+
используется `value_field`. Оба значения данных должны быть строками по
|
|
86
|
+
decimal-грамматике, иначе результат `FAIL` для check и `FALSE` для predicate.
|
|
87
|
+
|
|
88
|
+
## 4. Не входит в v1
|
|
89
|
+
|
|
90
|
+
`money_sum_equals` не включён, потому что требует отдельной семантики
|
|
91
|
+
материализации wildcard. `decimal_multiple_of` не включён из-за отсутствия
|
|
92
|
+
подтверждённого спроса для v1. `currency_allowed` должен выражаться ядровым
|
|
93
|
+
`in_dictionary`. `money_valid` собирается правилами из `decimal_valid`,
|
|
94
|
+
`decimal_scale` и словаря валют.
|
package/index.d.ts
ADDED
package/index.js
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const pkg = require("./package.json");
|
|
4
|
+
const decimal = require("./src/parser/decimal");
|
|
5
|
+
|
|
6
|
+
const checkDecimalValid = require("./src/check/decimal_valid");
|
|
7
|
+
const checkDecimalScale = require("./src/check/decimal_scale");
|
|
8
|
+
const checkDecimalPrecision = require("./src/check/decimal_precision");
|
|
9
|
+
const checkDecimalEquals = require("./src/check/decimal_equals");
|
|
10
|
+
const checkDecimalGreaterThan = require("./src/check/decimal_greater_than");
|
|
11
|
+
const checkDecimalLessThan = require("./src/check/decimal_less_than");
|
|
12
|
+
const checkDecimalBetween = require("./src/check/decimal_between");
|
|
13
|
+
const checkDecimalFieldGreaterThanField = require("./src/check/decimal_field_greater_than_field");
|
|
14
|
+
const checkDecimalFieldEqualsField = require("./src/check/decimal_field_equals_field");
|
|
15
|
+
|
|
16
|
+
const predicateDecimalValid = require("./src/predicate/decimal_valid");
|
|
17
|
+
const predicateDecimalScale = require("./src/predicate/decimal_scale");
|
|
18
|
+
const predicateDecimalPrecision = require("./src/predicate/decimal_precision");
|
|
19
|
+
const predicateDecimalEquals = require("./src/predicate/decimal_equals");
|
|
20
|
+
const predicateDecimalGreaterThan = require("./src/predicate/decimal_greater_than");
|
|
21
|
+
const predicateDecimalLessThan = require("./src/predicate/decimal_less_than");
|
|
22
|
+
const predicateDecimalBetween = require("./src/predicate/decimal_between");
|
|
23
|
+
const predicateDecimalFieldGreaterThanField = require("./src/predicate/decimal_field_greater_than_field");
|
|
24
|
+
const predicateDecimalFieldEqualsField = require("./src/predicate/decimal_field_equals_field");
|
|
25
|
+
|
|
26
|
+
function deepFreeze(value, seen = new Set()) {
|
|
27
|
+
if (!value || (typeof value !== "object" && typeof value !== "function")) {
|
|
28
|
+
return value;
|
|
29
|
+
}
|
|
30
|
+
if (seen.has(value)) return value;
|
|
31
|
+
seen.add(value);
|
|
32
|
+
for (const key of Reflect.ownKeys(value)) {
|
|
33
|
+
deepFreeze(value[key], seen);
|
|
34
|
+
}
|
|
35
|
+
return Object.freeze(value);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const check = {
|
|
39
|
+
decimal_valid: bind(checkDecimalValid),
|
|
40
|
+
decimal_scale: bind(checkDecimalScale),
|
|
41
|
+
decimal_precision: bind(checkDecimalPrecision),
|
|
42
|
+
decimal_equals: bind(checkDecimalEquals),
|
|
43
|
+
decimal_greater_than: bind(checkDecimalGreaterThan),
|
|
44
|
+
decimal_less_than: bind(checkDecimalLessThan),
|
|
45
|
+
decimal_between: bind(checkDecimalBetween),
|
|
46
|
+
decimal_field_greater_than_field: bind(checkDecimalFieldGreaterThanField),
|
|
47
|
+
decimal_field_equals_field: bind(checkDecimalFieldEqualsField),
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const predicate = {
|
|
51
|
+
decimal_valid: bind(predicateDecimalValid),
|
|
52
|
+
decimal_scale: bind(predicateDecimalScale),
|
|
53
|
+
decimal_precision: bind(predicateDecimalPrecision),
|
|
54
|
+
decimal_equals: bind(predicateDecimalEquals),
|
|
55
|
+
decimal_greater_than: bind(predicateDecimalGreaterThan),
|
|
56
|
+
decimal_less_than: bind(predicateDecimalLessThan),
|
|
57
|
+
decimal_between: bind(predicateDecimalBetween),
|
|
58
|
+
decimal_field_greater_than_field: bind(predicateDecimalFieldGreaterThanField),
|
|
59
|
+
decimal_field_equals_field: bind(predicateDecimalFieldEqualsField),
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
const meta = {
|
|
63
|
+
pack: "@jsonspecs/decimal",
|
|
64
|
+
version: pkg.version,
|
|
65
|
+
operators: {
|
|
66
|
+
decimal_valid: {
|
|
67
|
+
title: "decimal string is valid",
|
|
68
|
+
description: "Field is a string matching the canonical decimal grammar.",
|
|
69
|
+
template: "{field} is a valid decimal string",
|
|
70
|
+
},
|
|
71
|
+
decimal_scale: {
|
|
72
|
+
title: "decimal scale is allowed",
|
|
73
|
+
description: "Field has either no more than max fractional digits or exactly exact fractional digits.",
|
|
74
|
+
template: "{field} has the required decimal scale",
|
|
75
|
+
},
|
|
76
|
+
decimal_precision: {
|
|
77
|
+
title: "decimal precision is allowed",
|
|
78
|
+
description: "Field precision does not exceed max using BigDecimal-style unscaled precision.",
|
|
79
|
+
template: "{field} precision is at most {max}",
|
|
80
|
+
},
|
|
81
|
+
decimal_equals: {
|
|
82
|
+
title: "decimal equals value",
|
|
83
|
+
description: "Field is numerically equal to a decimal rule value.",
|
|
84
|
+
template: "{field} equals {value}",
|
|
85
|
+
},
|
|
86
|
+
decimal_greater_than: {
|
|
87
|
+
title: "decimal is greater than value",
|
|
88
|
+
description: "Field is numerically greater than a decimal rule value.",
|
|
89
|
+
template: "{field} is greater than {value}",
|
|
90
|
+
},
|
|
91
|
+
decimal_less_than: {
|
|
92
|
+
title: "decimal is less than value",
|
|
93
|
+
description: "Field is numerically less than a decimal rule value.",
|
|
94
|
+
template: "{field} is less than {value}",
|
|
95
|
+
},
|
|
96
|
+
decimal_between: {
|
|
97
|
+
title: "decimal is between values",
|
|
98
|
+
description: "Field is numerically between min and max, inclusive.",
|
|
99
|
+
template: "{field} is between {min} and {max}",
|
|
100
|
+
},
|
|
101
|
+
decimal_field_greater_than_field: {
|
|
102
|
+
title: "decimal field is greater than field",
|
|
103
|
+
description: "Field is numerically greater than value_field.",
|
|
104
|
+
template: "{field} is greater than {value_field}",
|
|
105
|
+
},
|
|
106
|
+
decimal_field_equals_field: {
|
|
107
|
+
title: "decimal field equals field",
|
|
108
|
+
description: "Field is numerically equal to value_field.",
|
|
109
|
+
template: "{field} equals {value_field}",
|
|
110
|
+
},
|
|
111
|
+
},
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
function bind(operator) {
|
|
115
|
+
return (rule, ctx) => operator(rule, ctx, decimal);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
module.exports = deepFreeze({ check, predicate, meta });
|
package/package.json
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@jsonspecs/decimal",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Decimal string operator pack for jsonspecs financial validations",
|
|
5
|
+
"type": "commonjs",
|
|
6
|
+
"main": "index.js",
|
|
7
|
+
"types": "./index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./index.d.ts",
|
|
11
|
+
"require": "./index.js",
|
|
12
|
+
"default": "./index.js"
|
|
13
|
+
},
|
|
14
|
+
"./package.json": "./package.json"
|
|
15
|
+
},
|
|
16
|
+
"files": [
|
|
17
|
+
"src/",
|
|
18
|
+
"index.js",
|
|
19
|
+
"index.d.ts",
|
|
20
|
+
"README.md",
|
|
21
|
+
"SPEC_RU.md",
|
|
22
|
+
"COMPATIBILITY.md",
|
|
23
|
+
"CHANGELOG.md",
|
|
24
|
+
"LICENSE"
|
|
25
|
+
],
|
|
26
|
+
"scripts": {
|
|
27
|
+
"test": "node --test tests/*.test.js",
|
|
28
|
+
"test:pack": "node scripts/pack-smoke.js",
|
|
29
|
+
"prepublishOnly": "npm test && npm run test:pack"
|
|
30
|
+
},
|
|
31
|
+
"keywords": [
|
|
32
|
+
"jsonspecs",
|
|
33
|
+
"jsonspecs-operators",
|
|
34
|
+
"operator-pack",
|
|
35
|
+
"rules-engine"
|
|
36
|
+
],
|
|
37
|
+
"author": "jsonspecs contributors",
|
|
38
|
+
"license": "MIT",
|
|
39
|
+
"repository": {
|
|
40
|
+
"type": "git",
|
|
41
|
+
"url": "git+https://github.com/jsonspecs/decimal.git"
|
|
42
|
+
},
|
|
43
|
+
"bugs": {
|
|
44
|
+
"url": "https://github.com/jsonspecs/decimal/issues"
|
|
45
|
+
},
|
|
46
|
+
"homepage": "https://github.com/jsonspecs/decimal#readme",
|
|
47
|
+
"engines": {
|
|
48
|
+
"node": ">=20"
|
|
49
|
+
},
|
|
50
|
+
"peerDependencies": {
|
|
51
|
+
"jsonspecs": "^2.0.0"
|
|
52
|
+
},
|
|
53
|
+
"devDependencies": {
|
|
54
|
+
"jsonspecs": "^2.3.0"
|
|
55
|
+
}
|
|
56
|
+
}
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const DECIMAL_RE = /^-?\d+(?:\.\d+)?$/;
|
|
4
|
+
|
|
5
|
+
function exception(message) {
|
|
6
|
+
return { status: "EXCEPTION", error: new Error(message) };
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function hasOwn(object, key) {
|
|
10
|
+
return Object.prototype.hasOwnProperty.call(object, key);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function requireField(rule) {
|
|
14
|
+
return typeof rule.field === "string" && rule.field.length > 0;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function requireValueField(rule) {
|
|
18
|
+
return typeof rule.value_field === "string" && rule.value_field.length > 0;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function parseDecimal(value) {
|
|
22
|
+
if (typeof value !== "string") return null;
|
|
23
|
+
if (!DECIMAL_RE.test(value)) return null;
|
|
24
|
+
const negative = value[0] === "-";
|
|
25
|
+
const body = negative ? value.slice(1) : value;
|
|
26
|
+
const dot = body.indexOf(".");
|
|
27
|
+
const intDigits = dot === -1 ? body : body.slice(0, dot);
|
|
28
|
+
const fracDigits = dot === -1 ? "" : body.slice(dot + 1);
|
|
29
|
+
return { negative, intDigits, fracDigits };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function scaleOf(parsed) {
|
|
33
|
+
return parsed.fracDigits.length;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function precisionOf(parsed) {
|
|
37
|
+
const unscaled = `${parsed.intDigits}${parsed.fracDigits}`;
|
|
38
|
+
const trimmed = unscaled.replace(/^0+/, "");
|
|
39
|
+
return trimmed.length === 0 ? 1 : trimmed.length;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function scaledInteger(parsed, scale) {
|
|
43
|
+
const digits = `${parsed.intDigits}${parsed.fracDigits.padEnd(scale, "0")}`;
|
|
44
|
+
const magnitude = BigInt(digits);
|
|
45
|
+
if (magnitude === 0n) return 0n;
|
|
46
|
+
return parsed.negative ? -magnitude : magnitude;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function compareParsed(left, right) {
|
|
50
|
+
const scale = Math.max(left.fracDigits.length, right.fracDigits.length);
|
|
51
|
+
const a = scaledInteger(left, scale);
|
|
52
|
+
const b = scaledInteger(right, scale);
|
|
53
|
+
if (a === b) return 0;
|
|
54
|
+
return a > b ? 1 : -1;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function statusFromCompare(cmp, op) {
|
|
58
|
+
if (op === "eq") return cmp === 0;
|
|
59
|
+
if (op === "gt") return cmp > 0;
|
|
60
|
+
if (op === "lt") return cmp < 0;
|
|
61
|
+
return false;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function validateScalarRule(rule, parameter) {
|
|
65
|
+
if (!requireField(rule)) return exception("field is required");
|
|
66
|
+
if (!hasOwn(rule, parameter)) return exception(`${parameter} is required`);
|
|
67
|
+
const parsed = parseDecimal(rule[parameter]);
|
|
68
|
+
if (!parsed) return exception(`${parameter} must be a decimal string`);
|
|
69
|
+
return { ok: true, parsed };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function checkValid(rule, ctx) {
|
|
73
|
+
if (!requireField(rule)) return exception("field is required");
|
|
74
|
+
const got = ctx.get(rule.field);
|
|
75
|
+
if (!got.ok) return { status: "FAIL" };
|
|
76
|
+
return { status: parseDecimal(got.value) ? "OK" : "FAIL", actual: got.value };
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function predicateValid(rule, ctx) {
|
|
80
|
+
if (!requireField(rule)) return exception("field is required");
|
|
81
|
+
const got = ctx.get(rule.field);
|
|
82
|
+
if (!got.ok) return { status: "UNDEFINED" };
|
|
83
|
+
return { status: parseDecimal(got.value) ? "TRUE" : "FALSE" };
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function getScaleConfig(rule) {
|
|
87
|
+
const hasMax = hasOwn(rule, "max");
|
|
88
|
+
const hasExact = hasOwn(rule, "exact");
|
|
89
|
+
if (hasMax === hasExact) return exception("exactly one of max or exact is required");
|
|
90
|
+
const key = hasMax ? "max" : "exact";
|
|
91
|
+
const value = rule[key];
|
|
92
|
+
if (typeof value !== "number" || !Number.isInteger(value) || value < 0) {
|
|
93
|
+
return exception(`${key} must be a non-negative integer`);
|
|
94
|
+
}
|
|
95
|
+
return { ok: true, key, value };
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function checkScale(rule, ctx) {
|
|
99
|
+
if (!requireField(rule)) return exception("field is required");
|
|
100
|
+
const config = getScaleConfig(rule);
|
|
101
|
+
if (!config.ok) return config;
|
|
102
|
+
const got = ctx.get(rule.field);
|
|
103
|
+
if (!got.ok) return { status: "FAIL" };
|
|
104
|
+
const parsed = parseDecimal(got.value);
|
|
105
|
+
if (!parsed) return { status: "FAIL", actual: got.value };
|
|
106
|
+
const scale = scaleOf(parsed);
|
|
107
|
+
const ok = config.key === "max" ? scale <= config.value : scale === config.value;
|
|
108
|
+
return { status: ok ? "OK" : "FAIL", actual: got.value, meta: { scale } };
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function predicateScale(rule, ctx) {
|
|
112
|
+
if (!requireField(rule)) return exception("field is required");
|
|
113
|
+
const config = getScaleConfig(rule);
|
|
114
|
+
if (!config.ok) return config;
|
|
115
|
+
const got = ctx.get(rule.field);
|
|
116
|
+
if (!got.ok) return { status: "UNDEFINED" };
|
|
117
|
+
const parsed = parseDecimal(got.value);
|
|
118
|
+
if (!parsed) return { status: "FALSE" };
|
|
119
|
+
const scale = scaleOf(parsed);
|
|
120
|
+
const ok = config.key === "max" ? scale <= config.value : scale === config.value;
|
|
121
|
+
return { status: ok ? "TRUE" : "FALSE" };
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function checkPrecision(rule, ctx) {
|
|
125
|
+
if (!requireField(rule)) return exception("field is required");
|
|
126
|
+
if (typeof rule.max !== "number" || !Number.isInteger(rule.max) || rule.max < 1) {
|
|
127
|
+
return exception("max must be a positive integer");
|
|
128
|
+
}
|
|
129
|
+
const got = ctx.get(rule.field);
|
|
130
|
+
if (!got.ok) return { status: "FAIL" };
|
|
131
|
+
const parsed = parseDecimal(got.value);
|
|
132
|
+
if (!parsed) return { status: "FAIL", actual: got.value };
|
|
133
|
+
const precision = precisionOf(parsed);
|
|
134
|
+
return { status: precision <= rule.max ? "OK" : "FAIL", actual: got.value, meta: { precision } };
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function predicatePrecision(rule, ctx) {
|
|
138
|
+
if (!requireField(rule)) return exception("field is required");
|
|
139
|
+
if (typeof rule.max !== "number" || !Number.isInteger(rule.max) || rule.max < 1) {
|
|
140
|
+
return exception("max must be a positive integer");
|
|
141
|
+
}
|
|
142
|
+
const got = ctx.get(rule.field);
|
|
143
|
+
if (!got.ok) return { status: "UNDEFINED" };
|
|
144
|
+
const parsed = parseDecimal(got.value);
|
|
145
|
+
if (!parsed) return { status: "FALSE" };
|
|
146
|
+
return { status: precisionOf(parsed) <= rule.max ? "TRUE" : "FALSE" };
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function checkScalarCompare(rule, ctx, op) {
|
|
150
|
+
const config = validateScalarRule(rule, "value");
|
|
151
|
+
if (!config.ok) return config;
|
|
152
|
+
const got = ctx.get(rule.field);
|
|
153
|
+
if (!got.ok) return { status: "FAIL" };
|
|
154
|
+
const parsed = parseDecimal(got.value);
|
|
155
|
+
if (!parsed) return { status: "FAIL", actual: got.value };
|
|
156
|
+
const ok = statusFromCompare(compareParsed(parsed, config.parsed), op);
|
|
157
|
+
return { status: ok ? "OK" : "FAIL", actual: got.value };
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function predicateScalarCompare(rule, ctx, op) {
|
|
161
|
+
const config = validateScalarRule(rule, "value");
|
|
162
|
+
if (!config.ok) return config;
|
|
163
|
+
const got = ctx.get(rule.field);
|
|
164
|
+
if (!got.ok) return { status: "UNDEFINED" };
|
|
165
|
+
const parsed = parseDecimal(got.value);
|
|
166
|
+
if (!parsed) return { status: "FALSE" };
|
|
167
|
+
const ok = statusFromCompare(compareParsed(parsed, config.parsed), op);
|
|
168
|
+
return { status: ok ? "TRUE" : "FALSE" };
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function checkBetween(rule, ctx) {
|
|
172
|
+
const minConfig = validateScalarRule(rule, "min");
|
|
173
|
+
if (!minConfig.ok) return minConfig;
|
|
174
|
+
if (!hasOwn(rule, "max")) return exception("max is required");
|
|
175
|
+
const max = parseDecimal(rule.max);
|
|
176
|
+
if (!max) return exception("max must be a decimal string");
|
|
177
|
+
if (compareParsed(minConfig.parsed, max) > 0) return exception("min must be less than or equal to max");
|
|
178
|
+
const got = ctx.get(rule.field);
|
|
179
|
+
if (!got.ok) return { status: "FAIL" };
|
|
180
|
+
const parsed = parseDecimal(got.value);
|
|
181
|
+
if (!parsed) return { status: "FAIL", actual: got.value };
|
|
182
|
+
const ok = compareParsed(parsed, minConfig.parsed) >= 0 && compareParsed(parsed, max) <= 0;
|
|
183
|
+
return { status: ok ? "OK" : "FAIL", actual: got.value };
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function predicateBetween(rule, ctx) {
|
|
187
|
+
const minConfig = validateScalarRule(rule, "min");
|
|
188
|
+
if (!minConfig.ok) return minConfig;
|
|
189
|
+
if (!hasOwn(rule, "max")) return exception("max is required");
|
|
190
|
+
const max = parseDecimal(rule.max);
|
|
191
|
+
if (!max) return exception("max must be a decimal string");
|
|
192
|
+
if (compareParsed(minConfig.parsed, max) > 0) return exception("min must be less than or equal to max");
|
|
193
|
+
const got = ctx.get(rule.field);
|
|
194
|
+
if (!got.ok) return { status: "UNDEFINED" };
|
|
195
|
+
const parsed = parseDecimal(got.value);
|
|
196
|
+
if (!parsed) return { status: "FALSE" };
|
|
197
|
+
const ok = compareParsed(parsed, minConfig.parsed) >= 0 && compareParsed(parsed, max) <= 0;
|
|
198
|
+
return { status: ok ? "TRUE" : "FALSE" };
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function checkFieldCompare(rule, ctx, op) {
|
|
202
|
+
if (!requireField(rule)) return exception("field is required");
|
|
203
|
+
if (!requireValueField(rule)) return exception("value_field is required");
|
|
204
|
+
const left = ctx.get(rule.field);
|
|
205
|
+
const right = ctx.get(rule.value_field);
|
|
206
|
+
if (!left.ok || !right.ok) return { status: "FAIL" };
|
|
207
|
+
const a = parseDecimal(left.value);
|
|
208
|
+
const b = parseDecimal(right.value);
|
|
209
|
+
if (!a || !b) return { status: "FAIL", actual: left.value };
|
|
210
|
+
const ok = statusFromCompare(compareParsed(a, b), op);
|
|
211
|
+
return { status: ok ? "OK" : "FAIL", actual: left.value };
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function predicateFieldCompare(rule, ctx, op) {
|
|
215
|
+
if (!requireField(rule)) return exception("field is required");
|
|
216
|
+
if (!requireValueField(rule)) return exception("value_field is required");
|
|
217
|
+
const left = ctx.get(rule.field);
|
|
218
|
+
const right = ctx.get(rule.value_field);
|
|
219
|
+
if (!left.ok || !right.ok) return { status: "UNDEFINED" };
|
|
220
|
+
const a = parseDecimal(left.value);
|
|
221
|
+
const b = parseDecimal(right.value);
|
|
222
|
+
if (!a || !b) return { status: "FALSE" };
|
|
223
|
+
const ok = statusFromCompare(compareParsed(a, b), op);
|
|
224
|
+
return { status: ok ? "TRUE" : "FALSE" };
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
module.exports = {
|
|
228
|
+
checkBetween,
|
|
229
|
+
checkFieldCompare,
|
|
230
|
+
checkPrecision,
|
|
231
|
+
checkScalarCompare,
|
|
232
|
+
checkScale,
|
|
233
|
+
checkValid,
|
|
234
|
+
compareParsed,
|
|
235
|
+
parseDecimal,
|
|
236
|
+
predicateBetween,
|
|
237
|
+
predicateFieldCompare,
|
|
238
|
+
predicatePrecision,
|
|
239
|
+
predicateScalarCompare,
|
|
240
|
+
predicateScale,
|
|
241
|
+
predicateValid,
|
|
242
|
+
precisionOf,
|
|
243
|
+
scaleOf,
|
|
244
|
+
};
|