🔥 Utilise les paquets publicodes depuis NPM
parent
cc50f8c3e7
commit
d5979264d2
|
@ -67,7 +67,7 @@ module.exports = {
|
|||
"jsx": true
|
||||
},
|
||||
"tsconfigRootDir": __dirname,
|
||||
"project": [ "./mon-entreprise/tsconfig.json", "./publicodes/tsconfig.json" ]
|
||||
"project": [ "./mon-entreprise/tsconfig.json" ]
|
||||
},
|
||||
plugins: [ "@typescript-eslint" ],
|
||||
rules: {
|
||||
|
|
|
@ -100,6 +100,12 @@ $ yarn lint
|
|||
$ yarn test:type
|
||||
```
|
||||
|
||||
Pour avoir les erreurs de type en direct dans la console, utilisez le paramètre `--watch` :
|
||||
|
||||
```sh
|
||||
$ yarn test:type --watch
|
||||
```
|
||||
|
||||
#### Tests unitaires
|
||||
|
||||
```sh
|
||||
|
@ -196,7 +202,7 @@ Pour se familiariser avec les règles, vous pouvez jeter un œil aux fichiers
|
|||
contenant les règles elles-mêmes (dans le dossier `modele-social`) mais cela
|
||||
peut s'avérer assez abrupt.
|
||||
|
||||
Essayez plutôt de jeter un oeil [aux tests](https://github.com/betagouv/publicodes/tree/master/core/test/m%C3%A9canismes)
|
||||
Essayez plutôt de jeter un œil [aux tests](https://github.com/betagouv/publicodes/tree/master/core/test/m%C3%A9canismes)
|
||||
dans un premier temps, et pourquoi pas à [à l'implémentation des mécanismes](https://github.com/betagouv/publicodes/tree/master/core/source/mecanisms).
|
||||
|
||||
### Traduction des normes (lois) en règles Publicodes
|
||||
|
@ -208,36 +214,3 @@ Checklist:
|
|||
- [ ] [Lire les normes][wiki normes] et noter leurs référence dans les règles Publicodes.
|
||||
|
||||
[wiki normes]: https://github.com/betagouv/mon-entreprise/wiki/Comment-lire-les-normes-(la-loi)-efficacement-pour-r%C3%A9diger-des-r%C3%A8gles-Publicodes%3F
|
||||
|
||||
### Modifier publicodes
|
||||
|
||||
Publicodes dispose désormais de son propre dépôt GitHub https://github.com/betagouv/publicodes
|
||||
|
||||
Néanmoins pour certaines nouvelles fonctionnalités de mon-entreprise nous concervons le besoin de
|
||||
modifier publicodes avec le moins de frictions possible. Pour tester une évolution du moteur il
|
||||
serait en effet trop lourd d'avoir à ouvrir d'abord une PR côté publicodes, la merger, publier une
|
||||
nouvelle version du paquet, puis ré-intégrer cette nouvelle version sur mon-entreprise.
|
||||
|
||||
C'est pourquoi nous intégrons le code source du publicode dans le sous-répertoire `publicodes/`. La
|
||||
commande `git subtree` nous permet de synchroniser les changements effectués dans l'un ou l'autre
|
||||
des dépôts.
|
||||
|
||||
La première chose à faire est d'ajouter une nouvelle `remote` pour `betagouv/publicodes`, ici nous l'appelons simplement `publicodes` :
|
||||
|
||||
```sh
|
||||
git remote add publicodes git@github.com:betagouv/publicodes.git
|
||||
```
|
||||
|
||||
Ensuite il est possible de remonter les changements effectués dans le sous-repertoire `publicodes/` vers la branche master de la remote `publicodes`.
|
||||
|
||||
```sh
|
||||
$ git subtree push --prefix=publicodes publicodes master
|
||||
```
|
||||
|
||||
Dans l'autre sens il est possible de rapatrier les changements avec la commande
|
||||
|
||||
```sh
|
||||
$ git subtree pull --prefix=publicodes publicodes master --squash
|
||||
```
|
||||
|
||||
Les dépendances peuvent avoir changé côté publicodes, mieux vaut donc enchaîner avec un `yarn install` pour être à jour.
|
||||
|
|
|
@ -94,8 +94,8 @@
|
|||
"fuse.js": "^6.4.6",
|
||||
"iframe-resizer": "^4.1.1",
|
||||
"modele-social": "^0.3.0",
|
||||
"publicodes": "^1.0.0-beta.15",
|
||||
"publicodes-react": "^1.0.0-beta.15",
|
||||
"publicodes": "^1.0.0-beta.20",
|
||||
"publicodes-react": "^1.0.0-beta.20",
|
||||
"ramda": "^0.27.0",
|
||||
"react": "^17.0.0",
|
||||
"react-color": "^2.14.0",
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
import { expect } from 'chai'
|
||||
import rules from 'modele-social'
|
||||
import { cyclicDependencies } from '../../publicodes/core/source/AST/graph'
|
||||
import { utils } from 'publicodes'
|
||||
|
||||
describe('DottedNames graph', () => {
|
||||
it("shouldn't have cycles", () => {
|
||||
const [cyclesDependencies, dotGraphs] = cyclicDependencies(rules)
|
||||
const [cyclesDependencies, dotGraphs] = utils.cyclicDependencies(rules)
|
||||
|
||||
const dotGraphsToLog = dotGraphs
|
||||
.map(
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { AssertionError } from 'chai'
|
||||
import Engine, { parsePublicodes } from 'publicodes'
|
||||
import { disambiguateRuleReference } from '../../publicodes/core/source/ruleUtils'
|
||||
import utils from 'publicodes'
|
||||
import rules from 'modele-social'
|
||||
|
||||
// les variables dans les tests peuvent être exprimées relativement à l'espace de nom de la règle,
|
||||
|
@ -13,8 +13,11 @@ let runExamples = (examples, rule) =>
|
|||
const situation = Object.entries(ex.situation).reduce(
|
||||
(acc, [name, value]) => ({
|
||||
...acc,
|
||||
[disambiguateRuleReference(engine.parsedRules, rule.dottedName, name)]:
|
||||
value,
|
||||
[utils.disambiguateRuleReference(
|
||||
engine.parsedRules,
|
||||
rule.dottedName,
|
||||
name
|
||||
)]: value,
|
||||
}),
|
||||
{}
|
||||
)
|
||||
|
|
11
package.json
11
package.json
|
@ -90,7 +90,6 @@
|
|||
"serve": "^11.1.0",
|
||||
"sinon": "^9.2.2",
|
||||
"sinon-chai": "^3.0.0",
|
||||
"stmux": "^1.8.1",
|
||||
"style-loader": "^0.23.1",
|
||||
"terser-webpack-plugin": "^3.0.2",
|
||||
"toml-loader": "^1.0.0",
|
||||
|
@ -117,17 +116,13 @@
|
|||
"prepare": "if [ -z \"$NETLIFY\" ]; then yarn workspaces run prepare; fi",
|
||||
"lint": "yarn lint:eslintrc && yarn lint:eslint && yarn lint:prettier",
|
||||
"test": "yarn workspaces run test",
|
||||
"test:type": "yarn workspaces run tsc -- --skipLibCheck",
|
||||
"test:type": "yarn workspaces run tsc -- --skipLibCheck --noEmit",
|
||||
"test:regressions": "yarn workspace modele-social build && jest --silent",
|
||||
"clean": "yarn workspaces run clean && rimraf node_modules",
|
||||
"start": "stmux -e ERROR -t mon-entreprise-build -M -- [ -s 3/4 [ [ 'yarn workspace publicodes build:watch' : 'yarn workspace publicodes-react build --watch' ] .. [ 'yarn workspace mon-entreprise start' ] ] : [ 'yarn workspace mon-entreprise run tsc -- -w --noEmit --skipLibCheck' ] ]",
|
||||
"moso:up": "yarn workspace modele-social run up && yarn workspace mon-entreprise upgrade modele-social",
|
||||
"publicodes:up": "yarn workspace publicodes-react upgrade publicodes && yarn workspace mon-entreprise upgrade publicodes publicodes-react"
|
||||
"start": "yarn workspace mon-entreprise start",
|
||||
"moso:up": "yarn workspace modele-social run up && yarn workspace mon-entreprise upgrade modele-social"
|
||||
},
|
||||
"workspaces": [
|
||||
"publicodes/core",
|
||||
"publicodes/ui-react",
|
||||
"publicodes/site",
|
||||
"modele-social",
|
||||
"mon-entreprise"
|
||||
],
|
||||
|
|
|
@ -1,25 +0,0 @@
|
|||
root = true
|
||||
|
||||
[*]
|
||||
charset = utf-8
|
||||
end_of_line = lf
|
||||
trim_trailing_whitespace = true
|
||||
# We shall not define indent_size ⬇️ when using tabs.
|
||||
# tab_width doesn't make much sense as it can be left to the reader to decide.
|
||||
indent_style = tab
|
||||
insert_final_newline = true
|
||||
max_line_length = 80
|
||||
|
||||
[**.{js,jsx,ts,tsx}]
|
||||
indent_size = 2
|
||||
|
||||
[**.{yml,yaml}]
|
||||
# Spaces are mandatory for yaml files:
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
|
||||
[*.md]
|
||||
indent_style = space
|
||||
indent_size = 4
|
||||
trim_trailing_whitespace = false
|
||||
|
|
@ -1,76 +0,0 @@
|
|||
name: Test and Publish
|
||||
on: [push]
|
||||
|
||||
jobs:
|
||||
lint:
|
||||
runs-on: ubuntu-20.04
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/cache@v2
|
||||
with:
|
||||
path: '**/node_modules'
|
||||
key: ${{ runner.os }}-modules-${{ hashFiles('**/yarn.lock') }}-v2
|
||||
- run: yarn install --frozen-lockfile
|
||||
- run: yarn lint:prettier
|
||||
|
||||
test:
|
||||
name: Unit tests
|
||||
runs-on: ubuntu-20.04
|
||||
steps:
|
||||
- uses: actions/checkout@v1
|
||||
- uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: 14
|
||||
- run: yarn install
|
||||
- run: yarn test
|
||||
|
||||
test-type:
|
||||
name: Type checking
|
||||
runs-on: ubuntu-20.04
|
||||
steps:
|
||||
- uses: actions/checkout@v1
|
||||
- uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: 14
|
||||
- run: yarn install
|
||||
- run: yarn test:type
|
||||
|
||||
test-example-app:
|
||||
name: Test example app
|
||||
runs-on: ubuntu-20.04
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/cache@v2
|
||||
with:
|
||||
path: '**/node_modules'
|
||||
key: ${{ runner.os }}-modules-${{ hashFiles('**/yarn.lock') }}-v2
|
||||
- run: yarn install --frozen-lockfile
|
||||
- working-directory: ./example/publicodes-react
|
||||
run: |
|
||||
yarn install
|
||||
yarn test
|
||||
|
||||
# This job could be in a separate workflow triggered when all the tests passes
|
||||
# using the `workflow_run` event, but it makes it difficult to retrieve the
|
||||
# commit message.
|
||||
publish:
|
||||
if: contains(join(github.event.commits.*.message, ' | '), '📦 Publicodes v1.0.0-beta.')
|
||||
needs: [test, test-type, test-example-app]
|
||||
runs-on: ubuntu-20.04
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/cache@v2
|
||||
with:
|
||||
path: '**/node_modules'
|
||||
key: ${{ runner.os }}-modules-${{ hashFiles('**/yarn.lock') }}-v2
|
||||
- run: yarn install --frozen-lockfile
|
||||
- uses: JS-DevTools/npm-publish@v1
|
||||
with:
|
||||
token: ${{ secrets.NPM_PUBLISH_SECRET }}
|
||||
dry-run: ${{ github.ref != 'refs/heads/master' }}
|
||||
package: ./core/package.json
|
||||
- uses: JS-DevTools/npm-publish@v1
|
||||
with:
|
||||
token: ${{ secrets.NPM_PUBLISH_SECRET }}
|
||||
dry-run: ${{ github.ref != 'refs/heads/master' }}
|
||||
package: ./ui-react/package.json
|
|
@ -1,12 +0,0 @@
|
|||
.tags*
|
||||
.tmp
|
||||
/tmp
|
||||
.DS_Store
|
||||
yarn-error.log
|
||||
package-lock.json
|
||||
node_modules/
|
||||
.env
|
||||
dist/
|
||||
|
||||
# Local Netlify folder
|
||||
.netlify
|
|
@ -1,3 +0,0 @@
|
|||
bracketSpacing: true
|
||||
semi: false
|
||||
singleQuote: true
|
|
@ -1,36 +0,0 @@
|
|||
# Changelog
|
||||
|
||||
## 1.0.0-beta.13
|
||||
|
||||
**core**
|
||||
|
||||
- Ajout d'un nouveau mécanisme : `résoudre la référence circulaire` (#1472)
|
||||
- Simplification de l'API de Engine (#1431)
|
||||
|
||||
**publicodes-react**
|
||||
|
||||
- Améliore l'affichage des règles virtuelles dépliée dans une somme
|
||||
- Ajoute les meta dans les pages de règles (#1411)
|
||||
|
||||
## 1.0.0-beta.14
|
||||
|
||||
**publicodes-react**
|
||||
|
||||
- Corrige un bug bloquant qui empêchait l'utilisation de la bibliothèque
|
||||
- Enlève la dépendance à i18n et react-i18n et toute la traduction qui n'était pas utilisée de toute façon
|
||||
- Ajoute des tests et une publication automatique des paquets publicodes
|
||||
|
||||
## 1.0.0-beta.15
|
||||
|
||||
**core**
|
||||
|
||||
- Fix bug sur le mécanisme minimum, une valeur non applicable n'est plus considérée comme valant "0" (#1493)
|
||||
|
||||
## 1.0.0-beta.16
|
||||
|
||||
**core**
|
||||
|
||||
- Répare un bug dans le mécanisme résoudre le cycle
|
||||
- Suppression des variables temporelles
|
||||
- Optimisation de la désactivation de branches
|
||||
- Meilleures performances
|
|
@ -1,22 +0,0 @@
|
|||
# Comment contribuer ?
|
||||
|
||||
Merci de prendre le temps de contribuer ! 🎉
|
||||
|
||||
Voici quelques informations pour démarrer :
|
||||
|
||||
## Rapport de bug, nouvelles fonctionnalités
|
||||
|
||||
Nous utilisons GitHub pour suivre tous les bugs et discussions sur les nouvelles fonctionnalités. Pour rapporter un bug ou proposer une évolution vous pouvez [ouvrir une nouvelle discussion](https://github.com/betagouv/publicodes/discussions). N'hésitez pas à utiliser la recherche pour vérifier si le sujet n'est pas déjà traité dans une discussion ouverte.
|
||||
|
||||
## Publier une nouvelle version sur NPM
|
||||
|
||||
Voici la marche à suivre pour publier une nouvelle version :
|
||||
|
||||
1. Renseigner les modifications dans `CHANGELOG.md`
|
||||
2. Remplacer les références à la précédente version par la nouvelle version dans les packages.json
|
||||
3. Ajouter tous les changements dans un commit avec le message suivant :
|
||||
```
|
||||
📦 Publicodes v1.0.0-beta.<n>
|
||||
```
|
||||
> **Important** Le message doit être exactement celui-ci (emoji compris), car le script de déploiement automatique sur le CI se base sur ce dernier.
|
||||
4. Laisser faire le CI, une fois le commit mergé sur master, le paquet sera déployé effectivement
|
|
@ -1,21 +0,0 @@
|
|||
MIT License
|
||||
|
||||
Copyright (c) 2018-2021 beta.gouv.fr
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
|
@ -1,41 +0,0 @@
|
|||
> 🇬🇧 Most of the documentation (including issues and commit messages) is written in French, please raise an [issue](https://github.com/betagouv/publicodes/issues/new) if you are interested and do not speak French. We intend to translate the language and the documentation in the coming weeks.
|
||||
|
||||
## <a href="https://publi.codes">Publicodes</a>
|
||||
|
||||
[![Npm version](https://img.shields.io/npm/v/publicodes)](https://www.npmjs.com/package/publicodes)
|
||||
[![Gitter chat](https://badges.gitter.im/publicodes/publicodes.png)](https://gitter.im/publicodes/community)
|
||||
|
||||
Publicodes est un langage déclaratif pour encoder les algorithmes d'intérêt
|
||||
public. Il permet de réaliser des calculs généraux tout en fournissant une
|
||||
explication permettant de comprendre et de documenter ces calculs.
|
||||
|
||||
Publicodes est adapté pour modéliser des domaines métiers complexes pouvant être
|
||||
décomposés en règles élémentaires simples (comme la [législation socio-fiscale](https://github.com/betagouv/mon-entreprise/tree/master/publicodes),
|
||||
[un bilan carbone](https://github.com/laem/futureco-data/blob/master/co2.yaml),
|
||||
un estimateur de rendement locatif, etc.).
|
||||
|
||||
Il permet de générer facilement des simulateurs web interactifs où l'on peut affiner
|
||||
progressivement le résultat affiché, et d'exposer une documentation du calcul explorable.
|
||||
|
||||
## Installation
|
||||
|
||||
```
|
||||
npm install publicodes
|
||||
```
|
||||
|
||||
## Documentation
|
||||
|
||||
- [Se lancer](https://publi.codes/langage/se-lancer)
|
||||
- [Principes de base](https://publi.codes/langage/principes-de-base)
|
||||
- [Bac à sable](https://publi.codes/studio)
|
||||
|
||||
## Projets phares
|
||||
|
||||
- **[mon-entreprise.fr](https://mon-entreprise.fr/simulateurs)** utilise publicodes
|
||||
pour spécifier l'ensemble des calculs relatifs à la législation socio-fiscale
|
||||
en France. Le site permet entre autre de simuler une fiche de paie complète,
|
||||
de calculer les cotisations sociales pour un indépendant ou encore connaître
|
||||
le montant du chômage partiel.
|
||||
- **[futur.eco](https://futur.eco/)** utilise publicodes pour calculer les bilans
|
||||
carbone d'un grand nombre d'activités, plats, transports ou biens.
|
||||
- **[Nos Gestes Climat](https://ecolab.ademe.fr/apps/climat)** utilise publicodes pour proposer un calculateur d'empreinte climat personnel de référence complètement ouvert
|
|
@ -1 +0,0 @@
|
|||
*.tgz
|
|
@ -1,20 +0,0 @@
|
|||
{
|
||||
"presets": [
|
||||
[
|
||||
"@babel/preset-env",
|
||||
{
|
||||
"targets": {
|
||||
"node": "current"
|
||||
}
|
||||
}
|
||||
],
|
||||
"@babel/preset-typescript"
|
||||
],
|
||||
"plugins": [
|
||||
"@babel/plugin-proposal-class-properties",
|
||||
"@babel/plugin-proposal-optional-chaining",
|
||||
"@babel/plugin-proposal-nullish-coalescing-operator",
|
||||
"@babel/plugin-proposal-object-rest-spread",
|
||||
"@babel/plugin-syntax-dynamic-import"
|
||||
]
|
||||
}
|
|
@ -1,12 +0,0 @@
|
|||
// ESM wrapper arrond publicodes CJS Module
|
||||
// For a deep explanation see:
|
||||
// https://redfin.engineering/node-modules-at-war-why-commonjs-and-es-modules-cant-get-along-9617135eeca1
|
||||
|
||||
import publicodes from '../dist/index.js'
|
||||
export default publicodes.default
|
||||
export const reduceAST = publicodes.reduceAST
|
||||
export const transformAST = publicodes.transformAST
|
||||
export const formatValue = publicodes.formatValue
|
||||
export const utils = publicodes.utils
|
||||
export const UNSAFE_isNotApplicable = publicodes.UNSAFE_isNotApplicable
|
||||
export const evaluateRule = publicodes.evaluateRule
|
|
@ -1,12 +0,0 @@
|
|||
// ESM wrapper arrond index.min.js (browser)
|
||||
|
||||
import '../dist/index.min.js'
|
||||
/* global publicodes */
|
||||
|
||||
export default publicodes.default
|
||||
export const reduceAST = publicodes.reduceAST
|
||||
export const transformAST = publicodes.transformAST
|
||||
export const formatValue = publicodes.formatValue
|
||||
export const utils = publicodes.utils
|
||||
export const UNSAFE_isNotApplicable = publicodes.UNSAFE_isNotApplicable
|
||||
export const evaluateRule = publicodes.evaluateRule
|
|
@ -1,6 +0,0 @@
|
|||
{
|
||||
"type": "module",
|
||||
"engines": {
|
||||
"node": ">=15.0.0"
|
||||
}
|
||||
}
|
|
@ -1,63 +0,0 @@
|
|||
{
|
||||
"name": "publicodes",
|
||||
"version": "1.0.0-beta.16",
|
||||
"description": "A declarative language for encoding public algorithm",
|
||||
"main": "dist/index.js",
|
||||
"types": "dist/types/index.d.ts",
|
||||
"exports": {
|
||||
"require": "./dist/index.js",
|
||||
"import": "./esm/index.js"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/betagouv/mon-entreprise.git",
|
||||
"directory": "publicodes"
|
||||
},
|
||||
"bugs": "https://github.com/betagouv/mon-entreprise/issues?q=is%3Aopen+is%3Aissue+label%3A\"%3Agear%3A+moteur\"",
|
||||
"homepage": "https://publi.codes/",
|
||||
"license": "MIT",
|
||||
"readme": "../README.md",
|
||||
"files": [
|
||||
"dist/index.js",
|
||||
"dist/index.min.js",
|
||||
"dist/types",
|
||||
"esm"
|
||||
],
|
||||
"private": false,
|
||||
"devDependencies": {
|
||||
"@babel/preset-typescript": "^7.14.5",
|
||||
"babel-loader": "^8.2.2",
|
||||
"chai": "^4.2.0",
|
||||
"dedent-js": "1.0.1",
|
||||
"intl": "^1.2.5",
|
||||
"json-loader": "^0.5.7",
|
||||
"mocha": "^9.0.1",
|
||||
"mochapack": "^2.1.2",
|
||||
"nearley-loader": "^2.0.0",
|
||||
"rimraf": "^3.0.2",
|
||||
"typescript": "^4.3.2",
|
||||
"webpack-cli": "^4.7.2",
|
||||
"yaml-loader": "^0.6.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/core": "^7.14.6",
|
||||
"@babel/preset-env": "^7.14.5",
|
||||
"@types/webpack-env": "^1.16.0",
|
||||
"moo": "^0.5.1",
|
||||
"nearley": "^2.19.2",
|
||||
"webpack": "^5.39.1",
|
||||
"yaml": "^1.9.2"
|
||||
},
|
||||
"scripts": {
|
||||
"prepublishOnly": "yarn test && NODE_ENV=production yarn run build",
|
||||
"clean": "rimraf dist node_modules",
|
||||
"prepare": "yarn run rimraf dist && yarn run build",
|
||||
"build": "yarn run webpack --config webpack.config.js && yarn run tsc",
|
||||
"build:watch": "concurrently \"yarn run webpack --watch --config webpack.config.js\" \"yarn run tsc -w\"",
|
||||
"test:file": "yarn mochapack --include test/setupIntl.js --webpack-config ./webpack.test.js ",
|
||||
"test": "yarn test:file \"./{,!(node_modules)/**/}!(webpack).test.js\""
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12.16.1"
|
||||
}
|
||||
}
|
|
@ -1,330 +0,0 @@
|
|||
/* eslint-disable prefer-rest-params */
|
||||
/* eslint-disable @typescript-eslint/no-this-alias */
|
||||
// Adapted from https://github.com/dagrejs/graphlib (MIT license)
|
||||
// and https://github.com/lodash/lodash (MIT license)
|
||||
|
||||
// TODO: type this
|
||||
|
||||
function has(obj, key) {
|
||||
return obj != null && Object.prototype.hasOwnProperty.call(obj, key)
|
||||
}
|
||||
function constant(value) {
|
||||
return function (...args) {
|
||||
return value
|
||||
}
|
||||
}
|
||||
|
||||
const DEFAULT_EDGE_NAME = '\x00'
|
||||
const EDGE_KEY_DELIM = '\x01'
|
||||
|
||||
const incrementOrInitEntry = (map, k) => {
|
||||
if (map[k]) {
|
||||
map[k]++
|
||||
} else {
|
||||
map[k] = 1
|
||||
}
|
||||
}
|
||||
|
||||
const decrementOrRemoveEntry = (map, k) => {
|
||||
if (!--map[k]) {
|
||||
delete map[k]
|
||||
}
|
||||
}
|
||||
|
||||
const edgeArgsToId = (isDirected, v_, w_, name) => {
|
||||
let v = '' + v_
|
||||
let w = '' + w_
|
||||
if (!isDirected && v > w) {
|
||||
const tmp = v
|
||||
v = w
|
||||
w = tmp
|
||||
}
|
||||
return (
|
||||
v +
|
||||
EDGE_KEY_DELIM +
|
||||
w +
|
||||
EDGE_KEY_DELIM +
|
||||
(name === undefined ? DEFAULT_EDGE_NAME : name)
|
||||
)
|
||||
}
|
||||
|
||||
const edgeArgsToObj = (isDirected, v_, w_, name) => {
|
||||
let v = '' + v_
|
||||
let w = '' + w_
|
||||
if (!isDirected && v > w) {
|
||||
const tmp = v
|
||||
v = w
|
||||
w = tmp
|
||||
}
|
||||
const edgeObj: any = { v: v, w: w }
|
||||
if (name) {
|
||||
edgeObj.name = name
|
||||
}
|
||||
return edgeObj
|
||||
}
|
||||
|
||||
const edgeObjToId = (isDirected, edgeObj) => {
|
||||
return edgeArgsToId(isDirected, edgeObj.v, edgeObj.w, edgeObj.name)
|
||||
}
|
||||
export class Graph {
|
||||
private _nodeCount = 0
|
||||
private _edgeCount = 0
|
||||
|
||||
private _isDirected: any
|
||||
|
||||
private _label: undefined
|
||||
private _defaultNodeLabelFn: (...args: any[]) => any
|
||||
private _defaultEdgeLabelFn: (...args: any[]) => any
|
||||
private _nodes: Record<string, any>
|
||||
private _in: Record<string, any>
|
||||
private _preds: Record<string, Record<string, number>>
|
||||
private _out: Record<string, Record<string, string>>
|
||||
private _sucs: Record<string, Record<string, number>>
|
||||
private _edgeObjs: Record<any, any>
|
||||
private _edgeLabels: Record<any, string>
|
||||
|
||||
constructor(opts: Record<string, boolean> = {}) {
|
||||
this._isDirected = has(opts, 'directed') ? opts.directed : true
|
||||
|
||||
// Label for the graph itself
|
||||
this._label = undefined
|
||||
|
||||
// Defaults to be set when creating a new node
|
||||
this._defaultNodeLabelFn = constant(undefined)
|
||||
|
||||
// Defaults to be set when creating a new edge
|
||||
this._defaultEdgeLabelFn = constant(undefined)
|
||||
|
||||
// v -> label
|
||||
this._nodes = {}
|
||||
|
||||
// v -> edgeObj
|
||||
this._in = {}
|
||||
|
||||
// u -> v -> Number
|
||||
this._preds = {}
|
||||
|
||||
// v -> edgeObj
|
||||
this._out = {} as Record<string, Record<string, string>>
|
||||
|
||||
// v -> w -> Number
|
||||
this._sucs = {}
|
||||
|
||||
// e -> edgeObj
|
||||
this._edgeObjs = {}
|
||||
|
||||
// e -> label
|
||||
this._edgeLabels = {}
|
||||
}
|
||||
|
||||
/* === Graph functions ========= */
|
||||
|
||||
isDirected() {
|
||||
return this._isDirected
|
||||
}
|
||||
setGraph(label) {
|
||||
this._label = label
|
||||
return this
|
||||
}
|
||||
graph() {
|
||||
return this._label
|
||||
}
|
||||
|
||||
/* === Node functions ========== */
|
||||
|
||||
nodeCount() {
|
||||
return this._nodeCount
|
||||
}
|
||||
nodes() {
|
||||
return Object.keys(this._nodes)
|
||||
}
|
||||
setNode(v, value: any = undefined) {
|
||||
if (has(this._nodes, v)) {
|
||||
if (arguments.length > 1) {
|
||||
this._nodes[v] = value
|
||||
}
|
||||
return this
|
||||
}
|
||||
|
||||
this._nodes[v] = arguments.length > 1 ? value : this._defaultNodeLabelFn(v)
|
||||
this._in[v] = {}
|
||||
this._preds[v] = {}
|
||||
this._out[v] = {}
|
||||
this._sucs[v] = {}
|
||||
++this._nodeCount
|
||||
return this
|
||||
}
|
||||
setNodes(vs, value) {
|
||||
vs.forEach((v) => {
|
||||
if (value !== undefined) {
|
||||
this.setNode(v, value)
|
||||
} else {
|
||||
this.setNode(v)
|
||||
}
|
||||
})
|
||||
return this
|
||||
}
|
||||
node(v) {
|
||||
return this._nodes[v]
|
||||
}
|
||||
hasNode(v) {
|
||||
return has(this._nodes, v)
|
||||
}
|
||||
successors(v) {
|
||||
const sucsV = this._sucs[v]
|
||||
if (sucsV) {
|
||||
return Object.keys(sucsV)
|
||||
}
|
||||
}
|
||||
|
||||
/* === Edge functions ========== */
|
||||
|
||||
edgeCount() {
|
||||
return this._edgeCount
|
||||
}
|
||||
edges() {
|
||||
return Object.values(this._edgeObjs)
|
||||
}
|
||||
setEdge(
|
||||
v: string,
|
||||
w: string,
|
||||
value: any = undefined,
|
||||
name: string | undefined = undefined
|
||||
) {
|
||||
v = '' + v
|
||||
w = '' + w
|
||||
|
||||
const e = edgeArgsToId(this._isDirected, v, w, name)
|
||||
if (has(this._edgeLabels, e)) {
|
||||
if (value !== undefined) {
|
||||
this._edgeLabels[e] = value
|
||||
}
|
||||
return this
|
||||
}
|
||||
|
||||
// It didn't exist, so we need to create it.
|
||||
// First ensure the nodes exist.
|
||||
this.setNode(v)
|
||||
this.setNode(w)
|
||||
|
||||
this._edgeLabels[e] =
|
||||
value !== undefined ? value : this._defaultEdgeLabelFn(v, w, name)
|
||||
|
||||
const edgeObj = edgeArgsToObj(this._isDirected, v, w, name)
|
||||
// Ensure we add undirected edges in a consistent way.
|
||||
v = edgeObj.v
|
||||
w = edgeObj.w
|
||||
|
||||
Object.freeze(edgeObj)
|
||||
this._edgeObjs[e] = edgeObj
|
||||
incrementOrInitEntry(this._preds[w], v)
|
||||
incrementOrInitEntry(this._sucs[v], w)
|
||||
this._in[w][e] = edgeObj
|
||||
this._out[v][e] = edgeObj
|
||||
this._edgeCount++
|
||||
return this
|
||||
}
|
||||
|
||||
edge(v, w, name) {
|
||||
const e =
|
||||
arguments.length === 1
|
||||
? edgeObjToId(this._isDirected, arguments[0])
|
||||
: edgeArgsToId(this._isDirected, v, w, name)
|
||||
return this._edgeLabels[e]
|
||||
}
|
||||
|
||||
hasEdge(v, w, name) {
|
||||
const e =
|
||||
arguments.length === 1
|
||||
? edgeObjToId(this._isDirected, arguments[0])
|
||||
: edgeArgsToId(this._isDirected, v, w, name)
|
||||
return has(this._edgeLabels, e)
|
||||
}
|
||||
|
||||
removeEdge(v, w, name) {
|
||||
const e =
|
||||
arguments.length === 1
|
||||
? edgeObjToId(this._isDirected, arguments[0])
|
||||
: edgeArgsToId(this._isDirected, v, w, name)
|
||||
const edge = this._edgeObjs[e]
|
||||
if (edge) {
|
||||
v = edge.v
|
||||
w = edge.w
|
||||
delete this._edgeLabels[e]
|
||||
delete this._edgeObjs[e]
|
||||
decrementOrRemoveEntry(this._preds[w], v)
|
||||
decrementOrRemoveEntry(this._sucs[v], w)
|
||||
delete this._in[w][e]
|
||||
delete this._out[v][e]
|
||||
this._edgeCount--
|
||||
}
|
||||
return this
|
||||
}
|
||||
|
||||
outEdges(v: string, w: string | undefined = undefined) {
|
||||
const outV = this._out[v]
|
||||
if (outV) {
|
||||
const edges: any = Object.values(outV)
|
||||
if (w === undefined) {
|
||||
return edges
|
||||
}
|
||||
return edges.filter(function (edge) {
|
||||
return edge.w === w
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Cycles stuff **/
|
||||
|
||||
function tarjan(graph) {
|
||||
let index = 0
|
||||
const stack: any[] = []
|
||||
const visited = {} // node id -> { onStack, lowlink, index }
|
||||
const results: any[] = []
|
||||
|
||||
function dfs(v) {
|
||||
const entry = (visited[v] = {
|
||||
onStack: true,
|
||||
lowlink: index,
|
||||
index: index++,
|
||||
})
|
||||
stack.push(v)
|
||||
|
||||
graph.successors(v).forEach(function (w) {
|
||||
if (!Object.prototype.hasOwnProperty.call(visited, w)) {
|
||||
dfs(w)
|
||||
entry.lowlink = Math.min(entry.lowlink, visited[w].lowlink)
|
||||
} else if (visited[w].onStack) {
|
||||
entry.lowlink = Math.min(entry.lowlink, visited[w].index)
|
||||
}
|
||||
})
|
||||
|
||||
if (entry.lowlink === entry.index) {
|
||||
const cmpt: any[] = []
|
||||
let w
|
||||
do {
|
||||
w = stack.pop()
|
||||
visited[w].onStack = false
|
||||
cmpt.push(w)
|
||||
} while (v !== w)
|
||||
results.push(cmpt)
|
||||
}
|
||||
}
|
||||
|
||||
graph.nodes().forEach(function (v) {
|
||||
if (!Object.prototype.hasOwnProperty.call(visited, v)) {
|
||||
dfs(v)
|
||||
}
|
||||
})
|
||||
|
||||
return results
|
||||
}
|
||||
|
||||
export function findCycles(graph): string[][] {
|
||||
return tarjan(graph).filter(function (cmpt) {
|
||||
return (
|
||||
cmpt.length > 1 || (cmpt.length === 1 && graph.hasEdge(cmpt[0], cmpt[0]))
|
||||
)
|
||||
})
|
||||
}
|
|
@ -1,182 +0,0 @@
|
|||
import { ASTNode } from './types'
|
||||
import parsePublicodes from '../parsePublicodes'
|
||||
import { RuleNode } from '../rule'
|
||||
import { getChildrenNodes, iterAST } from './index'
|
||||
import { findCycles, Graph } from './findCycles'
|
||||
|
||||
type RulesDependencies = [string, string[]][]
|
||||
type GraphCycles = string[][]
|
||||
|
||||
function buildRulesDependencies(
|
||||
parsedRules: Record<string, RuleNode>
|
||||
): RulesDependencies {
|
||||
const uniq = <T>(arr: Array<T>): Array<T> => [...new Set(arr)]
|
||||
return Object.entries(parsedRules).map(([name, node]) => [
|
||||
name,
|
||||
uniq(getDependencies(node)),
|
||||
])
|
||||
}
|
||||
|
||||
function getReferenceName(node: ASTNode): string | undefined {
|
||||
switch (node.nodeKind) {
|
||||
case 'reference':
|
||||
return node.dottedName as string
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Recursively selects the children nodes that have the ability to include a reference
|
||||
* to a rule.
|
||||
*/
|
||||
function getReferencingDescendants(node: ASTNode): ASTNode[] {
|
||||
return iterAST((node) => {
|
||||
switch (node.nodeKind) {
|
||||
case 'replacementRule':
|
||||
case 'inversion':
|
||||
case 'une possibilité':
|
||||
case 'reference':
|
||||
case 'résoudre référence circulaire':
|
||||
// "résoudre référence circulaire" is a chained mechanism. When returning `[]` we prevent
|
||||
// iteration inside of the rule's `valeur`, meaning the rule returns no descendants at all.
|
||||
return []
|
||||
case 'recalcul':
|
||||
return node.explanation.amendedSituation.map(([, astNode]) => astNode)
|
||||
case 'rule':
|
||||
return [node.explanation.valeur]
|
||||
case 'variations':
|
||||
if (node.visualisationKind === 'replacement') {
|
||||
return node.explanation
|
||||
.filter(({ condition }) => condition.isDefault)
|
||||
.map(({ consequence }) => consequence)
|
||||
.filter((consequence) => consequence.nodeKind === 'reference')
|
||||
}
|
||||
}
|
||||
return getChildrenNodes(node)
|
||||
}, node)
|
||||
}
|
||||
function getDependencies(node: ASTNode): string[] {
|
||||
const descendantNodes = Array.from(getReferencingDescendants(node))
|
||||
const descendantsReferences = descendantNodes
|
||||
.map(getReferenceName)
|
||||
.filter((refName): refName is string => refName !== undefined)
|
||||
return descendantsReferences
|
||||
}
|
||||
|
||||
function buildDependenciesGraph(rulesDeps: RulesDependencies) {
|
||||
const g = new Graph()
|
||||
rulesDeps.forEach(([ruleDottedName, dependencies]) => {
|
||||
dependencies.forEach((depDottedName) => {
|
||||
g.setEdge(ruleDottedName, depDottedName)
|
||||
})
|
||||
})
|
||||
return g
|
||||
}
|
||||
|
||||
type RawRules = Parameters<typeof parsePublicodes>[0]
|
||||
|
||||
export function cyclesInDependenciesGraph(rawRules: RawRules): GraphCycles {
|
||||
const parsedRules = parsePublicodes(rawRules)
|
||||
const rulesDependencies = buildRulesDependencies(parsedRules)
|
||||
const dependenciesGraph = buildDependenciesGraph(rulesDependencies)
|
||||
const cycles = findCycles(dependenciesGraph)
|
||||
|
||||
return cycles.map((c) => c.reverse())
|
||||
}
|
||||
|
||||
/**
|
||||
* Make the cycle as small as possible.
|
||||
*/
|
||||
export function squashCycle(
|
||||
rulesDependenciesObject: Record<string, string[]>,
|
||||
cycle: string[]
|
||||
): string[] {
|
||||
function* loopFrom(i: number) {
|
||||
let j = i
|
||||
while (true) {
|
||||
yield cycle[j++ % cycle.length]
|
||||
}
|
||||
}
|
||||
const smallCycleStartingAt: string[][] = []
|
||||
for (let i = 0; i < cycle.length; i++) {
|
||||
const smallCycle: string[] = []
|
||||
let previousVertex: string | undefined = undefined
|
||||
for (const vertex of loopFrom(i)) {
|
||||
if (previousVertex === undefined) {
|
||||
smallCycle.push(vertex)
|
||||
previousVertex = vertex
|
||||
} else if (rulesDependenciesObject[previousVertex].includes(vertex)) {
|
||||
if (smallCycle.includes(vertex)) {
|
||||
smallCycle.splice(0, smallCycle.lastIndexOf(vertex))
|
||||
break
|
||||
}
|
||||
smallCycle.push(vertex)
|
||||
previousVertex = vertex
|
||||
}
|
||||
}
|
||||
smallCycleStartingAt.push(smallCycle)
|
||||
}
|
||||
|
||||
const smallest = smallCycleStartingAt.reduce((minCycle, someCycle) =>
|
||||
someCycle.length > minCycle.length ? minCycle : someCycle
|
||||
)
|
||||
return smallest
|
||||
}
|
||||
|
||||
/**
|
||||
* This function is useful so as to print the dependencies at each node of the
|
||||
* cycle.
|
||||
* ⚠️ Indeed, the findCycles function returns the cycle found using the
|
||||
* Tarjan method, which is **not necessarily the smallest cycle**. However, the
|
||||
* smallest cycle is more readable.
|
||||
*/
|
||||
export function cyclicDependencies(
|
||||
rawRules: RawRules
|
||||
): [GraphCycles, string[]] {
|
||||
const parsedRules = parsePublicodes(rawRules)
|
||||
const rulesDependencies = buildRulesDependencies(parsedRules)
|
||||
const dependenciesGraph = buildDependenciesGraph(rulesDependencies)
|
||||
const cycles = findCycles(dependenciesGraph)
|
||||
|
||||
const reversedCycles = cycles.map((c) => c.reverse())
|
||||
const rulesDependenciesObject = Object.fromEntries(
|
||||
rulesDependencies
|
||||
) as Record<string, string[]>
|
||||
const smallCycles = reversedCycles.map((cycle) =>
|
||||
squashCycle(rulesDependenciesObject, cycle)
|
||||
)
|
||||
|
||||
const printableStronglyConnectedComponents = reversedCycles.map((c, i) =>
|
||||
printInDotFormat(dependenciesGraph, c, smallCycles[i])
|
||||
)
|
||||
|
||||
return [smallCycles, printableStronglyConnectedComponents]
|
||||
}
|
||||
|
||||
/**
|
||||
* Is edge in the cycle, in the same order?
|
||||
*/
|
||||
const edgeIsInCycle = (cycle: string[], v: string, w: string): boolean => {
|
||||
for (let i = 0; i < cycle.length + 1; i++) {
|
||||
if (v === cycle[i] && w === cycle[(i + 1) % cycle.length]) return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
export function printInDotFormat(
|
||||
dependenciesGraph: Graph,
|
||||
cycle: string[],
|
||||
subCycleToHighlight: string[]
|
||||
) {
|
||||
const edgesSet = new Set()
|
||||
cycle.forEach((vertex) => {
|
||||
dependenciesGraph
|
||||
.outEdges(vertex)
|
||||
.filter(({ w }) => cycle.includes(w))
|
||||
.forEach(({ v, w }) => {
|
||||
edgesSet.add(
|
||||
`"${v}" -> "${w}"` +
|
||||
(edgeIsInCycle(subCycleToHighlight, v, w) ? ' [color=red]' : '')
|
||||
)
|
||||
})
|
||||
})
|
||||
return `digraph Cycle {\n\t${[...edgesSet].join(';\n\t')};\n}`
|
||||
}
|
|
@ -1,381 +0,0 @@
|
|||
import { InternalError } from '../error'
|
||||
import { TrancheNodes } from '../mecanisms/trancheUtils'
|
||||
import { ReplacementRule } from '../replacement'
|
||||
import { RuleNode } from '../rule'
|
||||
import {
|
||||
ASTNode,
|
||||
ASTVisitor,
|
||||
ASTTransformer,
|
||||
NodeKind,
|
||||
TraverseFunction,
|
||||
} from './types'
|
||||
|
||||
/**
|
||||
This function creates a transormation of the AST from on a simpler
|
||||
callback function `fn`
|
||||
|
||||
`fn` will be called with the nodes of the ASTTree during the exploration
|
||||
|
||||
The outcome of the callback function has an influence on the exploration of the AST :
|
||||
- `false`, the node is not updated and the exploration does not continue further down this branch
|
||||
- `undefined`, the node is not updated but the exploration continues and its children will be transformed
|
||||
- `ASTNode`, the node is transformed to the new value and the exploration does not continue further down the branch
|
||||
|
||||
`updateFn` : It is possible to specifically use the updated version of a child
|
||||
by using the function passed as second argument. The returned value will be the
|
||||
transformed version of the node.
|
||||
*/
|
||||
export function makeASTTransformer(
|
||||
fn: (node: ASTNode, transform: ASTTransformer) => ASTNode | undefined | false
|
||||
): ASTTransformer {
|
||||
function transform(node: ASTNode): ASTNode {
|
||||
const updatedNode = fn(node, transform)
|
||||
if (updatedNode === false) {
|
||||
return node
|
||||
}
|
||||
if (updatedNode === undefined) {
|
||||
return traverseASTNode(transform, node)
|
||||
}
|
||||
return updatedNode
|
||||
}
|
||||
return transform
|
||||
}
|
||||
export function makeASTVisitor(
|
||||
fn: (node: ASTNode, visit: ASTVisitor) => 'continue' | 'stop'
|
||||
): ASTVisitor {
|
||||
function visit(node: ASTNode) {
|
||||
switch (fn(node, visit)) {
|
||||
case 'continue':
|
||||
traverseASTNode(transformizedVisit, node)
|
||||
return
|
||||
case 'stop':
|
||||
return
|
||||
}
|
||||
}
|
||||
const transformizedVisit: ASTTransformer = (node) => {
|
||||
visit(node)
|
||||
return node
|
||||
}
|
||||
return visit
|
||||
}
|
||||
|
||||
// Can be made more flexible with other args like a filter function (ASTNode -> Bool).
|
||||
export function iterAST(
|
||||
childrenSelector: (node: ASTNode) => Iterable<ASTNode>,
|
||||
node: ASTNode
|
||||
): ASTNode[] {
|
||||
function* iterate(node: ASTNode): IterableIterator<ASTNode> {
|
||||
yield node
|
||||
const selectedSubNodes = childrenSelector(node)
|
||||
for (const subNode of selectedSubNodes) yield* iterate(subNode)
|
||||
}
|
||||
return [...iterate(node)]
|
||||
}
|
||||
|
||||
/**
|
||||
* This function allows to construct a specific value while exploring the AST with
|
||||
* a simple reducing function as argument.
|
||||
*
|
||||
* `fn` will be called with the currently reduced value `acc` and the current node of the AST
|
||||
*
|
||||
* If the callback function returns:
|
||||
* - `undefined`, the exploration continues further down and all the children are reduced
|
||||
* successively to a single value
|
||||
* - `T`, the reduced value is returned
|
||||
*
|
||||
* `reduceFn` : It is possible to specifically use the reduced value of a child
|
||||
* by using the function passed as second argument. The returned value will be the reduced version
|
||||
* of the node
|
||||
*/
|
||||
export function reduceAST<T>(
|
||||
fn: (acc: T, n: ASTNode, reduceFn: (n: ASTNode) => T) => T | undefined,
|
||||
start: T,
|
||||
node: ASTNode
|
||||
): T {
|
||||
function traverseFn(acc: T, node: ASTNode): T {
|
||||
const result = fn(acc, node, traverseFn.bind(null, start))
|
||||
if (result === undefined) {
|
||||
return getChildrenNodes(node).reduce(traverseFn, acc)
|
||||
}
|
||||
return result
|
||||
}
|
||||
return traverseFn(start, node)
|
||||
}
|
||||
|
||||
export function getChildrenNodes(node: ASTNode): ASTNode[] {
|
||||
const nodes: ASTNode[] = []
|
||||
traverseASTNode((node) => {
|
||||
nodes.push(node)
|
||||
return node
|
||||
}, node)
|
||||
return nodes
|
||||
}
|
||||
|
||||
export function traverseParsedRules(
|
||||
fn: ASTTransformer,
|
||||
parsedRules: Record<string, RuleNode>
|
||||
): Record<string, RuleNode> {
|
||||
return Object.fromEntries(
|
||||
Object.entries(parsedRules).map(([name, rule]) => [name, fn(rule)])
|
||||
) as Record<string, RuleNode>
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply a transform function on children. Not recursive.
|
||||
*/
|
||||
export const traverseASTNode: TraverseFunction<NodeKind> = (fn, node) => {
|
||||
switch (node.nodeKind) {
|
||||
case 'rule':
|
||||
return traverseRuleNode(fn, node)
|
||||
case 'reference':
|
||||
case 'constant':
|
||||
return traverseLeafNode(fn, node)
|
||||
case 'applicable si':
|
||||
case 'non applicable si':
|
||||
return traverseApplicableNode(fn, node)
|
||||
case 'arrondi':
|
||||
return traverseArrondiNode(fn, node)
|
||||
case 'barème':
|
||||
case 'taux progressif':
|
||||
case 'grille':
|
||||
return traverseNodeWithTranches(fn, node)
|
||||
case 'somme':
|
||||
case 'une de ces conditions':
|
||||
case 'une possibilité':
|
||||
case 'toutes ces conditions':
|
||||
case 'minimum':
|
||||
case 'maximum':
|
||||
return traverseArrayNode(fn, node)
|
||||
case 'durée':
|
||||
return traverseDuréeNode(fn, node)
|
||||
case 'résoudre référence circulaire':
|
||||
return traverseRésoudreRéférenceCirculaireNode(fn, node)
|
||||
case 'inversion':
|
||||
return traverseInversionNode(fn, node)
|
||||
case 'operation':
|
||||
return traverseOperationNode(fn, node)
|
||||
case 'par défaut':
|
||||
return traverseParDéfautNode(fn, node)
|
||||
case 'plancher':
|
||||
return traversePlancherNode(fn, node)
|
||||
case 'plafond':
|
||||
return traversePlafondNode(fn, node)
|
||||
case 'produit':
|
||||
return traverseProductNode(fn, node)
|
||||
case 'recalcul':
|
||||
return traverseRecalculNode(fn, node)
|
||||
case 'abattement':
|
||||
return traverseAbattementNode(fn, node)
|
||||
case 'nom dans la situation':
|
||||
return traverseSituationNode(fn, node)
|
||||
case 'synchronisation':
|
||||
return traverseSynchronisationNode(fn, node)
|
||||
case 'unité':
|
||||
return traverseUnitéNode(fn, node)
|
||||
case 'variations':
|
||||
return traverseVariationNode(fn, node)
|
||||
case 'replacementRule':
|
||||
return traverseReplacementNode(fn, node)
|
||||
default:
|
||||
throw new InternalError(node)
|
||||
}
|
||||
}
|
||||
|
||||
const traverseRuleNode: TraverseFunction<'rule'> = (fn, node) => ({
|
||||
...node,
|
||||
replacements: node.replacements.map(fn) as Array<ReplacementRule>,
|
||||
suggestions: Object.fromEntries(
|
||||
Object.entries(node.suggestions).map(([key, value]) => [key, fn(value)])
|
||||
),
|
||||
explanation: {
|
||||
parent: node.explanation.parent && fn(node.explanation.parent),
|
||||
valeur: fn(node.explanation.valeur),
|
||||
},
|
||||
})
|
||||
|
||||
const traverseReplacementNode: TraverseFunction<'replacementRule'> = (
|
||||
fn,
|
||||
node
|
||||
) =>
|
||||
({
|
||||
...node,
|
||||
definitionRule: fn(node.definitionRule),
|
||||
replacedReference: fn(node.replacedReference),
|
||||
replacementNode: fn(node.replacementNode),
|
||||
whiteListedNames: node.whiteListedNames.map(fn),
|
||||
blackListedNames: node.blackListedNames.map(fn),
|
||||
} as ReplacementRule)
|
||||
|
||||
const traverseLeafNode: TraverseFunction<'reference' | 'constant'> = (
|
||||
_,
|
||||
node
|
||||
) => node
|
||||
const traverseApplicableNode: TraverseFunction<
|
||||
'applicable si' | 'non applicable si'
|
||||
> = (fn, node) => ({
|
||||
...node,
|
||||
explanation: {
|
||||
condition: fn(node.explanation.condition),
|
||||
valeur: fn(node.explanation.valeur),
|
||||
},
|
||||
})
|
||||
|
||||
function traverseTranche(fn: (n: ASTNode) => ASTNode, tranches: TrancheNodes) {
|
||||
return tranches.map((tranche) => ({
|
||||
...tranche,
|
||||
...(tranche.plafond && { plafond: fn(tranche.plafond) }),
|
||||
...('montant' in tranche && { montant: fn(tranche.montant) }),
|
||||
...('taux' in tranche && { taux: fn(tranche.taux) }),
|
||||
}))
|
||||
}
|
||||
const traverseNodeWithTranches: TraverseFunction<
|
||||
'barème' | 'taux progressif' | 'grille'
|
||||
> = (fn, node) => ({
|
||||
...node,
|
||||
explanation: {
|
||||
assiette: fn(node.explanation.assiette),
|
||||
multiplicateur: fn(node.explanation.multiplicateur),
|
||||
tranches: traverseTranche(fn, node.explanation.tranches),
|
||||
},
|
||||
})
|
||||
|
||||
const traverseArrayNode: TraverseFunction<
|
||||
| 'maximum'
|
||||
| 'minimum'
|
||||
| 'somme'
|
||||
| 'toutes ces conditions'
|
||||
| 'une de ces conditions'
|
||||
| 'une possibilité'
|
||||
> = (fn, node) => ({
|
||||
...node,
|
||||
explanation: node.explanation.map(fn),
|
||||
})
|
||||
|
||||
const traverseOperationNode: TraverseFunction<'operation'> = (fn, node) => ({
|
||||
...node,
|
||||
explanation: [fn(node.explanation[0]), fn(node.explanation[1])],
|
||||
})
|
||||
const traverseDuréeNode: TraverseFunction<'durée'> = (fn, node) => ({
|
||||
...node,
|
||||
explanation: {
|
||||
depuis: fn(node.explanation.depuis),
|
||||
"jusqu'à": fn(node.explanation["jusqu'à"]),
|
||||
},
|
||||
})
|
||||
|
||||
const traverseInversionNode: TraverseFunction<'inversion'> = (fn, node) => ({
|
||||
...node,
|
||||
explanation: {
|
||||
...node.explanation,
|
||||
inversionCandidates: node.explanation.inversionCandidates.map(fn) as any, // TODO
|
||||
},
|
||||
})
|
||||
|
||||
const traverseParDéfautNode: TraverseFunction<'par défaut'> = (fn, node) => ({
|
||||
...node,
|
||||
explanation: {
|
||||
valeur: fn(node.explanation.valeur),
|
||||
parDéfaut: fn(node.explanation.parDéfaut),
|
||||
},
|
||||
})
|
||||
|
||||
const traverseArrondiNode: TraverseFunction<'arrondi'> = (fn, node) => ({
|
||||
...node,
|
||||
explanation: {
|
||||
valeur: fn(node.explanation.valeur),
|
||||
arrondi: fn(node.explanation.arrondi),
|
||||
},
|
||||
})
|
||||
|
||||
const traversePlancherNode: TraverseFunction<'plancher'> = (fn, node) => ({
|
||||
...node,
|
||||
explanation: {
|
||||
valeur: fn(node.explanation.valeur),
|
||||
plancher: fn(node.explanation.plancher),
|
||||
},
|
||||
})
|
||||
|
||||
const traverseRésoudreRéférenceCirculaireNode: TraverseFunction<'résoudre référence circulaire'> =
|
||||
(fn, node) => ({
|
||||
...node,
|
||||
explanation: {
|
||||
...node.explanation,
|
||||
valeur: fn(node.explanation.valeur),
|
||||
},
|
||||
})
|
||||
|
||||
const traversePlafondNode: TraverseFunction<'plafond'> = (fn, node) => ({
|
||||
...node,
|
||||
explanation: {
|
||||
valeur: fn(node.explanation.valeur),
|
||||
plafond: fn(node.explanation.plafond),
|
||||
},
|
||||
})
|
||||
|
||||
const traverseProductNode: TraverseFunction<'produit'> = (fn, node) => ({
|
||||
...node,
|
||||
explanation: {
|
||||
assiette: fn(node.explanation.assiette),
|
||||
taux: fn(node.explanation.taux),
|
||||
facteur: fn(node.explanation.facteur),
|
||||
plafond: fn(node.explanation.plafond),
|
||||
},
|
||||
})
|
||||
|
||||
const traverseRecalculNode: TraverseFunction<'recalcul'> = (fn, node) => ({
|
||||
...node,
|
||||
explanation: {
|
||||
...node.explanation,
|
||||
amendedSituation: node.explanation.amendedSituation.map(([name, value]) => [
|
||||
fn(name),
|
||||
fn(value),
|
||||
]) as any, //TODO
|
||||
recalcul: fn(node.explanation.recalcul),
|
||||
},
|
||||
})
|
||||
|
||||
const traverseAbattementNode: TraverseFunction<'abattement'> = (fn, node) => ({
|
||||
...node,
|
||||
explanation: {
|
||||
assiette: fn(node.explanation.assiette),
|
||||
abattement: fn(node.explanation.abattement),
|
||||
},
|
||||
})
|
||||
|
||||
const traverseSituationNode: TraverseFunction<'nom dans la situation'> = (
|
||||
fn,
|
||||
node
|
||||
) => ({
|
||||
...node,
|
||||
explanation: {
|
||||
...node.explanation,
|
||||
...(node.explanation.situationValeur && {
|
||||
situationValeur: fn(node.explanation.situationValeur),
|
||||
}),
|
||||
valeur: fn(node.explanation.valeur),
|
||||
},
|
||||
})
|
||||
|
||||
const traverseSynchronisationNode: TraverseFunction<'synchronisation'> = (
|
||||
fn,
|
||||
node
|
||||
) => ({
|
||||
...node,
|
||||
explanation: {
|
||||
...node.explanation,
|
||||
data: fn(node.explanation.data),
|
||||
},
|
||||
})
|
||||
|
||||
const traverseUnitéNode: TraverseFunction<'unité'> = (fn, node) => ({
|
||||
...node,
|
||||
explanation: fn(node.explanation),
|
||||
})
|
||||
|
||||
const traverseVariationNode: TraverseFunction<'variations'> = (fn, node) => ({
|
||||
...node,
|
||||
explanation: node.explanation.map(({ condition, consequence }) => ({
|
||||
condition: fn(condition),
|
||||
consequence: fn(consequence),
|
||||
})),
|
||||
})
|
|
@ -1,129 +0,0 @@
|
|||
import { AbattementNode } from '../mecanisms/abattement'
|
||||
import { ApplicableSiNode } from '../mecanisms/applicable'
|
||||
import { ArrondiNode } from '../mecanisms/arrondi'
|
||||
import { BarèmeNode } from '../mecanisms/barème'
|
||||
import { TouteCesConditionsNode } from '../mecanisms/condition-allof'
|
||||
import { UneDeCesConditionsNode } from '../mecanisms/condition-oneof'
|
||||
import { DuréeNode } from '../mecanisms/durée'
|
||||
import { GrilleNode } from '../mecanisms/grille'
|
||||
import { InversionNode } from '../mecanisms/inversion'
|
||||
import { MaxNode } from '../mecanisms/max'
|
||||
import { MinNode } from '../mecanisms/min'
|
||||
import { NonApplicableSiNode } from '../mecanisms/nonApplicable'
|
||||
import { PossibilityNode } from '../mecanisms/one-possibility'
|
||||
import { OperationNode } from '../mecanisms/operation'
|
||||
import { ParDéfautNode } from '../mecanisms/parDéfaut'
|
||||
import { PlafondNode } from '../mecanisms/plafond'
|
||||
import { PlancherNode } from '../mecanisms/plancher'
|
||||
import { ProductNode } from '../mecanisms/product'
|
||||
import { RecalculNode } from '../mecanisms/recalcul'
|
||||
import { RésoudreRéférenceCirculaireNode } from '../mecanisms/résoudre-référence-circulaire'
|
||||
import { SituationNode } from '../mecanisms/situation'
|
||||
import { SommeNode } from '../mecanisms/sum'
|
||||
import { SynchronisationNode } from '../mecanisms/synchronisation'
|
||||
import { TauxProgressifNode } from '../mecanisms/tauxProgressif'
|
||||
import { UnitéNode } from '../mecanisms/unité'
|
||||
import { VariationNode } from '../mecanisms/variations'
|
||||
import { ReferenceNode } from '../reference'
|
||||
import { ReplacementRule } from '../replacement'
|
||||
import { RuleNode } from '../rule'
|
||||
|
||||
export type ConstantNode = {
|
||||
type: 'boolean' | 'objet' | 'number' | 'string'
|
||||
nodeValue: Evaluation
|
||||
nodeKind: 'constant'
|
||||
isDefault?: boolean
|
||||
}
|
||||
export type ASTNode = (
|
||||
| RuleNode
|
||||
| ReferenceNode
|
||||
| AbattementNode
|
||||
| ApplicableSiNode
|
||||
| ArrondiNode
|
||||
| BarèmeNode
|
||||
| TouteCesConditionsNode
|
||||
| UneDeCesConditionsNode
|
||||
| DuréeNode
|
||||
| GrilleNode
|
||||
| MaxNode
|
||||
| InversionNode
|
||||
| MinNode
|
||||
| NonApplicableSiNode
|
||||
| OperationNode
|
||||
| ParDéfautNode
|
||||
| PossibilityNode
|
||||
| PlafondNode
|
||||
| PlancherNode
|
||||
| ProductNode
|
||||
| RecalculNode
|
||||
| RésoudreRéférenceCirculaireNode
|
||||
| SituationNode
|
||||
| SommeNode
|
||||
| SynchronisationNode
|
||||
| TauxProgressifNode
|
||||
| UnitéNode
|
||||
| VariationNode
|
||||
| ConstantNode
|
||||
| ReplacementRule
|
||||
) & {
|
||||
isDefault?: boolean
|
||||
visualisationKind?: string
|
||||
rawNode?: string | Record<string, unknown>
|
||||
} & (
|
||||
| EvaluationDecoration<Types>
|
||||
// We remove the ESLINT warning as it does not concern intersection type and is actually useful here
|
||||
// https://github.com/typescript-eslint/typescript-eslint/issues/2063#issuecomment-675156492
|
||||
// eslint-disable-next-line @typescript-eslint/ban-types
|
||||
| {}
|
||||
)
|
||||
// TODO : separate type for evaluated AST Tree
|
||||
|
||||
export type MecanismNode = Exclude<
|
||||
ASTNode,
|
||||
RuleNode | ConstantNode | ReferenceNode
|
||||
>
|
||||
|
||||
export type ASTTransformer = (n: ASTNode) => ASTNode
|
||||
export type ASTVisitor = (n: ASTNode) => void
|
||||
|
||||
export type NodeKind = ASTNode['nodeKind']
|
||||
export type TraverseFunction<Kind extends NodeKind> = (
|
||||
fn: ASTTransformer,
|
||||
node: ASTNode & { nodeKind: Kind }
|
||||
) => ASTNode & { nodeKind: Kind }
|
||||
|
||||
type BaseUnit = string
|
||||
|
||||
// TODO: I believe it would be more effecient (for unit conversion and for
|
||||
// inference), and more general to represent units using a map of base unit to
|
||||
// their power number :
|
||||
//
|
||||
// type Unit = Map<BaseUnit, number>
|
||||
// N.m²/kg² <-> {N: 1, m: 2, kg: -2} (gravity constant)
|
||||
export type Unit = {
|
||||
numerators: Array<BaseUnit>
|
||||
denominators: Array<BaseUnit>
|
||||
}
|
||||
|
||||
// Idée : une évaluation est un n-uple : (value, unit, missingVariable, isApplicable)
|
||||
type EvaluationDecoration<T extends Types> = {
|
||||
nodeValue: Evaluation<T>
|
||||
missingVariables: Record<string, number>
|
||||
unit?: Unit
|
||||
}
|
||||
export type Types = number | boolean | string | Record<string, unknown>
|
||||
// TODO: type NotYetDefined & NotApplicable properly (see #14) then refactor any code depending on these:
|
||||
export type NotYetDefined = null
|
||||
export function isNotYetDefined(value): value is NotYetDefined {
|
||||
return value === null
|
||||
}
|
||||
export type NotApplicable = false
|
||||
export function isNotApplicable(value): value is NotApplicable {
|
||||
return typeof value === 'boolean' && value === false
|
||||
}
|
||||
export type Evaluation<T extends Types = Types> =
|
||||
| T
|
||||
| NotApplicable
|
||||
| NotYetDefined
|
||||
export type EvaluatedNode<T extends Types = Types> = ASTNode &
|
||||
EvaluationDecoration<T>
|
|
@ -1,71 +0,0 @@
|
|||
export function normalizeDateString(dateString: string): string {
|
||||
let [day, month, year] = dateString.split('/')
|
||||
if (!year) {
|
||||
;[day, month, year] = ['01', day, month]
|
||||
}
|
||||
return normalizeDate(+year, +month, +day)
|
||||
}
|
||||
|
||||
const pad = (n: number): string => (+n < 10 ? `0${n}` : '' + n)
|
||||
export function normalizeDate(
|
||||
year: number,
|
||||
month: number,
|
||||
day: number
|
||||
): string {
|
||||
const date = new Date(+year, +month - 1, +day)
|
||||
if (!+date || date.getDate() !== +day) {
|
||||
throw new SyntaxError(`La date ${day}/${month}/${year} n'est pas valide`)
|
||||
}
|
||||
return `${pad(day)}/${pad(month)}/${pad(year)}`
|
||||
}
|
||||
|
||||
export function convertToDate(value: string): Date {
|
||||
const [day, month, year] = normalizeDateString(value).split('/')
|
||||
const result = new Date(+year, +month - 1, +day)
|
||||
// Reset date to utc midnight for exact calculation of day difference (no
|
||||
// daylight saving effect)
|
||||
result.setMinutes(result.getMinutes() - result.getTimezoneOffset())
|
||||
return result
|
||||
}
|
||||
|
||||
export function convertToString(date: Date): string {
|
||||
return normalizeDate(date.getFullYear(), date.getMonth() + 1, date.getDate())
|
||||
}
|
||||
|
||||
export function getRelativeDate(date: string, dayDifferential: number): string {
|
||||
const relativeDate = new Date(convertToDate(date))
|
||||
relativeDate.setDate(relativeDate.getDate() + dayDifferential)
|
||||
return convertToString(relativeDate)
|
||||
}
|
||||
|
||||
export function getYear(date: string): number {
|
||||
return +date.slice(-4)
|
||||
}
|
||||
|
||||
export function getDifferenceInDays(from: string, to: string): number {
|
||||
const millisecondsPerDay = 1000 * 60 * 60 * 24
|
||||
return (
|
||||
1 +
|
||||
(convertToDate(from).getTime() - convertToDate(to).getTime()) /
|
||||
millisecondsPerDay
|
||||
)
|
||||
}
|
||||
|
||||
export function getDifferenceInMonths(from: string, to: string): number {
|
||||
// We want to compute the difference in actual month between the two dates
|
||||
// For date that start during a month, a pro-rata will be done depending on
|
||||
// the duration of the month in days
|
||||
const [dayFrom, monthFrom, yearFrom] = from.split('/').map((x) => +x)
|
||||
const [dayTo, monthTo, yearTo] = to.split('/').map((x) => +x)
|
||||
const numberOfFullMonth = monthTo - monthFrom + 12 * (yearTo - yearFrom)
|
||||
const numDayMonthFrom = new Date(yearFrom, monthFrom, 0).getDate()
|
||||
const numDayMonthTo = new Date(yearTo, monthTo, 0).getDate()
|
||||
const prorataMonthFrom = (dayFrom - 1) / numDayMonthFrom
|
||||
const prorataMonthTo = dayTo / numDayMonthTo
|
||||
return numberOfFullMonth - prorataMonthFrom + prorataMonthTo
|
||||
}
|
||||
|
||||
export function getDifferenceInYears(from: string, to: string): number {
|
||||
// Todo : take leap year into account
|
||||
return getDifferenceInDays(from, to) / 365.25
|
||||
}
|
|
@ -1,60 +0,0 @@
|
|||
import { Logger } from '.'
|
||||
|
||||
export class EngineError extends Error {}
|
||||
export function syntaxError(
|
||||
dottedName: string,
|
||||
message: string,
|
||||
originalError?: Error
|
||||
) {
|
||||
throw new EngineError(
|
||||
`\n[ Erreur syntaxique ]
|
||||
➡️ Dans la règle "${dottedName}"
|
||||
✖️ ${message}
|
||||
${originalError ? originalError.message : ''}
|
||||
`
|
||||
)
|
||||
}
|
||||
export function warning(
|
||||
logger: Logger,
|
||||
rule: string,
|
||||
message: string,
|
||||
originalError?: Error
|
||||
) {
|
||||
logger.warn(
|
||||
`\n[ Avertissement ]
|
||||
➡️ Dans la règle "${rule}"
|
||||
⚠️ ${message}
|
||||
${originalError ? `ℹ️ ${originalError.message}` : ''}
|
||||
`
|
||||
)
|
||||
}
|
||||
|
||||
export function evaluationError(
|
||||
logger: Logger,
|
||||
rule: string,
|
||||
message: string,
|
||||
originalError?: Error
|
||||
) {
|
||||
logger.error(
|
||||
`\n[ Erreur d'évaluation ]
|
||||
➡️ Dans la règle "${rule}"
|
||||
✖️ ${message}
|
||||
${originalError ? originalError.message : ''}
|
||||
`
|
||||
)
|
||||
}
|
||||
|
||||
export class InternalError extends EngineError {
|
||||
constructor(payload) {
|
||||
super(
|
||||
`
|
||||
Erreur interne du moteur.
|
||||
|
||||
Cette erreur est le signe d'un bug dans publicodes. Pour nous aider à le résoudre, vous pouvez copier ce texte dans un nouveau ticket : https://github.com/betagouv/mon-entreprise/issues/new.
|
||||
|
||||
payload:
|
||||
\t${JSON.stringify(payload, null, 2)}
|
||||
`
|
||||
)
|
||||
}
|
||||
}
|
|
@ -1,105 +0,0 @@
|
|||
import Engine, { EvaluationFunction } from '.'
|
||||
import {
|
||||
ASTNode,
|
||||
ConstantNode,
|
||||
EvaluatedNode,
|
||||
Evaluation,
|
||||
NodeKind,
|
||||
} from './AST/types'
|
||||
import { warning } from './error'
|
||||
import { convertNodeToUnit } from './nodeUnits'
|
||||
import parse from './parse'
|
||||
|
||||
export const collectNodeMissing = (
|
||||
node: EvaluatedNode | ASTNode
|
||||
): Record<string, number> =>
|
||||
'missingVariables' in node ? node.missingVariables : {}
|
||||
|
||||
export const bonus = (missings: Record<string, number> = {}) =>
|
||||
Object.fromEntries(
|
||||
Object.entries(missings).map(([key, value]) => [key, value + 0.0001])
|
||||
)
|
||||
export const mergeMissing = (
|
||||
left: Record<string, number> | undefined = {},
|
||||
right: Record<string, number> | undefined = {}
|
||||
): Record<string, number> =>
|
||||
Object.fromEntries(
|
||||
[...Object.keys(left), ...Object.keys(right)].map((key) => [
|
||||
key,
|
||||
(left[key] ?? 0) + (right[key] ?? 0),
|
||||
])
|
||||
)
|
||||
|
||||
export const mergeAllMissing = (missings: Array<EvaluatedNode | ASTNode>) =>
|
||||
missings.map(collectNodeMissing).reduce(mergeMissing, {})
|
||||
|
||||
function convertNodesToSameUnit(this: Engine, nodes, mecanismName) {
|
||||
const firstNodeWithUnit = nodes.find((node) => !!node.unit)
|
||||
if (!firstNodeWithUnit) {
|
||||
return nodes
|
||||
}
|
||||
return nodes.map((node) => {
|
||||
try {
|
||||
return convertNodeToUnit(firstNodeWithUnit.unit, node)
|
||||
} catch (e) {
|
||||
warning(
|
||||
this.options.logger,
|
||||
this.cache._meta.evaluationRuleStack[0],
|
||||
`Les unités des éléments suivants sont incompatibles entre elles : \n\t\t${
|
||||
node?.name || node?.rawNode
|
||||
}\n\t\t${firstNodeWithUnit?.name || firstNodeWithUnit?.rawNode}'`,
|
||||
e
|
||||
)
|
||||
return node
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export const evaluateArray: <NodeName extends NodeKind>(
|
||||
reducer,
|
||||
start
|
||||
) => EvaluationFunction<NodeName> = (reducer, start) =>
|
||||
function (node: any) {
|
||||
const evaluate = this.evaluate.bind(this)
|
||||
const evaluatedNodes = convertNodesToSameUnit.call(
|
||||
this,
|
||||
node.explanation.map(evaluate),
|
||||
node.name
|
||||
)
|
||||
const values = evaluatedNodes.map(({ nodeValue }) => nodeValue)
|
||||
const nodeValue = values.some((value) => value === null)
|
||||
? null
|
||||
: values.reduce(reducer, start)
|
||||
|
||||
return {
|
||||
...node,
|
||||
missingVariables: mergeAllMissing(evaluatedNodes),
|
||||
explanation: evaluatedNodes,
|
||||
...(evaluatedNodes[0] && { unit: evaluatedNodes[0].unit }),
|
||||
nodeValue,
|
||||
}
|
||||
}
|
||||
|
||||
export const defaultNode = (nodeValue: Evaluation) =>
|
||||
({
|
||||
nodeValue,
|
||||
type: typeof nodeValue,
|
||||
isDefault: true,
|
||||
nodeKind: 'constant',
|
||||
} as ConstantNode)
|
||||
|
||||
export const parseObject = (objectShape, value, context) => {
|
||||
return Object.fromEntries(
|
||||
Object.entries(objectShape).map(([key, defaultValue]) => {
|
||||
if (value[key] == null && !defaultValue) {
|
||||
throw new Error(
|
||||
`Il manque une clé '${key}' dans ${JSON.stringify(value)} `
|
||||
)
|
||||
}
|
||||
|
||||
const parsedValue =
|
||||
value[key] != null ? parse(value[key], context) : defaultValue
|
||||
return [key, parsedValue]
|
||||
})
|
||||
)
|
||||
}
|
|
@ -1,18 +0,0 @@
|
|||
import { EvaluationFunction } from '.'
|
||||
import { ASTNode } from './AST/types'
|
||||
|
||||
export let evaluationFunctions = {
|
||||
constant: (node) => node,
|
||||
} as any
|
||||
|
||||
export function registerEvaluationFunction<
|
||||
NodeName extends ASTNode['nodeKind']
|
||||
>(nodeKind: NodeName, evaluationFunction: EvaluationFunction<NodeName>) {
|
||||
evaluationFunctions ??= {}
|
||||
if (evaluationFunctions[nodeKind]) {
|
||||
throw Error(
|
||||
`Multiple evaluation functions registered for the nodeKind \x1b[4m${nodeKind}`
|
||||
)
|
||||
}
|
||||
evaluationFunctions[nodeKind] = evaluationFunction
|
||||
}
|
|
@ -1,179 +0,0 @@
|
|||
import { Evaluation, Unit } from './AST/types'
|
||||
import { simplifyNodeUnit } from './nodeUnits'
|
||||
import { formatUnit, serializeUnit } from './units'
|
||||
|
||||
export const numberFormatter =
|
||||
({
|
||||
style,
|
||||
maximumFractionDigits = 2,
|
||||
minimumFractionDigits = 0,
|
||||
language,
|
||||
}: {
|
||||
style?: string
|
||||
maximumFractionDigits?: number
|
||||
minimumFractionDigits?: number
|
||||
language?: string
|
||||
}) =>
|
||||
(value: number) => {
|
||||
// When we format currency we don't want to display a single decimal digit
|
||||
// ie 8,1€ but we want to display 8,10€
|
||||
const adaptedMinimumFractionDigits =
|
||||
style === 'currency' &&
|
||||
maximumFractionDigits >= 2 &&
|
||||
minimumFractionDigits === 0 &&
|
||||
!Number.isInteger(value)
|
||||
? 2
|
||||
: minimumFractionDigits
|
||||
return Intl.NumberFormat(language, {
|
||||
style,
|
||||
currency: 'EUR',
|
||||
maximumFractionDigits,
|
||||
minimumFractionDigits: adaptedMinimumFractionDigits,
|
||||
}).format(value)
|
||||
}
|
||||
|
||||
export const formatCurrency = (
|
||||
nodeValue: number | undefined,
|
||||
language: string
|
||||
) => {
|
||||
return nodeValue == null
|
||||
? ''
|
||||
: (formatNumber({ unit: '€', language, nodeValue }) ?? '').replace(
|
||||
/^(-)?€/,
|
||||
'$1€\u00A0'
|
||||
)
|
||||
}
|
||||
|
||||
export const formatPercentage = (nodeValue: number | undefined) =>
|
||||
nodeValue == null
|
||||
? ''
|
||||
: formatNumber({ unit: '%', nodeValue, maximumFractionDigits: 2 })
|
||||
|
||||
type formatValueOptions = {
|
||||
maximumFractionDigits?: number
|
||||
minimumFractionDigits?: number
|
||||
language?: string
|
||||
unit?: Unit | string
|
||||
formatUnit?: formatUnit
|
||||
nodeValue: number
|
||||
}
|
||||
|
||||
function formatNumber({
|
||||
maximumFractionDigits,
|
||||
minimumFractionDigits,
|
||||
language,
|
||||
formatUnit,
|
||||
unit,
|
||||
nodeValue,
|
||||
}: formatValueOptions) {
|
||||
if (typeof nodeValue !== 'number') {
|
||||
return nodeValue
|
||||
}
|
||||
const serializedUnit = unit
|
||||
? serializeUnit(unit, nodeValue, formatUnit)
|
||||
: undefined
|
||||
switch (serializedUnit) {
|
||||
case '€':
|
||||
return numberFormatter({
|
||||
style: 'currency',
|
||||
maximumFractionDigits,
|
||||
minimumFractionDigits,
|
||||
language,
|
||||
})(nodeValue)
|
||||
case '%':
|
||||
return numberFormatter({
|
||||
style: 'percent',
|
||||
maximumFractionDigits,
|
||||
language,
|
||||
})(nodeValue / 100)
|
||||
default:
|
||||
return (
|
||||
numberFormatter({
|
||||
style: 'decimal',
|
||||
minimumFractionDigits,
|
||||
maximumFractionDigits,
|
||||
language,
|
||||
})(nodeValue) +
|
||||
(typeof serializedUnit === 'string' ? `\u00A0${serializedUnit}` : '')
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export function capitalise0(name: undefined): undefined
|
||||
export function capitalise0(name: string): string
|
||||
export function capitalise0(name?: string) {
|
||||
return name && name[0].toUpperCase() + name.slice(1)
|
||||
}
|
||||
|
||||
const booleanTranslations = {
|
||||
fr: { true: 'Oui', false: 'Non' },
|
||||
en: { true: 'Yes', false: 'No' },
|
||||
}
|
||||
|
||||
type Options = {
|
||||
language?: string
|
||||
displayedUnit?: string
|
||||
precision?: number
|
||||
formatUnit?: formatUnit
|
||||
}
|
||||
|
||||
export function formatValue(
|
||||
value: number | { nodeValue: Evaluation; unit?: Unit } | undefined,
|
||||
|
||||
{ language = 'fr', displayedUnit, formatUnit, precision = 2 }: Options = {}
|
||||
) {
|
||||
let nodeValue =
|
||||
typeof value === 'number' || typeof value === 'undefined'
|
||||
? value
|
||||
: value.nodeValue
|
||||
|
||||
if (
|
||||
(typeof nodeValue === 'number' && Number.isNaN(nodeValue)) ||
|
||||
nodeValue == null
|
||||
) {
|
||||
return '-'
|
||||
}
|
||||
if (typeof nodeValue === 'string') {
|
||||
return capitalise0(nodeValue.replace('\\n', '\n'))
|
||||
}
|
||||
if (typeof nodeValue === 'object') return (nodeValue as any).nom
|
||||
if (typeof nodeValue === 'boolean')
|
||||
return booleanTranslations[language][nodeValue]
|
||||
if (typeof nodeValue === 'number') {
|
||||
let unit =
|
||||
typeof value === 'number' ||
|
||||
typeof value === 'undefined' ||
|
||||
!('unit' in value)
|
||||
? undefined
|
||||
: value.unit
|
||||
if (unit) {
|
||||
const simplifiedNode = simplifyNodeUnit({
|
||||
unit,
|
||||
nodeValue,
|
||||
})
|
||||
unit = simplifiedNode.unit
|
||||
nodeValue = simplifiedNode.nodeValue as number
|
||||
}
|
||||
return formatNumber({
|
||||
minimumFractionDigits: 0,
|
||||
maximumFractionDigits: precision,
|
||||
language,
|
||||
formatUnit,
|
||||
nodeValue,
|
||||
unit: displayedUnit ?? unit,
|
||||
})
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
export function serializeValue(
|
||||
{ nodeValue, unit }: { nodeValue: Evaluation; unit?: Unit },
|
||||
{ format }: { format: formatUnit }
|
||||
) {
|
||||
const serializedUnit = (
|
||||
unit && typeof nodeValue === 'number'
|
||||
? serializeUnit(unit, nodeValue, format)
|
||||
: ''
|
||||
)?.replace(/\s*\/\s*/g, '/')
|
||||
return `${nodeValue} ${serializedUnit}`.trim()
|
||||
}
|
|
@ -1,117 +0,0 @@
|
|||
# This grammar is inspired by the "fancier grammar" tab of the nearley playground : https://omrelli.ug/nearley-playground
|
||||
|
||||
# Look for the PEMDAS system : Parentheses, Exponents (omitted here), Multiplication, and you should guess the rest :)
|
||||
|
||||
# This preprocessor was disabled because it doesn't work with Jest
|
||||
# @preprocessor esmodule
|
||||
|
||||
@{%
|
||||
const {
|
||||
string, date, variable, binaryOperation, unaryOperation, boolean, number, numberWithUnit, JSONObject
|
||||
} = require('./grammarFunctions')
|
||||
|
||||
const moo = require("moo");
|
||||
|
||||
const dateRegexp = `(?:(?:0?[1-9]|[12][0-9]|3[01])\\/)?(?:0?[1-9]|1[012])\\/\\d{4}`
|
||||
const letter = '[a-zA-Z\u00C0-\u017F€$%]';
|
||||
const letterOrNumber = '[a-zA-Z\u00C0-\u017F0-9\'°]';
|
||||
const word = `${letter}(?:[-']?${letterOrNumber}+)*`;
|
||||
const wordOrNumber = `(?:${word}|${letterOrNumber}+)`
|
||||
const words = `${word}(?:[,\\s]?${wordOrNumber}+)*`
|
||||
const periodWord = `\\| ${word}(?:[\\s]${word})*`
|
||||
|
||||
const numberRegExp = '-?(?:[1-9][0-9]+|[0-9])(?:\\.[0-9]+)?';
|
||||
const lexer = moo.compile({
|
||||
|
||||
'(': '(',
|
||||
')': ')',
|
||||
'[': '[',
|
||||
']': ']',
|
||||
comparison: ['>','<','>=','<=','=','!='],
|
||||
infinity: 'Infinity',
|
||||
colon: " : ",
|
||||
date: new RegExp(dateRegexp),
|
||||
periodWord: new RegExp(periodWord),
|
||||
words: new RegExp(words),
|
||||
number: new RegExp(numberRegExp),
|
||||
string: /'.*'/,
|
||||
JSONObject: /{.*}/,
|
||||
additionSubstraction: /[\+-]/,
|
||||
multiplicationDivision: ['*','/'],
|
||||
dot: ' . ',
|
||||
'.': '.',
|
||||
letterOrNumber: new RegExp(letterOrNumber),
|
||||
space: { match: /[\s]+/, lineBreaks: true },
|
||||
});
|
||||
|
||||
const join = (args) => ({value: (args.map(x => x && x.value).join(""))})
|
||||
const flattenJoin = ([a, b]) => Array.isArray(b) ? join([a, ...b]) : a
|
||||
%}
|
||||
|
||||
@lexer lexer
|
||||
|
||||
main ->
|
||||
Comparison {% id %}
|
||||
| NumericValue {% id %}
|
||||
| Date {% id %}
|
||||
| NonNumericTerminal {% id %}
|
||||
| JSONObject {% id %}
|
||||
|
||||
NumericValue ->
|
||||
AdditionSubstraction {% id %}
|
||||
| Negation {% id %}
|
||||
|
||||
NumericTerminal ->
|
||||
Variable {% id %}
|
||||
| number {% id %}
|
||||
|
||||
Negation ->
|
||||
"-" %space Parentheses {% unaryOperation('calculation') %}
|
||||
|
||||
Parentheses ->
|
||||
"(" NumericValue ")" {% ([,e]) => e %}
|
||||
| NumericTerminal {% id %}
|
||||
|
||||
Date ->
|
||||
Variable {% id %}
|
||||
| %date {% date %}
|
||||
|
||||
Comparison ->
|
||||
Comparable %space %comparison %space Comparable {% binaryOperation('comparison')%}
|
||||
| Date %space %comparison %space Date {% binaryOperation('comparison')%}
|
||||
|
||||
Comparable -> ( AdditionSubstraction | NonNumericTerminal) {% ([[e]]) => e %}
|
||||
|
||||
NonNumericTerminal ->
|
||||
boolean {% id %}
|
||||
| string {% id %}
|
||||
|
||||
Variable -> %words (%dot %words {% ([,words]) => words %}):* {% variable %}
|
||||
|
||||
UnitDenominator ->
|
||||
(%space):? "/" %words {% join %}
|
||||
UnitNumerator -> %words ("." %words):? {% flattenJoin %}
|
||||
|
||||
Unit -> UnitNumerator:? UnitDenominator:* {% flattenJoin %}
|
||||
|
||||
AdditionSubstraction ->
|
||||
AdditionSubstraction %space %additionSubstraction %space MultiplicationDivision {% binaryOperation('calculation') %}
|
||||
| MultiplicationDivision {% id %}
|
||||
|
||||
MultiplicationDivision ->
|
||||
MultiplicationDivision %space %multiplicationDivision %space Parentheses {% binaryOperation('calculation') %}
|
||||
| Parentheses {% id %}
|
||||
|
||||
|
||||
boolean ->
|
||||
"oui" {% boolean(true) %}
|
||||
| "non" {% boolean(false) %}
|
||||
|
||||
number ->
|
||||
%number {% number %}
|
||||
| %infinity {% number %}
|
||||
| %number (%space):? Unit {% numberWithUnit %}
|
||||
|
||||
string -> %string {% string %}
|
||||
|
||||
JSONObject -> %JSONObject {% JSONObject %}
|
|
@ -1,70 +0,0 @@
|
|||
/* Those are postprocessor functions for the Nearley grammar.ne.
|
||||
The advantage of putting them here is to get prettier's JS formatting, since Nealrey doesn't support it https://github.com/kach/nearley/issues/310 */
|
||||
import { normalizeDateString } from './date'
|
||||
|
||||
export let binaryOperation =
|
||||
(operationType) =>
|
||||
([A, , operator, , B]) => ({
|
||||
[operator]: {
|
||||
operationType,
|
||||
explanation: [A, B],
|
||||
},
|
||||
})
|
||||
|
||||
export let unaryOperation =
|
||||
(operationType) =>
|
||||
([operator, , A]) => ({
|
||||
[operator]: {
|
||||
operationType,
|
||||
explanation: [number([{ value: '0' }]), A],
|
||||
},
|
||||
})
|
||||
|
||||
export let variable = ([firstFragment, nextFragment], _, reject) => {
|
||||
const fragments = [firstFragment, ...nextFragment].map(({ value }) => value)
|
||||
if (!nextFragment.length && ['oui', 'non'].includes(firstFragment)) {
|
||||
return reject
|
||||
}
|
||||
return {
|
||||
variable: fragments.join(' . '),
|
||||
}
|
||||
}
|
||||
|
||||
export const JSONObject = ([{ value }]) => {
|
||||
console.log(value)
|
||||
// TODO
|
||||
}
|
||||
export let number = ([{ value }]) => ({
|
||||
constant: {
|
||||
type: 'number',
|
||||
nodeValue: parseFloat(value),
|
||||
},
|
||||
})
|
||||
|
||||
export let numberWithUnit = (value) => ({
|
||||
...number(value),
|
||||
unité: value[2].value,
|
||||
})
|
||||
|
||||
export let date = ([{ value }]) => {
|
||||
return {
|
||||
constant: {
|
||||
type: 'date',
|
||||
nodeValue: normalizeDateString(value),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export let boolean = (nodeValue) => () => ({
|
||||
constant: {
|
||||
type: 'boolean',
|
||||
nodeValue,
|
||||
},
|
||||
})
|
||||
|
||||
export let string = ([{ value }]) => ({
|
||||
constant: {
|
||||
type: 'string',
|
||||
nodeValue: value.slice(1, -1),
|
||||
},
|
||||
})
|
|
@ -1,293 +0,0 @@
|
|||
import { reduceAST } from './AST'
|
||||
import { ASTNode, EvaluatedNode, NodeKind } from './AST/types'
|
||||
import { evaluationFunctions } from './evaluationFunctions'
|
||||
import parse from './parse'
|
||||
import parsePublicodes, { disambiguateReference } from './parsePublicodes'
|
||||
import {
|
||||
getReplacements,
|
||||
inlineReplacements,
|
||||
ReplacementRule,
|
||||
} from './replacement'
|
||||
import { Rule, RuleNode } from './rule'
|
||||
import * as utils from './ruleUtils'
|
||||
import { formatUnit, getUnitKey } from './units'
|
||||
|
||||
const emptyCache = (): Cache => ({
|
||||
_meta: {
|
||||
parentRuleStack: [],
|
||||
evaluationRuleStack: [],
|
||||
disableApplicabilityContextCounter: 0,
|
||||
},
|
||||
nodes: new Map(),
|
||||
nodesApplicability: new Map(),
|
||||
})
|
||||
|
||||
type Cache = {
|
||||
_meta: {
|
||||
parentRuleStack: Array<string>
|
||||
evaluationRuleStack: Array<string>
|
||||
disableApplicabilityContextCounter: number
|
||||
inversionFail?:
|
||||
| {
|
||||
given: string
|
||||
estimated: string
|
||||
}
|
||||
| true
|
||||
inRecalcul?: boolean
|
||||
filter?: string
|
||||
}
|
||||
nodes: Map<PublicodesExpression | ASTNode, EvaluatedNode>
|
||||
nodesApplicability: Map<PublicodesExpression | ASTNode, EvaluatedNode>
|
||||
}
|
||||
|
||||
export type EvaluationOptions = Partial<{
|
||||
unit: string
|
||||
}>
|
||||
|
||||
export { reduceAST, makeASTTransformer as transformAST } from './AST/index'
|
||||
export {
|
||||
Evaluation,
|
||||
Unit,
|
||||
NotYetDefined,
|
||||
isNotYetDefined,
|
||||
NotApplicable,
|
||||
isNotApplicable,
|
||||
} from './AST/types'
|
||||
export { capitalise0, formatValue } from './format'
|
||||
export { simplifyNodeUnit } from './nodeUnits'
|
||||
export { default as serializeEvaluation } from './serializeEvaluation'
|
||||
export { parseUnit, serializeUnit } from './units'
|
||||
export { parsePublicodes, utils }
|
||||
export { Rule, RuleNode, ASTNode, EvaluatedNode }
|
||||
|
||||
export type PublicodesExpression = string | Record<string, unknown> | number
|
||||
|
||||
export type Logger = {
|
||||
log(message: string): void
|
||||
warn(message: string): void
|
||||
error(message: string): void
|
||||
}
|
||||
|
||||
type Options = {
|
||||
logger: Logger
|
||||
getUnitKey?: getUnitKey
|
||||
formatUnit?: formatUnit
|
||||
}
|
||||
|
||||
export type EvaluationFunction<Kind extends NodeKind = NodeKind> = (
|
||||
this: Engine,
|
||||
node: ASTNode & { nodeKind: Kind }
|
||||
) => ASTNode & { nodeKind: Kind } & EvaluatedNode
|
||||
|
||||
export type ParsedRules<Name extends string> = Record<
|
||||
Name,
|
||||
RuleNode & { dottedName: Name }
|
||||
>
|
||||
|
||||
export default class Engine<Name extends string = string> {
|
||||
parsedRules: ParsedRules<Name>
|
||||
parsedSituation: Record<string, ASTNode> = {}
|
||||
replacements: Record<string, Array<ReplacementRule>> = {}
|
||||
cache: Cache = emptyCache()
|
||||
options: Options
|
||||
|
||||
// The subEngines attribute is used to get an outside reference to the
|
||||
// recalcul intermediate calculations. The recalcul mechanism uses
|
||||
// `shallowCopy` to instanciate a new engine, and we want to keep a reference
|
||||
// to it for the documentation.
|
||||
//
|
||||
// TODO: A better implementation would to remove the "runtime" concept of
|
||||
// "subEngines" and instead duplicate all rules names in the scope of the
|
||||
// recalcul as described in
|
||||
// https://github.com/betagouv/publicodes/discussions/92
|
||||
subEngines: Array<Engine<Name>> = []
|
||||
subEngineId: number | undefined
|
||||
|
||||
constructor(
|
||||
rules: string | Record<string, Rule> = {},
|
||||
options: Partial<Options> = {}
|
||||
) {
|
||||
this.options = { ...options, logger: options.logger ?? console }
|
||||
this.parsedRules = parsePublicodes(rules, this.options) as ParsedRules<Name>
|
||||
this.replacements = getReplacements(this.parsedRules)
|
||||
}
|
||||
|
||||
setOptions(options: Partial<Options>) {
|
||||
this.options = { ...this.options, ...options }
|
||||
}
|
||||
|
||||
resetCache() {
|
||||
this.cache = emptyCache()
|
||||
}
|
||||
|
||||
setSituation(
|
||||
situation: Partial<Record<Name, PublicodesExpression | ASTNode>> = {}
|
||||
) {
|
||||
this.resetCache()
|
||||
this.parsedSituation = Object.fromEntries(
|
||||
Object.entries(situation).map(([key, value]) => {
|
||||
if (value && typeof value === 'object' && 'nodeKind' in value) {
|
||||
return [key, value as ASTNode]
|
||||
}
|
||||
const parsedValue =
|
||||
value && typeof value === 'object' && 'nodeKind' in value
|
||||
? (value as ASTNode)
|
||||
: this.parse(value, {
|
||||
dottedName: `situation [${key}]`,
|
||||
parsedRules: {},
|
||||
...this.options,
|
||||
})
|
||||
return [key, parsedValue]
|
||||
})
|
||||
)
|
||||
return this
|
||||
}
|
||||
|
||||
private parse(...args: Parameters<typeof parse>) {
|
||||
return inlineReplacements(
|
||||
this.replacements,
|
||||
this.options.logger
|
||||
)(disambiguateReference(this.parsedRules)(parse(...args)))
|
||||
}
|
||||
|
||||
inversionFail(): boolean {
|
||||
return !!this.cache._meta.inversionFail
|
||||
}
|
||||
|
||||
getRule(dottedName: Name): ParsedRules<Name>[Name] {
|
||||
if (!(dottedName in this.parsedRules)) {
|
||||
throw new Error(`La règle '${dottedName}' n'existe pas`)
|
||||
}
|
||||
return this.parsedRules[dottedName]
|
||||
}
|
||||
|
||||
getParsedRules(): ParsedRules<Name> {
|
||||
return this.parsedRules
|
||||
}
|
||||
|
||||
getOptions(): Options {
|
||||
return this.options
|
||||
}
|
||||
|
||||
evaluate<N extends ASTNode = ASTNode>(value: N): N & EvaluatedNode
|
||||
evaluate(value: PublicodesExpression): EvaluatedNode
|
||||
evaluate(value: PublicodesExpression | ASTNode): EvaluatedNode {
|
||||
const cachedNode = this.cache.nodes.get(value)
|
||||
// The evaluation of parent applicabilty is slightly different from
|
||||
// regular rules since we cut some of the paths (sums) for optimization.
|
||||
// That's why we need to have a separate cache for this evaluation.
|
||||
|
||||
if (cachedNode !== undefined) {
|
||||
return cachedNode
|
||||
} else if (this.inApplicabilityEvaluationContext) {
|
||||
const cachedNodeApplicability = this.cache.nodesApplicability.get(value)
|
||||
if (cachedNodeApplicability) {
|
||||
return cachedNodeApplicability
|
||||
}
|
||||
}
|
||||
|
||||
let parsedNode: ASTNode
|
||||
if (!value || typeof value !== 'object' || !('nodeKind' in value)) {
|
||||
parsedNode = this.parse(value, {
|
||||
dottedName: 'evaluation',
|
||||
parsedRules: {},
|
||||
...this.options,
|
||||
})
|
||||
} else {
|
||||
parsedNode = value as ASTNode
|
||||
}
|
||||
|
||||
if (!evaluationFunctions[parsedNode.nodeKind]) {
|
||||
throw Error(`Unknown "nodeKind": ${parsedNode.nodeKind}`)
|
||||
}
|
||||
|
||||
const evaluatedNode = evaluationFunctions[parsedNode.nodeKind].call(
|
||||
this,
|
||||
parsedNode
|
||||
)
|
||||
|
||||
// TODO: In most cases the two evaluation provide the same result, this
|
||||
// could be optimized. The idea would be to use the “nodesApplicability”
|
||||
// cache iff the rule uses a sum mechanism (ie, some paths are cut from
|
||||
// the full evaluaiton).
|
||||
if (!this.inApplicabilityEvaluationContext) {
|
||||
this.cache.nodes.set(value, evaluatedNode)
|
||||
} else {
|
||||
this.cache.nodesApplicability.set(value, evaluatedNode)
|
||||
}
|
||||
return evaluatedNode
|
||||
}
|
||||
|
||||
/**
|
||||
* Shallow Engine instance copy. Keeps references to the original Engine instance attributes.
|
||||
*/
|
||||
shallowCopy(): Engine<Name> {
|
||||
const newEngine = new Engine<Name>()
|
||||
newEngine.options = this.options
|
||||
newEngine.parsedRules = this.parsedRules
|
||||
newEngine.replacements = this.replacements
|
||||
newEngine.parsedSituation = this.parsedSituation
|
||||
newEngine.cache = this.cache
|
||||
newEngine.subEngineId = this.subEngines.length
|
||||
this.subEngines.push(newEngine)
|
||||
return newEngine
|
||||
}
|
||||
|
||||
get inApplicabilityEvaluationContext(): boolean {
|
||||
return (
|
||||
this.cache._meta.parentRuleStack.length > 0 &&
|
||||
this.cache._meta.disableApplicabilityContextCounter === 0
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
This function allows to mimic the former 'isApplicable' property on evaluatedRules
|
||||
|
||||
It will be deprecated when applicability will be encoded as a Literal type
|
||||
*/
|
||||
export function UNSAFE_isNotApplicable<DottedName extends string = string>(
|
||||
engine: Engine<DottedName>,
|
||||
dottedName: DottedName
|
||||
): boolean {
|
||||
const rule = engine.getRule(dottedName)
|
||||
return reduceAST<boolean>(
|
||||
function (isNotApplicable, node, fn) {
|
||||
if (isNotApplicable) return isNotApplicable
|
||||
if (!('nodeValue' in node)) {
|
||||
return isNotApplicable
|
||||
}
|
||||
if (node.nodeKind === 'variations') {
|
||||
return node.explanation.some(
|
||||
({ consequence }) =>
|
||||
fn(consequence) ||
|
||||
((consequence as any).nodeValue === false &&
|
||||
(consequence as any).dottedName !== dottedName)
|
||||
)
|
||||
}
|
||||
if (node.nodeKind === 'reference' && node.dottedName === dottedName) {
|
||||
return fn(engine.evaluate(rule))
|
||||
}
|
||||
if (node.nodeKind === 'applicable si') {
|
||||
return (
|
||||
(node.explanation.condition as any).nodeValue === false ||
|
||||
fn(node.explanation.valeur)
|
||||
)
|
||||
}
|
||||
if (node.nodeKind === 'non applicable si') {
|
||||
return (
|
||||
(node.explanation.condition as any).nodeValue !== false &&
|
||||
(node.explanation.condition as any).nodeValue !== null
|
||||
)
|
||||
}
|
||||
if (node.nodeKind === 'rule') {
|
||||
return (
|
||||
(node.explanation.parent as any).nodeValue === false ||
|
||||
fn(node.explanation.valeur)
|
||||
)
|
||||
}
|
||||
},
|
||||
false,
|
||||
engine.evaluate(dottedName)
|
||||
)
|
||||
}
|
|
@ -1,74 +0,0 @@
|
|||
import { EvaluationFunction, serializeUnit } from '..'
|
||||
import { ASTNode } from '../AST/types'
|
||||
import { warning } from '../error'
|
||||
import { mergeAllMissing } from '../evaluation'
|
||||
import { registerEvaluationFunction } from '../evaluationFunctions'
|
||||
import { convertNodeToUnit } from '../nodeUnits'
|
||||
import parse from '../parse'
|
||||
import { Context } from '../parsePublicodes'
|
||||
|
||||
export type AbattementNode = {
|
||||
explanation: {
|
||||
assiette: ASTNode
|
||||
abattement: ASTNode
|
||||
}
|
||||
nodeKind: 'abattement'
|
||||
}
|
||||
|
||||
const evaluateAbattement: EvaluationFunction<'abattement'> = function (node) {
|
||||
const assiette = this.evaluate(node.explanation.assiette)
|
||||
let abattement = this.evaluate(node.explanation.abattement)
|
||||
const percentageAbattement = serializeUnit(abattement.unit) === '%'
|
||||
if (assiette.unit && !percentageAbattement) {
|
||||
try {
|
||||
abattement = convertNodeToUnit(assiette.unit, abattement)
|
||||
} catch (e) {
|
||||
warning(
|
||||
this.options.logger,
|
||||
this.cache._meta.evaluationRuleStack[0],
|
||||
"Impossible de convertir les unités de l'allègement entre elles",
|
||||
e
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const assietteValue = assiette.nodeValue as number | null
|
||||
const abattementValue = abattement.nodeValue as number | null
|
||||
const nodeValue = abattementValue
|
||||
? assietteValue == null
|
||||
? null
|
||||
: abattementValue == null
|
||||
? assietteValue == 0
|
||||
? 0
|
||||
: null
|
||||
: serializeUnit(abattement.unit) === '%'
|
||||
? Math.max(0, assietteValue - (abattementValue / 100) * assietteValue)
|
||||
: Math.max(0, assietteValue - abattementValue)
|
||||
: assietteValue
|
||||
|
||||
return {
|
||||
...node,
|
||||
nodeValue,
|
||||
unit: assiette.unit,
|
||||
missingVariables: mergeAllMissing([assiette, abattement]),
|
||||
explanation: {
|
||||
assiette,
|
||||
abattement,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export default function parseAbattement(v, context: Context) {
|
||||
const explanation = {
|
||||
assiette: parse(v.valeur, context),
|
||||
abattement: parse(v.abattement, context),
|
||||
}
|
||||
return {
|
||||
explanation,
|
||||
nodeKind: parseAbattement.nom,
|
||||
}
|
||||
}
|
||||
|
||||
parseAbattement.nom = 'abattement' as const
|
||||
|
||||
registerEvaluationFunction(parseAbattement.nom, evaluateAbattement)
|
|
@ -1,51 +0,0 @@
|
|||
import { EvaluationFunction } from '..'
|
||||
import parse from '../parse'
|
||||
import { bonus, mergeMissing } from '../evaluation'
|
||||
import { registerEvaluationFunction } from '../evaluationFunctions'
|
||||
import { ASTNode, EvaluatedNode } from '../AST/types'
|
||||
|
||||
export type ApplicableSiNode = {
|
||||
explanation: {
|
||||
condition: ASTNode
|
||||
valeur: ASTNode
|
||||
}
|
||||
nodeKind: 'applicable si'
|
||||
}
|
||||
|
||||
const evaluate: EvaluationFunction<'applicable si'> = function (node) {
|
||||
const explanation = { ...node.explanation }
|
||||
const condition = this.evaluate(explanation.condition)
|
||||
let valeur = explanation.valeur
|
||||
if (condition.nodeValue !== false) {
|
||||
valeur = this.evaluate(valeur)
|
||||
}
|
||||
return {
|
||||
...node,
|
||||
nodeValue:
|
||||
condition.nodeValue == null || condition.nodeValue === false
|
||||
? condition.nodeValue
|
||||
: 'nodeValue' in valeur
|
||||
? (valeur as EvaluatedNode).nodeValue
|
||||
: null,
|
||||
explanation: { valeur, condition },
|
||||
missingVariables: mergeMissing(
|
||||
'missingVariables' in valeur ? valeur.missingVariables : {},
|
||||
bonus(condition.missingVariables)
|
||||
),
|
||||
...('unit' in valeur && { unit: valeur.unit }),
|
||||
}
|
||||
}
|
||||
parseApplicable.nom = 'applicable si' as const
|
||||
|
||||
export default function parseApplicable(v, context) {
|
||||
const explanation = {
|
||||
valeur: parse(v.valeur, context),
|
||||
condition: parse(v[parseApplicable.nom], context),
|
||||
}
|
||||
return {
|
||||
explanation,
|
||||
nodeKind: parseApplicable.nom,
|
||||
}
|
||||
}
|
||||
|
||||
registerEvaluationFunction(parseApplicable.nom, evaluate)
|
|
@ -1,75 +0,0 @@
|
|||
import { EvaluationFunction, simplifyNodeUnit } from '..'
|
||||
import { mergeAllMissing } from '../evaluation'
|
||||
import { registerEvaluationFunction } from '../evaluationFunctions'
|
||||
import parse from '../parse'
|
||||
import { ASTNode, EvaluatedNode } from '../AST/types'
|
||||
import { serializeUnit } from '../units'
|
||||
import { evaluationError } from '../error'
|
||||
|
||||
export type ArrondiNode = {
|
||||
explanation: {
|
||||
arrondi: ASTNode
|
||||
valeur: ASTNode
|
||||
}
|
||||
nodeKind: 'arrondi'
|
||||
}
|
||||
|
||||
function roundWithPrecision(n: number, fractionDigits: number) {
|
||||
return +n.toFixed(fractionDigits)
|
||||
}
|
||||
|
||||
const evaluate: EvaluationFunction<'arrondi'> = function (node) {
|
||||
// We need to simplify the node unit to correctly round values containing
|
||||
// percentages units, see #1358
|
||||
const valeur = simplifyNodeUnit(this.evaluate(node.explanation.valeur))
|
||||
const nodeValue = valeur.nodeValue
|
||||
let arrondi = node.explanation.arrondi
|
||||
if (nodeValue !== false) {
|
||||
arrondi = this.evaluate(arrondi)
|
||||
|
||||
if (
|
||||
typeof (arrondi as EvaluatedNode).nodeValue === 'number' &&
|
||||
!serializeUnit((arrondi as EvaluatedNode).unit)?.match(/décimales?/)
|
||||
) {
|
||||
evaluationError(
|
||||
this.options.logger,
|
||||
this.cache._meta.evaluationRuleStack[0],
|
||||
`L'unité ${serializeUnit(
|
||||
(arrondi as EvaluatedNode).unit
|
||||
)} de l'arrondi est inconnu. Vous devez utiliser l'unité “décimales”`
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
...node,
|
||||
nodeValue:
|
||||
typeof valeur.nodeValue !== 'number' || !('nodeValue' in arrondi)
|
||||
? valeur.nodeValue
|
||||
: typeof arrondi.nodeValue === 'number'
|
||||
? roundWithPrecision(valeur.nodeValue, arrondi.nodeValue)
|
||||
: arrondi.nodeValue === true
|
||||
? roundWithPrecision(valeur.nodeValue, 0)
|
||||
: arrondi.nodeValue === null
|
||||
? null
|
||||
: valeur.nodeValue,
|
||||
explanation: { valeur, arrondi },
|
||||
missingVariables: mergeAllMissing([valeur, arrondi]),
|
||||
unit: valeur.unit,
|
||||
}
|
||||
}
|
||||
|
||||
export default function parseArrondi(v, context) {
|
||||
const explanation = {
|
||||
valeur: parse(v.valeur, context),
|
||||
arrondi: parse(v.arrondi, context),
|
||||
}
|
||||
return {
|
||||
explanation,
|
||||
nodeKind: parseArrondi.nom,
|
||||
}
|
||||
}
|
||||
|
||||
parseArrondi.nom = 'arrondi' as const
|
||||
|
||||
registerEvaluationFunction(parseArrondi.nom, evaluate)
|
|
@ -1,102 +0,0 @@
|
|||
import { EvaluationFunction } from '..'
|
||||
import { ASTNode } from '../AST/types'
|
||||
import { defaultNode, mergeAllMissing } from '../evaluation'
|
||||
import { registerEvaluationFunction } from '../evaluationFunctions'
|
||||
import parse from '../parse'
|
||||
import { convertUnit, parseUnit } from '../units'
|
||||
import {
|
||||
evaluatePlafondUntilActiveTranche,
|
||||
parseTranches,
|
||||
TrancheNodes,
|
||||
} from './trancheUtils'
|
||||
|
||||
// Barème en taux marginaux.
|
||||
export type BarèmeNode = {
|
||||
explanation: {
|
||||
tranches: TrancheNodes
|
||||
multiplicateur: ASTNode
|
||||
assiette: ASTNode
|
||||
}
|
||||
nodeKind: 'barème'
|
||||
}
|
||||
export default function parseBarème(v, context): BarèmeNode {
|
||||
const explanation = {
|
||||
assiette: parse(v.assiette, context),
|
||||
multiplicateur: v.multiplicateur
|
||||
? parse(v.multiplicateur, context)
|
||||
: defaultNode(1),
|
||||
tranches: parseTranches(v.tranches, context),
|
||||
}
|
||||
return {
|
||||
explanation,
|
||||
nodeKind: 'barème',
|
||||
}
|
||||
}
|
||||
|
||||
function evaluateBarème(tranches, assiette, evaluate) {
|
||||
return tranches.map((tranche) => {
|
||||
if (tranche.isAfterActive) {
|
||||
return { ...tranche, nodeValue: 0 }
|
||||
}
|
||||
const taux = evaluate(tranche.taux)
|
||||
const missingVariables = mergeAllMissing([taux, tranche])
|
||||
|
||||
if (
|
||||
[
|
||||
assiette.nodeValue,
|
||||
taux.nodeValue,
|
||||
tranche.plafondValue,
|
||||
tranche.plancherValue,
|
||||
].some((value) => value === null)
|
||||
) {
|
||||
return {
|
||||
...tranche,
|
||||
taux,
|
||||
nodeValue: null,
|
||||
missingVariables,
|
||||
}
|
||||
}
|
||||
return {
|
||||
...tranche,
|
||||
taux,
|
||||
...('unit' in assiette && { unit: assiette.unit }),
|
||||
nodeValue:
|
||||
(Math.min(assiette.nodeValue, tranche.plafondValue) -
|
||||
tranche.plancherValue) *
|
||||
convertUnit(taux.unit, parseUnit(''), taux.nodeValue as number),
|
||||
missingVariables,
|
||||
}
|
||||
})
|
||||
}
|
||||
const evaluate: EvaluationFunction<'barème'> = function (node) {
|
||||
const evaluate = this.evaluate.bind(this)
|
||||
const assiette = this.evaluate(node.explanation.assiette)
|
||||
const multiplicateur = this.evaluate(node.explanation.multiplicateur)
|
||||
const tranches = evaluateBarème(
|
||||
evaluatePlafondUntilActiveTranche.call(this, {
|
||||
parsedTranches: node.explanation.tranches,
|
||||
assiette,
|
||||
multiplicateur,
|
||||
}),
|
||||
assiette,
|
||||
evaluate
|
||||
)
|
||||
const nodeValue = tranches.reduce(
|
||||
(value, { nodeValue }) => (nodeValue == null ? null : value + nodeValue),
|
||||
0
|
||||
)
|
||||
|
||||
return {
|
||||
...node,
|
||||
nodeValue,
|
||||
missingVariables: mergeAllMissing(tranches),
|
||||
explanation: {
|
||||
assiette,
|
||||
multiplicateur,
|
||||
tranches,
|
||||
},
|
||||
unit: assiette.unit,
|
||||
} as any
|
||||
}
|
||||
|
||||
registerEvaluationFunction('barème', evaluate)
|
|
@ -1,25 +0,0 @@
|
|||
import { ASTNode } from '../AST/types'
|
||||
import parse from '../parse'
|
||||
|
||||
export const decompose = (k, v, context): ASTNode => {
|
||||
const { composantes, ...factoredKeys } = v
|
||||
const explanation = parse(
|
||||
{
|
||||
somme: composantes.map((composante) => {
|
||||
const { attributs, ...otherKeys } = composante
|
||||
return {
|
||||
...attributs,
|
||||
[k]: {
|
||||
...factoredKeys,
|
||||
...otherKeys,
|
||||
},
|
||||
}
|
||||
}),
|
||||
},
|
||||
context
|
||||
)
|
||||
return {
|
||||
...explanation,
|
||||
visualisationKind: 'composantes',
|
||||
}
|
||||
}
|
|
@ -1,49 +0,0 @@
|
|||
import { EvaluationFunction } from '..'
|
||||
import { ASTNode } from '../AST/types'
|
||||
import { mergeAllMissing } from '../evaluation'
|
||||
import { registerEvaluationFunction } from '../evaluationFunctions'
|
||||
import parse from '../parse'
|
||||
|
||||
export type TouteCesConditionsNode = {
|
||||
explanation: Array<ASTNode>
|
||||
nodeKind: 'toutes ces conditions'
|
||||
}
|
||||
|
||||
const evaluate: EvaluationFunction<'toutes ces conditions'> = function (node) {
|
||||
const [nodeValue, explanation] = node.explanation.reduce<
|
||||
[boolean | null, Array<ASTNode>]
|
||||
>(
|
||||
([nodeValue, explanation], node) => {
|
||||
if (nodeValue === false) {
|
||||
return [nodeValue, [...explanation, node]]
|
||||
}
|
||||
const evaluatedNode = this.evaluate(node)
|
||||
return [
|
||||
nodeValue === null || evaluatedNode.nodeValue === null
|
||||
? null
|
||||
: !!evaluatedNode.nodeValue,
|
||||
[...explanation, evaluatedNode],
|
||||
]
|
||||
},
|
||||
[true, []]
|
||||
)
|
||||
|
||||
return {
|
||||
...node,
|
||||
nodeValue,
|
||||
explanation,
|
||||
missingVariables: mergeAllMissing(explanation),
|
||||
}
|
||||
}
|
||||
|
||||
export const mecanismAllOf = (v, context) => {
|
||||
if (!Array.isArray(v)) throw new Error('should be array')
|
||||
const explanation = v.map((node) => parse(node, context))
|
||||
|
||||
return {
|
||||
explanation,
|
||||
nodeKind: 'toutes ces conditions',
|
||||
}
|
||||
}
|
||||
|
||||
registerEvaluationFunction('toutes ces conditions', evaluate)
|
|
@ -1,66 +0,0 @@
|
|||
import { EvaluationFunction } from '..'
|
||||
import { ASTNode, EvaluatedNode, Evaluation } from '../AST/types'
|
||||
import { mergeMissing } from '../evaluation'
|
||||
import { registerEvaluationFunction } from '../evaluationFunctions'
|
||||
import parse from '../parse'
|
||||
import { InternalError } from '../error'
|
||||
|
||||
export type UneDeCesConditionsNode = {
|
||||
explanation: Array<ASTNode>
|
||||
nodeKind: 'une de ces conditions'
|
||||
}
|
||||
|
||||
const evaluate: EvaluationFunction<'une de ces conditions'> = function (node) {
|
||||
type Calculations = {
|
||||
explanation: Array<ASTNode | EvaluatedNode>
|
||||
nodeValue: Evaluation<boolean>
|
||||
missingVariables: Record<string, number>
|
||||
}
|
||||
const calculations = node.explanation.reduce<Calculations>(
|
||||
(acc, node) => {
|
||||
if (acc.nodeValue === true) {
|
||||
return {
|
||||
...acc,
|
||||
explanation: [...acc.explanation, node],
|
||||
}
|
||||
}
|
||||
if (acc.nodeValue === null || acc.nodeValue === false) {
|
||||
const evaluatedNode = this.evaluate(node)
|
||||
return {
|
||||
nodeValue: evaluatedNode.nodeValue
|
||||
? true
|
||||
: evaluatedNode.nodeValue === null
|
||||
? null
|
||||
: acc.nodeValue,
|
||||
missingVariables: mergeMissing(
|
||||
acc.missingVariables,
|
||||
evaluatedNode.missingVariables
|
||||
),
|
||||
explanation: [...acc.explanation, evaluatedNode],
|
||||
}
|
||||
}
|
||||
throw new InternalError([node, acc])
|
||||
},
|
||||
{
|
||||
nodeValue: false,
|
||||
missingVariables: {},
|
||||
explanation: [],
|
||||
}
|
||||
)
|
||||
return {
|
||||
...node,
|
||||
...calculations,
|
||||
}
|
||||
}
|
||||
|
||||
export const mecanismOneOf = (v, context) => {
|
||||
if (!Array.isArray(v)) throw new Error('should be array')
|
||||
const explanation = v.map((node) => parse(node, context))
|
||||
|
||||
return {
|
||||
explanation,
|
||||
nodeKind: 'une de ces conditions',
|
||||
}
|
||||
}
|
||||
|
||||
registerEvaluationFunction('une de ces conditions', evaluate)
|
|
@ -1,60 +0,0 @@
|
|||
import { EvaluationFunction } from '..'
|
||||
import { ASTNode, Unit } from '../AST/types'
|
||||
import { convertToDate, convertToString } from '../date'
|
||||
import { defaultNode, mergeAllMissing, parseObject } from '../evaluation'
|
||||
import { registerEvaluationFunction } from '../evaluationFunctions'
|
||||
import { parseUnit } from '../units'
|
||||
|
||||
export type DuréeNode = {
|
||||
explanation: {
|
||||
depuis: ASTNode
|
||||
"jusqu'à": ASTNode
|
||||
}
|
||||
unit: Unit
|
||||
nodeKind: 'durée'
|
||||
}
|
||||
|
||||
const todayString = convertToString(new Date())
|
||||
const objectShape = {
|
||||
depuis: defaultNode(todayString),
|
||||
"jusqu'à": defaultNode(todayString),
|
||||
}
|
||||
const evaluate: EvaluationFunction<'durée'> = function (node) {
|
||||
const from = this.evaluate(node.explanation.depuis)
|
||||
const to = this.evaluate(node.explanation["jusqu'à"])
|
||||
let nodeValue
|
||||
if ([from, to].some(({ nodeValue }) => nodeValue === null)) {
|
||||
nodeValue = null
|
||||
} else {
|
||||
const [fromDate, toDate] = [from.nodeValue, to.nodeValue].map(
|
||||
convertToDate as any
|
||||
)
|
||||
nodeValue = Math.max(
|
||||
0,
|
||||
Math.round(
|
||||
(toDate.getTime() - fromDate.getTime()) / (1000 * 60 * 60 * 24)
|
||||
)
|
||||
)
|
||||
}
|
||||
const missingVariables = mergeAllMissing([from, to])
|
||||
return {
|
||||
...node,
|
||||
missingVariables,
|
||||
nodeValue,
|
||||
explanation: {
|
||||
depuis: from,
|
||||
"jusqu'à": to,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export default (v, context) => {
|
||||
const explanation = parseObject(objectShape, v, context)
|
||||
return {
|
||||
explanation,
|
||||
unit: parseUnit('jour'),
|
||||
nodeKind: 'durée',
|
||||
} as DuréeNode
|
||||
}
|
||||
|
||||
registerEvaluationFunction('durée', evaluate)
|
|
@ -1,89 +0,0 @@
|
|||
import { EvaluationFunction } from '..'
|
||||
import { ASTNode } from '../AST/types'
|
||||
import { defaultNode, mergeAllMissing } from '../evaluation'
|
||||
import { registerEvaluationFunction } from '../evaluationFunctions'
|
||||
import parse from '../parse'
|
||||
import {
|
||||
evaluatePlafondUntilActiveTranche,
|
||||
parseTranches,
|
||||
TrancheNodes,
|
||||
} from './trancheUtils'
|
||||
|
||||
export type GrilleNode = {
|
||||
explanation: {
|
||||
assiette: ASTNode
|
||||
multiplicateur: ASTNode
|
||||
tranches: TrancheNodes
|
||||
}
|
||||
nodeKind: 'grille'
|
||||
}
|
||||
|
||||
export default function parseGrille(v, context): GrilleNode {
|
||||
const explanation = {
|
||||
assiette: parse(v.assiette, context),
|
||||
multiplicateur: v.multiplicateur
|
||||
? parse(v.multiplicateur, context)
|
||||
: defaultNode(1),
|
||||
tranches: parseTranches(v.tranches, context),
|
||||
}
|
||||
return {
|
||||
explanation,
|
||||
nodeKind: 'grille',
|
||||
}
|
||||
}
|
||||
|
||||
const evaluate: EvaluationFunction<'grille'> = function (node) {
|
||||
const evaluate = this.evaluate.bind(this)
|
||||
const assiette = this.evaluate(node.explanation.assiette)
|
||||
const multiplicateur = this.evaluate(node.explanation.multiplicateur)
|
||||
const tranches = evaluatePlafondUntilActiveTranche
|
||||
.call(this, {
|
||||
parsedTranches: node.explanation.tranches,
|
||||
assiette,
|
||||
multiplicateur,
|
||||
})
|
||||
.map((tranche) => {
|
||||
if (tranche.isActive === false) {
|
||||
return tranche
|
||||
}
|
||||
const montant = evaluate(tranche.montant)
|
||||
return {
|
||||
...tranche,
|
||||
montant,
|
||||
nodeValue: montant.nodeValue,
|
||||
unit: montant.unit,
|
||||
missingVariables: mergeAllMissing([montant, tranche]),
|
||||
}
|
||||
})
|
||||
|
||||
let activeTranches
|
||||
const activeTranche = tranches.find((tranche) => tranche.isActive)
|
||||
if (activeTranche) {
|
||||
activeTranches = [activeTranche]
|
||||
} else if (tranches[tranches.length - 1].isAfterActive === false) {
|
||||
activeTranches = [{ nodeValue: false }]
|
||||
} else {
|
||||
activeTranches = tranches.filter((tranche) => tranche.isActive === null)
|
||||
}
|
||||
|
||||
const nodeValue = !activeTranches[0]
|
||||
? false
|
||||
: activeTranches[0].isActive === null
|
||||
? null
|
||||
: activeTranches[0].nodeValue
|
||||
|
||||
return {
|
||||
...node,
|
||||
nodeValue,
|
||||
missingVariables: mergeAllMissing(activeTranches),
|
||||
explanation: {
|
||||
...node.explanation,
|
||||
assiette,
|
||||
multiplicateur,
|
||||
tranches,
|
||||
},
|
||||
unit: activeTranches[0]?.unit ?? undefined,
|
||||
} as any
|
||||
}
|
||||
|
||||
registerEvaluationFunction('grille', evaluate)
|
|
@ -1,176 +0,0 @@
|
|||
import { EvaluationFunction } from '..'
|
||||
import { ConstantNode, Unit } from '../AST/types'
|
||||
import { mergeMissing } from '../evaluation'
|
||||
import { registerEvaluationFunction } from '../evaluationFunctions'
|
||||
import { convertNodeToUnit } from '../nodeUnits'
|
||||
import parse from '../parse'
|
||||
import { Context } from '../parsePublicodes'
|
||||
import { ReferenceNode } from '../reference'
|
||||
import uniroot from '../uniroot'
|
||||
import { parseUnit } from '../units'
|
||||
import { UnitéNode } from './unité'
|
||||
|
||||
export type InversionNode = {
|
||||
explanation: {
|
||||
ruleToInverse: string
|
||||
inversionCandidates: Array<ReferenceNode>
|
||||
unit?: Unit
|
||||
}
|
||||
nodeKind: 'inversion'
|
||||
}
|
||||
|
||||
// The user of the inversion mechanism has to define a list of "inversion
|
||||
// candidates". At runtime, the evaluation function of the mechanism will look
|
||||
// at the situation value of these candidates, and use the first one that is
|
||||
// defined as its "goal" for the inversion
|
||||
//
|
||||
// The game is then to find an input such as the computed value of the "goal" is
|
||||
// equal to its situation value, mathematically we search for the zero of the
|
||||
// function x → f(x) - goal. The iteration logic between each test is
|
||||
// implemented in the `uniroot` file.
|
||||
export const evaluateInversion: EvaluationFunction<'inversion'> = function (
|
||||
node
|
||||
) {
|
||||
const inversionGoal = node.explanation.inversionCandidates.find(
|
||||
(candidate) =>
|
||||
this.parsedSituation[candidate.dottedName as string] != undefined
|
||||
)
|
||||
|
||||
if (inversionGoal === undefined) {
|
||||
const missingVariables = {
|
||||
...Object.fromEntries(
|
||||
node.explanation.inversionCandidates.map((candidate) => [
|
||||
candidate.dottedName,
|
||||
1,
|
||||
])
|
||||
),
|
||||
[node.explanation.ruleToInverse]: 1,
|
||||
}
|
||||
return {
|
||||
...node,
|
||||
missingVariables,
|
||||
nodeValue: null,
|
||||
}
|
||||
}
|
||||
const evaluatedInversionGoal = this.evaluate(inversionGoal)
|
||||
const unit = 'unit' in node ? node.unit : evaluatedInversionGoal.unit
|
||||
const originalCache = this.cache
|
||||
const originalSituation = { ...this.parsedSituation }
|
||||
let inversionNumberOfIterations = 0
|
||||
delete this.parsedSituation[inversionGoal.dottedName as string]
|
||||
const evaluateWithValue = (n: number) => {
|
||||
inversionNumberOfIterations++
|
||||
this.resetCache()
|
||||
this.cache._meta = { ...originalCache._meta }
|
||||
this.parsedSituation[node.explanation.ruleToInverse] = {
|
||||
unit: unit,
|
||||
nodeKind: 'unité',
|
||||
explanation: {
|
||||
nodeKind: 'constant',
|
||||
nodeValue: n,
|
||||
type: 'number',
|
||||
} as ConstantNode,
|
||||
} as UnitéNode
|
||||
|
||||
return convertNodeToUnit(unit, this.evaluate(inversionGoal))
|
||||
}
|
||||
|
||||
const goal = convertNodeToUnit(unit, evaluatedInversionGoal)
|
||||
.nodeValue as number
|
||||
let nodeValue: number | null | undefined = null
|
||||
|
||||
// We do some blind attempts here to avoid using the default minimum and
|
||||
// maximum of +/- 10^8 that are required by the `uniroot` function. For the
|
||||
// first attempt we use the goal value as a very rough first approximation.
|
||||
// For the second attempt we do a proportionality coefficient with the result
|
||||
// from the first try and the goal value. The two attempts are then used in
|
||||
// the following way:
|
||||
// - if both results are `null` we assume that the inversion is impossible
|
||||
// because of missing variables
|
||||
// - otherwise, we calculate the missing variables of the node as the union of
|
||||
// the missings variables of our two attempts
|
||||
// - we cache the result of our two attempts so that `uniroot` doesn't
|
||||
// recompute them
|
||||
const x1 = goal
|
||||
const y1Node = evaluateWithValue(x1)
|
||||
const y1 = y1Node.nodeValue as number
|
||||
const coeff = y1 > goal ? 0.9 : 1.2
|
||||
const x2 = y1 !== null ? (x1 * goal * coeff) / y1 : 2000
|
||||
const y2Node = evaluateWithValue(x2)
|
||||
const y2 = y2Node.nodeValue as number
|
||||
|
||||
const missingVariables = mergeMissing(
|
||||
y1Node.missingVariables,
|
||||
y2Node.missingVariables
|
||||
)
|
||||
|
||||
if (y1 !== null || y2 !== null) {
|
||||
// The `uniroot` function parameter. It will be called with its `min` and
|
||||
// `max` arguments, so we can use our cached nodes if the function is called
|
||||
// with the already computed x1 or x2.
|
||||
const test = (x: number): number => {
|
||||
const y = x === x1 ? y1 : x === x2 ? y2 : evaluateWithValue(x).nodeValue
|
||||
return (y as number) - goal
|
||||
}
|
||||
|
||||
const defaultMin = -1000000
|
||||
const defaultMax = 100000000
|
||||
const nearestBelowGoal =
|
||||
y2 !== null && y2 < goal && (y2 > y1 || y1 > goal)
|
||||
? x2
|
||||
: y1 !== null && y1 < goal && (y1 > y2 || y2 > goal)
|
||||
? x1
|
||||
: defaultMin
|
||||
const nearestAboveGoal =
|
||||
y2 !== null && y2 > goal && (y2 < y1 || y1 < goal)
|
||||
? x2
|
||||
: y1 !== null && y1 > goal && (y1 < y2 || y2 < goal)
|
||||
? x1
|
||||
: defaultMax
|
||||
|
||||
nodeValue = uniroot(test, nearestBelowGoal, nearestAboveGoal, 0.1, 10, 1)
|
||||
}
|
||||
if (nodeValue === undefined) {
|
||||
nodeValue = null
|
||||
originalCache._meta.inversionFail = true
|
||||
}
|
||||
|
||||
// // Uncomment to display the two attempts and their result
|
||||
// console.table([{ x: x1, y: y1 }, { x: x2, y: y2 }])
|
||||
// console.log('iteration inversion:', inversionNumberOfIterations)
|
||||
|
||||
this.cache = originalCache
|
||||
this.parsedSituation = originalSituation
|
||||
|
||||
return {
|
||||
...node,
|
||||
unit,
|
||||
nodeValue,
|
||||
explanation: {
|
||||
...node.explanation,
|
||||
inversionGoal,
|
||||
inversionNumberOfIterations,
|
||||
},
|
||||
missingVariables,
|
||||
}
|
||||
}
|
||||
|
||||
export const mecanismInversion = (v, context: Context) => {
|
||||
if (!v.avec) {
|
||||
throw new Error(
|
||||
"Une formule d'inversion doit préciser _avec_ quoi on peut inverser la variable"
|
||||
)
|
||||
}
|
||||
return {
|
||||
explanation: {
|
||||
ruleToInverse: context.dottedName,
|
||||
inversionCandidates: v.avec.map((node) => parse(node, context)),
|
||||
},
|
||||
...('unité' in v && {
|
||||
unit: parseUnit(v.unité, context.getUnitKey),
|
||||
}),
|
||||
nodeKind: 'inversion',
|
||||
} as InversionNode
|
||||
}
|
||||
|
||||
registerEvaluationFunction('inversion', evaluateInversion)
|
|
@ -1,33 +0,0 @@
|
|||
import { ASTNode } from '../AST/types'
|
||||
import { evaluateArray } from '../evaluation'
|
||||
import { registerEvaluationFunction } from '../evaluationFunctions'
|
||||
import parse from '../parse'
|
||||
|
||||
export type MaxNode = {
|
||||
explanation: Array<ASTNode>
|
||||
nodeKind: 'maximum'
|
||||
}
|
||||
|
||||
export const mecanismMax = (v, context) => {
|
||||
const explanation = v.map((node) => parse(node, context))
|
||||
|
||||
return {
|
||||
explanation,
|
||||
nodeKind: 'maximum',
|
||||
} as MaxNode
|
||||
}
|
||||
|
||||
const max = (a, b) => {
|
||||
if (a === false) {
|
||||
return b
|
||||
}
|
||||
if (b === false) {
|
||||
return a
|
||||
}
|
||||
if (a === null || b === null) {
|
||||
return null
|
||||
}
|
||||
return Math.max(a, b)
|
||||
}
|
||||
const evaluate = evaluateArray<'maximum'>(max, false)
|
||||
registerEvaluationFunction('maximum', evaluate)
|
|
@ -1,33 +0,0 @@
|
|||
import { ASTNode } from '../AST/types'
|
||||
import { evaluateArray } from '../evaluation'
|
||||
import { registerEvaluationFunction } from '../evaluationFunctions'
|
||||
import parse from '../parse'
|
||||
|
||||
export type MinNode = {
|
||||
explanation: Array<ASTNode>
|
||||
nodeKind: 'minimum'
|
||||
}
|
||||
export const mecanismMin = (v, context) => {
|
||||
const explanation = v.map((node) => parse(node, context))
|
||||
|
||||
return {
|
||||
explanation,
|
||||
nodeKind: 'minimum',
|
||||
} as MinNode
|
||||
}
|
||||
|
||||
const min = (a, b) => {
|
||||
if (a === false) {
|
||||
return b
|
||||
}
|
||||
if (b === false) {
|
||||
return a
|
||||
}
|
||||
if (a === null || b === null) {
|
||||
return null
|
||||
}
|
||||
return Math.min(a, b)
|
||||
}
|
||||
const evaluate = evaluateArray<'minimum'>(min, false)
|
||||
|
||||
registerEvaluationFunction('minimum', evaluate)
|
|
@ -1,53 +0,0 @@
|
|||
import { EvaluationFunction } from '..'
|
||||
import { ASTNode, EvaluatedNode } from '../AST/types'
|
||||
import { bonus, mergeMissing } from '../evaluation'
|
||||
import { registerEvaluationFunction } from '../evaluationFunctions'
|
||||
import parse from '../parse'
|
||||
|
||||
export type NonApplicableSiNode = {
|
||||
explanation: {
|
||||
condition: ASTNode
|
||||
valeur: ASTNode
|
||||
}
|
||||
nodeKind: 'non applicable si'
|
||||
}
|
||||
|
||||
const evaluate: EvaluationFunction<'non applicable si'> = function (node) {
|
||||
const condition = this.evaluate(node.explanation.condition)
|
||||
let valeur = node.explanation.valeur
|
||||
if (condition.nodeValue === false || condition.nodeValue === null) {
|
||||
valeur = this.evaluate(valeur)
|
||||
}
|
||||
return {
|
||||
...node,
|
||||
nodeValue:
|
||||
condition.nodeValue === null
|
||||
? null
|
||||
: condition.nodeValue !== false
|
||||
? false
|
||||
: 'nodeValue' in valeur
|
||||
? (valeur as EvaluatedNode).nodeValue
|
||||
: null,
|
||||
explanation: { valeur, condition },
|
||||
missingVariables: mergeMissing(
|
||||
'missingVariables' in valeur ? valeur.missingVariables : {},
|
||||
bonus(condition.missingVariables)
|
||||
),
|
||||
...('unit' in valeur && { unit: valeur.unit }),
|
||||
}
|
||||
}
|
||||
|
||||
export default function parseNonApplicable(v, context) {
|
||||
const explanation = {
|
||||
valeur: parse(v.valeur, context),
|
||||
condition: parse(v[parseNonApplicable.nom], context),
|
||||
}
|
||||
return {
|
||||
explanation,
|
||||
nodeKind: parseNonApplicable.nom,
|
||||
} as NonApplicableSiNode
|
||||
}
|
||||
|
||||
parseNonApplicable.nom = 'non applicable si' as const
|
||||
|
||||
registerEvaluationFunction(parseNonApplicable.nom, evaluate)
|
|
@ -1,30 +0,0 @@
|
|||
import { ASTNode } from '../AST/types'
|
||||
import { registerEvaluationFunction } from '../evaluationFunctions'
|
||||
import parse from '../parse'
|
||||
import { Context } from '../parsePublicodes'
|
||||
|
||||
export type PossibilityNode = {
|
||||
explanation: Array<ASTNode>
|
||||
'choix obligatoire'?: 'oui' | 'non'
|
||||
context: string
|
||||
nodeKind: 'une possibilité'
|
||||
}
|
||||
// TODO : This isn't a real mecanism, cf. #963
|
||||
export const mecanismOnePossibility = (v, context: Context) => {
|
||||
if (Array.isArray(v)) {
|
||||
v = {
|
||||
possibilités: v,
|
||||
}
|
||||
}
|
||||
return {
|
||||
...v,
|
||||
explanation: v.possibilités.map((p) => parse(p, context)),
|
||||
nodeKind: 'une possibilité',
|
||||
context: context.dottedName,
|
||||
} as PossibilityNode
|
||||
}
|
||||
registerEvaluationFunction<'une possibilité'>('une possibilité', (node) => ({
|
||||
...node,
|
||||
nodeValue: null,
|
||||
missingVariables: { [node.context]: 1 },
|
||||
}))
|
|
@ -1,131 +0,0 @@
|
|||
import { EvaluationFunction } from '..'
|
||||
import { ASTNode, EvaluatedNode } from '../AST/types'
|
||||
import { convertToDate } from '../date'
|
||||
import { warning } from '../error'
|
||||
import { mergeAllMissing } from '../evaluation'
|
||||
import { registerEvaluationFunction } from '../evaluationFunctions'
|
||||
import { convertNodeToUnit } from '../nodeUnits'
|
||||
import parse from '../parse'
|
||||
import { inferUnit, serializeUnit } from '../units'
|
||||
|
||||
const knownOperations = {
|
||||
'*': [(a, b) => a * b, '×'],
|
||||
'/': [(a, b) => a / b, '∕'],
|
||||
'+': [(a, b) => a + b],
|
||||
'-': [(a, b) => a - b, '−'],
|
||||
'<': [(a, b) => a < b],
|
||||
'<=': [(a, b) => a <= b, '≤'],
|
||||
'>': [(a, b) => a > b],
|
||||
'>=': [(a, b) => a >= b, '≥'],
|
||||
'=': [(a, b) => a === b],
|
||||
'!=': [(a, b) => a !== b, '≠'],
|
||||
} as const
|
||||
|
||||
export type OperationNode = {
|
||||
nodeKind: 'operation'
|
||||
explanation: [ASTNode, ASTNode]
|
||||
operationKind: keyof typeof knownOperations
|
||||
operator: string
|
||||
}
|
||||
|
||||
const parseOperation = (k, symbol) => (v, context) => {
|
||||
const explanation = v.explanation.map((node) => parse(node, context))
|
||||
|
||||
return {
|
||||
...v,
|
||||
nodeKind: 'operation',
|
||||
operationKind: k,
|
||||
operator: symbol || k,
|
||||
explanation,
|
||||
} as OperationNode
|
||||
}
|
||||
|
||||
const evaluate: EvaluationFunction<'operation'> = function (node) {
|
||||
// When we only need to evaluate the applicability of a rule, we don't enter
|
||||
// inside “sum terms” since we know that the sum will always be applicable.
|
||||
// However, if somewhere in the evaluation stack we do a comparison, we need
|
||||
// to disable this optimization since in this case we'll need the exact value
|
||||
// of sums in the evaluation subtree.
|
||||
const disableApplicabilityContext = ['≠', '=', '<', '>', '≤', '≥'].includes(
|
||||
node.operator
|
||||
)
|
||||
if (disableApplicabilityContext && this.inApplicabilityEvaluationContext) {
|
||||
this.cache._meta.disableApplicabilityContextCounter += 1
|
||||
}
|
||||
const explanation = node.explanation.map((node) => this.evaluate(node)) as [
|
||||
EvaluatedNode,
|
||||
EvaluatedNode
|
||||
]
|
||||
if (disableApplicabilityContext && this.inApplicabilityEvaluationContext) {
|
||||
this.cache._meta.disableApplicabilityContextCounter -= 1
|
||||
}
|
||||
let [node1, node2] = explanation
|
||||
const missingVariables = mergeAllMissing([node1, node2])
|
||||
|
||||
if (node1.nodeValue == null || node2.nodeValue == null) {
|
||||
return { ...node, nodeValue: null, explanation, missingVariables }
|
||||
}
|
||||
if (!['∕', '×'].includes(node.operator)) {
|
||||
try {
|
||||
if (node1.unit && 'unit' in node2) {
|
||||
node2 = convertNodeToUnit(node1.unit, node2)
|
||||
} else if (node2.unit) {
|
||||
node1 = convertNodeToUnit(node2.unit, node1)
|
||||
}
|
||||
} catch (e) {
|
||||
warning(
|
||||
this.options.logger,
|
||||
`Dans l'expression '${
|
||||
node.operator
|
||||
}', la partie gauche (unité: ${serializeUnit(
|
||||
node1.unit
|
||||
)}) n'est pas compatible avec la partie droite (unité: ${serializeUnit(
|
||||
node2.unit
|
||||
)})`,
|
||||
e
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const operatorFunction = knownOperations[node.operationKind][0]
|
||||
|
||||
const a = node1.nodeValue as string | false
|
||||
const b = node2.nodeValue as string | false
|
||||
|
||||
const nodeValue =
|
||||
!['≠', '='].includes(node.operator) && a === false && b === false
|
||||
? false
|
||||
: ['<', '>', '≤', '≥', '∕', '×'].includes(node.operator) &&
|
||||
(a === false || b === false)
|
||||
? false
|
||||
: a !== false &&
|
||||
b !== false &&
|
||||
['≠', '=', '<', '>', '≤', '≥'].includes(node.operator) &&
|
||||
[a, b].every((value) => value.match?.(/[\d]{2}\/[\d]{2}\/[\d]{4}/))
|
||||
? operatorFunction(convertToDate(a), convertToDate(b))
|
||||
: operatorFunction(a, b)
|
||||
|
||||
return {
|
||||
...node,
|
||||
explanation,
|
||||
...((node.operationKind === '*' ||
|
||||
node.operationKind === '/' ||
|
||||
node.operationKind === '-' ||
|
||||
node.operationKind === '+') && {
|
||||
unit: inferUnit(node.operationKind, [node1.unit, node2.unit]),
|
||||
}),
|
||||
missingVariables,
|
||||
nodeValue,
|
||||
}
|
||||
}
|
||||
|
||||
registerEvaluationFunction('operation', evaluate)
|
||||
|
||||
const operationDispatch = Object.fromEntries(
|
||||
Object.entries(knownOperations).map(([k, [f, symbol]]) => [
|
||||
k,
|
||||
parseOperation(k, symbol),
|
||||
])
|
||||
)
|
||||
|
||||
export default operationDispatch
|
|
@ -1,55 +0,0 @@
|
|||
import { EvaluationFunction } from '..'
|
||||
import { ASTNode } from '../AST/types'
|
||||
import { bonus, mergeMissing } from '../evaluation'
|
||||
import { registerEvaluationFunction } from '../evaluationFunctions'
|
||||
import parse from '../parse'
|
||||
import { EvaluatedNode } from '../AST/types'
|
||||
|
||||
export type ParDéfautNode = {
|
||||
explanation: {
|
||||
valeur: ASTNode
|
||||
parDéfaut: ASTNode
|
||||
}
|
||||
nodeKind: 'par défaut'
|
||||
}
|
||||
|
||||
const evaluate: EvaluationFunction<'par défaut'> = function (node) {
|
||||
const explanation: {
|
||||
parDéfaut: EvaluatedNode | ASTNode
|
||||
valeur: EvaluatedNode | ASTNode
|
||||
} = { ...node.explanation }
|
||||
let valeur = this.evaluate(explanation.valeur)
|
||||
explanation.valeur = valeur
|
||||
if (valeur.nodeValue === null) {
|
||||
valeur = this.evaluate(explanation.parDéfaut)
|
||||
explanation.parDéfaut = valeur
|
||||
}
|
||||
|
||||
return {
|
||||
...node,
|
||||
nodeValue: valeur.nodeValue,
|
||||
explanation,
|
||||
missingVariables: mergeMissing(
|
||||
bonus((explanation.valeur as EvaluatedNode).missingVariables),
|
||||
'missingVariables' in explanation.parDéfaut
|
||||
? explanation.parDéfaut.missingVariables
|
||||
: {}
|
||||
),
|
||||
...('unit' in valeur && { unit: valeur.unit }),
|
||||
}
|
||||
}
|
||||
|
||||
export default function parseParDéfaut(v, context) {
|
||||
const explanation = {
|
||||
valeur: parse(v.valeur, context),
|
||||
parDéfaut: parse(v['par défaut'], context),
|
||||
}
|
||||
return {
|
||||
explanation,
|
||||
nodeKind: parseParDéfaut.nom,
|
||||
} as ParDéfautNode
|
||||
}
|
||||
|
||||
parseParDéfaut.nom = 'par défaut' as const
|
||||
|
||||
registerEvaluationFunction(parseParDéfaut.nom, evaluate)
|
|
@ -1,69 +0,0 @@
|
|||
import { EvaluationFunction } from '..'
|
||||
import { ASTNode } from '../AST/types'
|
||||
import { warning } from '../error'
|
||||
import { mergeAllMissing } from '../evaluation'
|
||||
import { registerEvaluationFunction } from '../evaluationFunctions'
|
||||
import { convertNodeToUnit } from '../nodeUnits'
|
||||
import parse from '../parse'
|
||||
|
||||
export type PlafondNode = {
|
||||
explanation: {
|
||||
plafond: ASTNode
|
||||
valeur: ASTNode
|
||||
}
|
||||
nodeKind: 'plafond'
|
||||
}
|
||||
|
||||
const evaluate: EvaluationFunction<'plafond'> = function (node) {
|
||||
const valeur = this.evaluate(node.explanation.valeur)
|
||||
|
||||
let nodeValue = valeur.nodeValue
|
||||
let plafond = node.explanation.plafond
|
||||
if (nodeValue !== false) {
|
||||
const evaluatedPlafond = this.evaluate(plafond)
|
||||
if (valeur.unit) {
|
||||
try {
|
||||
plafond = convertNodeToUnit(valeur.unit, evaluatedPlafond)
|
||||
} catch (e) {
|
||||
warning(
|
||||
this.options.logger,
|
||||
this.cache._meta.evaluationRuleStack[0],
|
||||
"L'unité du plafond n'est pas compatible avec celle de la valeur à encadrer",
|
||||
e
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
typeof nodeValue === 'number' &&
|
||||
'nodeValue' in plafond &&
|
||||
typeof plafond.nodeValue === 'number' &&
|
||||
nodeValue > plafond.nodeValue
|
||||
) {
|
||||
nodeValue = plafond.nodeValue
|
||||
;(plafond as any).isActive = true
|
||||
}
|
||||
return {
|
||||
...node,
|
||||
nodeValue,
|
||||
...('unit' in valeur && { unit: valeur.unit }),
|
||||
explanation: { valeur, plafond },
|
||||
missingVariables: mergeAllMissing([valeur, plafond]),
|
||||
}
|
||||
}
|
||||
|
||||
export default function parsePlafond(v, context) {
|
||||
const explanation = {
|
||||
valeur: parse(v.valeur, context),
|
||||
plafond: parse(v.plafond, context),
|
||||
}
|
||||
return {
|
||||
explanation,
|
||||
nodeKind: 'plafond',
|
||||
} as PlafondNode
|
||||
}
|
||||
|
||||
parsePlafond.nom = 'plafond'
|
||||
|
||||
registerEvaluationFunction('plafond', evaluate)
|
|
@ -1,67 +0,0 @@
|
|||
import { EvaluationFunction } from '..'
|
||||
import { ASTNode } from '../AST/types'
|
||||
import { warning } from '../error'
|
||||
import { mergeAllMissing } from '../evaluation'
|
||||
import { registerEvaluationFunction } from '../evaluationFunctions'
|
||||
import { convertNodeToUnit } from '../nodeUnits'
|
||||
import parse from '../parse'
|
||||
|
||||
export type PlancherNode = {
|
||||
explanation: {
|
||||
plancher: ASTNode
|
||||
valeur: ASTNode
|
||||
}
|
||||
nodeKind: 'plancher'
|
||||
}
|
||||
|
||||
const evaluate: EvaluationFunction<'plancher'> = function (node) {
|
||||
const valeur = this.evaluate(node.explanation.valeur)
|
||||
let nodeValue = valeur.nodeValue
|
||||
let plancher = node.explanation.plancher
|
||||
if (nodeValue !== false) {
|
||||
const evaluatedPlancher = this.evaluate(plancher)
|
||||
if (valeur.unit) {
|
||||
try {
|
||||
plancher = convertNodeToUnit(valeur.unit, evaluatedPlancher)
|
||||
} catch (e) {
|
||||
warning(
|
||||
this.options.logger,
|
||||
this.cache._meta.evaluationRuleStack[0],
|
||||
"L'unité du plancher n'est pas compatible avec celle de la valeur à encadrer",
|
||||
e
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
if (
|
||||
typeof nodeValue === 'number' &&
|
||||
'nodeValue' in plancher &&
|
||||
typeof plancher.nodeValue === 'number' &&
|
||||
nodeValue < plancher.nodeValue
|
||||
) {
|
||||
nodeValue = plancher.nodeValue
|
||||
;(plancher as any).isActive = true
|
||||
}
|
||||
return {
|
||||
...node,
|
||||
nodeValue,
|
||||
...('unit' in valeur && { unit: valeur.unit }),
|
||||
explanation: { valeur, plancher },
|
||||
missingVariables: mergeAllMissing([valeur, plancher]),
|
||||
}
|
||||
}
|
||||
|
||||
export default function Plancher(v, context) {
|
||||
const explanation = {
|
||||
valeur: parse(v.valeur, context),
|
||||
plancher: parse(v.plancher, context),
|
||||
}
|
||||
return {
|
||||
explanation,
|
||||
nodeKind: 'plancher',
|
||||
} as PlancherNode
|
||||
}
|
||||
|
||||
Plancher.nom = 'plancher'
|
||||
|
||||
registerEvaluationFunction('plancher', evaluate)
|
|
@ -1,91 +0,0 @@
|
|||
import { EvaluationFunction } from '..'
|
||||
import { ASTNode } from '../AST/types'
|
||||
import { warning } from '../error'
|
||||
import { defaultNode, mergeAllMissing, parseObject } from '../evaluation'
|
||||
import { registerEvaluationFunction } from '../evaluationFunctions'
|
||||
import { convertNodeToUnit, simplifyNodeUnit } from '../nodeUnits'
|
||||
import { areUnitConvertible, convertUnit, inferUnit } from '../units'
|
||||
|
||||
export type ProductNode = {
|
||||
explanation: {
|
||||
assiette: ASTNode
|
||||
facteur: ASTNode
|
||||
plafond: ASTNode
|
||||
taux: ASTNode
|
||||
}
|
||||
nodeKind: 'produit'
|
||||
}
|
||||
|
||||
const objectShape = {
|
||||
assiette: false,
|
||||
taux: defaultNode(1),
|
||||
facteur: defaultNode(1),
|
||||
plafond: defaultNode(Infinity),
|
||||
}
|
||||
|
||||
export const mecanismProduct = (v, context) => {
|
||||
const explanation = parseObject(objectShape, v, context)
|
||||
|
||||
return {
|
||||
explanation,
|
||||
nodeKind: 'produit',
|
||||
} as ProductNode
|
||||
}
|
||||
|
||||
const evaluateProduit: EvaluationFunction<'produit'> = function (node) {
|
||||
const assiette = this.evaluate(node.explanation.assiette)
|
||||
const taux = this.evaluate(node.explanation.taux)
|
||||
const facteur = this.evaluate(node.explanation.facteur)
|
||||
let plafond = this.evaluate(node.explanation.plafond)
|
||||
|
||||
if (assiette.unit) {
|
||||
try {
|
||||
plafond = convertNodeToUnit(assiette.unit, plafond)
|
||||
} catch (e) {
|
||||
warning(
|
||||
this.options.logger,
|
||||
this.cache._meta.evaluationRuleStack[0],
|
||||
"Impossible de convertir l'unité du plafond du produit dans celle de l'assiette",
|
||||
e
|
||||
)
|
||||
}
|
||||
}
|
||||
const mult = (base, rate, facteur, plafond) =>
|
||||
Math.min(base, plafond === false ? Infinity : plafond) * rate * facteur
|
||||
let nodeValue = [taux, assiette, facteur].some((n) => n.nodeValue === false)
|
||||
? false
|
||||
: [taux, assiette, facteur].some((n) => n.nodeValue === 0)
|
||||
? 0
|
||||
: [taux, assiette, facteur].some((n) => n.nodeValue === null)
|
||||
? null
|
||||
: mult(
|
||||
assiette.nodeValue,
|
||||
taux.nodeValue,
|
||||
facteur.nodeValue,
|
||||
plafond.nodeValue
|
||||
)
|
||||
let unit = inferUnit(
|
||||
'*',
|
||||
[assiette, taux, facteur].map((el) => el.unit)
|
||||
)
|
||||
if (areUnitConvertible(unit, assiette.unit)) {
|
||||
nodeValue = convertUnit(unit, assiette.unit, nodeValue)
|
||||
unit = assiette.unit
|
||||
}
|
||||
|
||||
return simplifyNodeUnit({
|
||||
...node,
|
||||
missingVariables: mergeAllMissing([assiette, taux, facteur, plafond]),
|
||||
nodeValue,
|
||||
unit,
|
||||
explanation: {
|
||||
assiette,
|
||||
taux,
|
||||
facteur,
|
||||
plafond,
|
||||
plafondActif: (assiette.nodeValue as any) > (plafond as any).nodeValue,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
registerEvaluationFunction('produit', evaluateProduit)
|
|
@ -1,86 +0,0 @@
|
|||
import Engine, { EvaluationFunction } from '..'
|
||||
import { ASTNode, EvaluatedNode } from '../AST/types'
|
||||
import { defaultNode } from '../evaluation'
|
||||
import { registerEvaluationFunction } from '../evaluationFunctions'
|
||||
import parse from '../parse'
|
||||
import { ReferenceNode } from '../reference'
|
||||
import { disambiguateRuleReference } from '../ruleUtils'
|
||||
import { serializeUnit } from '../units'
|
||||
|
||||
export type RecalculNode = {
|
||||
explanation: {
|
||||
recalcul: ASTNode
|
||||
amendedSituation: Array<[ReferenceNode, ASTNode]>
|
||||
parsedSituation?: Engine['parsedSituation']
|
||||
subEngineId: number
|
||||
}
|
||||
nodeKind: 'recalcul'
|
||||
}
|
||||
|
||||
const evaluateRecalcul: EvaluationFunction<'recalcul'> = function (node) {
|
||||
if (this.cache._meta.inRecalcul) {
|
||||
return defaultNode(null) as any as RecalculNode & EvaluatedNode
|
||||
}
|
||||
|
||||
const amendedSituation = node.explanation.amendedSituation
|
||||
.map(([originRule, replacement]) => [
|
||||
this.evaluate(originRule),
|
||||
this.evaluate(replacement),
|
||||
])
|
||||
.filter(
|
||||
([originRule, replacement]) =>
|
||||
originRule.nodeValue !== replacement.nodeValue ||
|
||||
serializeUnit(originRule.unit) !== serializeUnit(replacement.unit)
|
||||
) as Array<[ReferenceNode & EvaluatedNode, EvaluatedNode]>
|
||||
|
||||
const engine = amendedSituation.length
|
||||
? this.shallowCopy().setSituation({
|
||||
...this.parsedSituation,
|
||||
...Object.fromEntries(
|
||||
amendedSituation.map(([reference, replacement]) => [
|
||||
disambiguateRuleReference(
|
||||
this.parsedRules,
|
||||
reference.contextDottedName,
|
||||
reference.name
|
||||
),
|
||||
replacement,
|
||||
]) as any
|
||||
),
|
||||
})
|
||||
: this
|
||||
|
||||
engine.cache._meta.inRecalcul = true
|
||||
const evaluatedNode = engine.evaluate(node.explanation.recalcul)
|
||||
engine.cache._meta.inRecalcul = false
|
||||
|
||||
return {
|
||||
...node,
|
||||
nodeValue: evaluatedNode.nodeValue,
|
||||
explanation: {
|
||||
recalcul: evaluatedNode,
|
||||
amendedSituation,
|
||||
parsedSituation: engine.parsedSituation,
|
||||
subEngineId: engine.subEngineId as number,
|
||||
},
|
||||
missingVariables: evaluatedNode.missingVariables,
|
||||
...('unit' in evaluatedNode && { unit: evaluatedNode.unit }),
|
||||
}
|
||||
}
|
||||
|
||||
export const mecanismRecalcul = (v, context) => {
|
||||
const amendedSituation = Object.keys(v.avec).map((dottedName) => [
|
||||
parse(dottedName, context),
|
||||
parse(v.avec[dottedName], context),
|
||||
])
|
||||
const defaultRuleToEvaluate = context.dottedName
|
||||
const nodeToEvaluate = parse(v.règle ?? defaultRuleToEvaluate, context)
|
||||
return {
|
||||
explanation: {
|
||||
recalcul: nodeToEvaluate,
|
||||
amendedSituation,
|
||||
},
|
||||
nodeKind: 'recalcul',
|
||||
} as RecalculNode
|
||||
}
|
||||
|
||||
registerEvaluationFunction('recalcul', evaluateRecalcul)
|
|
@ -1,109 +0,0 @@
|
|||
import { EvaluationFunction } from '..'
|
||||
import { ASTNode, ConstantNode, Unit } from '../AST/types'
|
||||
import { registerEvaluationFunction } from '../evaluationFunctions'
|
||||
import parse from '../parse'
|
||||
import { Context } from '../parsePublicodes'
|
||||
import uniroot from '../uniroot'
|
||||
import { UnitéNode } from './unité'
|
||||
|
||||
export type RésoudreRéférenceCirculaireNode = {
|
||||
explanation: {
|
||||
ruleToSolve: string
|
||||
valeur: ASTNode
|
||||
}
|
||||
nodeKind: 'résoudre référence circulaire'
|
||||
}
|
||||
|
||||
export const evaluateRésoudreRéférenceCirculaire: EvaluationFunction<'résoudre référence circulaire'> =
|
||||
function (node) {
|
||||
const originalCache = this.cache
|
||||
let inversionNumberOfIterations = 0
|
||||
|
||||
const evaluateWithValue = (
|
||||
n: number,
|
||||
unit: Unit = { numerators: [], denominators: [] }
|
||||
) => {
|
||||
inversionNumberOfIterations++
|
||||
this.resetCache()
|
||||
|
||||
this.parsedSituation[node.explanation.ruleToSolve] = {
|
||||
unit: unit,
|
||||
nodeKind: 'unité',
|
||||
explanation: {
|
||||
nodeKind: 'constant',
|
||||
nodeValue: n,
|
||||
type: 'number',
|
||||
} as ConstantNode,
|
||||
} as UnitéNode
|
||||
return this.evaluate(node.explanation.valeur)
|
||||
}
|
||||
|
||||
let nodeValue: number | null | undefined = null
|
||||
|
||||
const x0 = 0
|
||||
let valeur = evaluateWithValue(x0)
|
||||
|
||||
const y0 = valeur.nodeValue as number
|
||||
const unit = valeur.unit
|
||||
const missingVariables = valeur.missingVariables
|
||||
let i = 0
|
||||
if (y0 !== null) {
|
||||
// The `uniroot` function parameter. It will be called with its `min` and
|
||||
// `max` arguments, so we can use our cached nodes if the function is called
|
||||
// with the already computed x1 or x2.
|
||||
const test = (x: number): number => {
|
||||
if (x === x0) {
|
||||
return y0 - x0
|
||||
}
|
||||
valeur = evaluateWithValue(x, unit)
|
||||
const y = valeur.nodeValue
|
||||
i++
|
||||
return (y as number) - x
|
||||
}
|
||||
|
||||
const defaultMin = -1_000_000
|
||||
const defaultMax = 100_000_000
|
||||
|
||||
nodeValue = uniroot(test, defaultMin, defaultMax, 0.5, 30, 2)
|
||||
}
|
||||
|
||||
this.cache = originalCache
|
||||
|
||||
if (nodeValue === undefined) {
|
||||
nodeValue = null
|
||||
this.cache._meta.inversionFail = true
|
||||
}
|
||||
if (nodeValue !== null) {
|
||||
valeur = evaluateWithValue(nodeValue, unit)
|
||||
}
|
||||
delete this.parsedSituation[node.explanation.ruleToSolve]
|
||||
|
||||
return {
|
||||
...node,
|
||||
unit,
|
||||
nodeValue,
|
||||
explanation: {
|
||||
...node.explanation,
|
||||
valeur,
|
||||
inversionNumberOfIterations,
|
||||
},
|
||||
missingVariables,
|
||||
}
|
||||
}
|
||||
|
||||
export default function parseRésoudreRéférenceCirculaire(v, context: Context) {
|
||||
return {
|
||||
explanation: {
|
||||
ruleToSolve: context.dottedName,
|
||||
valeur: parse(v.valeur, context),
|
||||
},
|
||||
nodeKind: 'résoudre référence circulaire',
|
||||
} as RésoudreRéférenceCirculaireNode
|
||||
}
|
||||
|
||||
parseRésoudreRéférenceCirculaire.nom = 'résoudre la référence circulaire'
|
||||
|
||||
registerEvaluationFunction(
|
||||
'résoudre référence circulaire',
|
||||
evaluateRésoudreRéférenceCirculaire
|
||||
)
|
|
@ -1,58 +0,0 @@
|
|||
import { ASTNode, EvaluatedNode } from '../AST/types'
|
||||
import { mergeAllMissing } from '../evaluation'
|
||||
import { registerEvaluationFunction } from '../evaluationFunctions'
|
||||
import parse from '../parse'
|
||||
|
||||
export type SituationNode = {
|
||||
explanation: {
|
||||
situationKey: string
|
||||
valeur: ASTNode
|
||||
situationValeur?: ASTNode
|
||||
}
|
||||
nodeKind: 'nom dans la situation'
|
||||
}
|
||||
export default function parseSituation(v, context) {
|
||||
const explanation = {
|
||||
situationKey: v[parseSituation.nom],
|
||||
valeur: parse(v.valeur, context),
|
||||
}
|
||||
return {
|
||||
nodeKind: parseSituation.nom,
|
||||
explanation,
|
||||
} as SituationNode
|
||||
}
|
||||
|
||||
parseSituation.nom = 'nom dans la situation' as const
|
||||
|
||||
registerEvaluationFunction(parseSituation.nom, function evaluate(node) {
|
||||
const explanation = { ...node.explanation }
|
||||
const situationKey = explanation.situationKey
|
||||
let valeur: EvaluatedNode
|
||||
if (situationKey in this.parsedSituation) {
|
||||
valeur = this.evaluate(this.parsedSituation[situationKey])
|
||||
explanation.situationValeur = valeur
|
||||
} else {
|
||||
valeur = this.evaluate(explanation.valeur)
|
||||
explanation.valeur = valeur
|
||||
delete explanation.situationValeur
|
||||
}
|
||||
|
||||
const unit =
|
||||
valeur.unit ??
|
||||
('unit' in explanation.valeur ? explanation.valeur.unit : undefined)
|
||||
const missingVariables = mergeAllMissing(
|
||||
[explanation.situationValeur, explanation.valeur].filter(
|
||||
Boolean
|
||||
) as Array<EvaluatedNode>
|
||||
)
|
||||
return {
|
||||
...node,
|
||||
nodeValue: valeur.nodeValue,
|
||||
missingVariables:
|
||||
Object.keys(missingVariables).length === 0 && valeur.nodeValue === null
|
||||
? { [situationKey]: 1 }
|
||||
: missingVariables,
|
||||
...(unit !== undefined && { unit }),
|
||||
explanation,
|
||||
}
|
||||
})
|
|
@ -1,34 +0,0 @@
|
|||
import { ASTNode } from '../AST/types'
|
||||
import { evaluateArray } from '../evaluation'
|
||||
import { registerEvaluationFunction } from '../evaluationFunctions'
|
||||
import parse from '../parse'
|
||||
|
||||
const evaluate = evaluateArray<'somme'>((x: any, y: any) => x + y, 0)
|
||||
|
||||
export type SommeNode = {
|
||||
explanation: Array<ASTNode>
|
||||
nodeKind: 'somme'
|
||||
}
|
||||
|
||||
export const mecanismSum = (v, context) => {
|
||||
const explanation = v.map((node) => parse(node, context))
|
||||
return {
|
||||
explanation,
|
||||
nodeKind: 'somme',
|
||||
} as SommeNode
|
||||
}
|
||||
|
||||
registerEvaluationFunction('somme', function (node) {
|
||||
if (this.inApplicabilityEvaluationContext) {
|
||||
return {
|
||||
// With a clearer distinction between `getApplicability` and
|
||||
// `getValue` we could avoid faking a `nodeValue: true` and instead
|
||||
// simply return `isApplicable: true, nodeValue: undefined`
|
||||
nodeValue: true,
|
||||
nodeKind: 'somme',
|
||||
missingVariables: {},
|
||||
explanation: [],
|
||||
}
|
||||
}
|
||||
return evaluate.call(this, node)
|
||||
})
|
|
@ -1,37 +0,0 @@
|
|||
import { EvaluationFunction } from '..'
|
||||
import { ASTNode } from '../AST/types'
|
||||
import { registerEvaluationFunction } from '../evaluationFunctions'
|
||||
import parse from '../parse'
|
||||
|
||||
export type SynchronisationNode = {
|
||||
explanation: {
|
||||
chemin: string
|
||||
data: ASTNode
|
||||
}
|
||||
nodeKind: 'synchronisation'
|
||||
}
|
||||
|
||||
const evaluate: EvaluationFunction<'synchronisation'> = function (node: any) {
|
||||
const data = this.evaluate(node.explanation.data)
|
||||
const valuePath = node.explanation.chemin.split(' . ')
|
||||
const path = (obj) => valuePath.reduce((res, prop) => res?.[prop], obj)
|
||||
const nodeValue = path(data.nodeValue) ?? null
|
||||
|
||||
const missingVariables = {
|
||||
...data.missingVariables,
|
||||
...(data.nodeValue === null ? { [data.dottedName]: 1 } : {}),
|
||||
}
|
||||
|
||||
const explanation = { ...node.explanation, data }
|
||||
return { ...node, nodeValue, explanation, missingVariables }
|
||||
}
|
||||
|
||||
export const mecanismSynchronisation = (v, context) => {
|
||||
return {
|
||||
// TODO : expect API exists ?
|
||||
explanation: { ...v, data: parse(v.data, context) },
|
||||
nodeKind: 'synchronisation',
|
||||
} as SynchronisationNode
|
||||
}
|
||||
|
||||
registerEvaluationFunction('synchronisation', evaluate)
|
|
@ -1,129 +0,0 @@
|
|||
import { EvaluationFunction } from '..'
|
||||
import { defaultNode, mergeAllMissing } from '../evaluation'
|
||||
import { registerEvaluationFunction } from '../evaluationFunctions'
|
||||
import parse from '../parse'
|
||||
|
||||
import { convertNodeToUnit } from '../nodeUnits'
|
||||
import { parseUnit } from '../units'
|
||||
import {
|
||||
evaluatePlafondUntilActiveTranche,
|
||||
parseTranches,
|
||||
TrancheNodes,
|
||||
} from './trancheUtils'
|
||||
import { ASTNode } from '../AST/types'
|
||||
export type TauxProgressifNode = {
|
||||
explanation: {
|
||||
tranches: TrancheNodes
|
||||
multiplicateur: ASTNode
|
||||
assiette: ASTNode
|
||||
}
|
||||
nodeKind: 'taux progressif'
|
||||
}
|
||||
export default function parseTauxProgressif(v, context): TauxProgressifNode {
|
||||
const explanation = {
|
||||
assiette: parse(v.assiette, context),
|
||||
multiplicateur: v.multiplicateur
|
||||
? parse(v.multiplicateur, context)
|
||||
: defaultNode(1),
|
||||
tranches: parseTranches(v.tranches, context),
|
||||
} as TauxProgressifNode['explanation']
|
||||
return {
|
||||
explanation,
|
||||
nodeKind: 'taux progressif',
|
||||
}
|
||||
}
|
||||
|
||||
const evaluate: EvaluationFunction<'taux progressif'> = function (node) {
|
||||
const evaluate = this.evaluate.bind(this)
|
||||
const assiette = this.evaluate(node.explanation.assiette)
|
||||
const multiplicateur = this.evaluate(node.explanation.multiplicateur)
|
||||
const tranches = evaluatePlafondUntilActiveTranche.call(this, {
|
||||
parsedTranches: node.explanation.tranches,
|
||||
assiette,
|
||||
multiplicateur,
|
||||
})
|
||||
|
||||
const evaluatedNode = {
|
||||
...node,
|
||||
explanation: {
|
||||
tranches,
|
||||
assiette,
|
||||
multiplicateur,
|
||||
},
|
||||
unit: parseUnit('%'),
|
||||
}
|
||||
|
||||
const lastTranche = tranches[tranches.length - 1]
|
||||
if (
|
||||
tranches.every(({ isActive }) => isActive === false) ||
|
||||
(lastTranche.isActive && lastTranche.plafond.nodeValue === Infinity)
|
||||
) {
|
||||
const taux = convertNodeToUnit(parseUnit('%'), evaluate(lastTranche.taux))
|
||||
const { nodeValue, missingVariables } = taux
|
||||
lastTranche.taux = taux
|
||||
lastTranche.nodeValue = nodeValue
|
||||
lastTranche.missingVariables = missingVariables
|
||||
return {
|
||||
...evaluatedNode,
|
||||
nodeValue,
|
||||
missingVariables,
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
tranches.every(({ isActive }) => isActive !== true) ||
|
||||
typeof assiette.nodeValue !== 'number'
|
||||
) {
|
||||
return {
|
||||
...evaluatedNode,
|
||||
nodeValue: null,
|
||||
missingVariables: mergeAllMissing(tranches),
|
||||
}
|
||||
}
|
||||
|
||||
const activeTrancheIndex = tranches.findIndex(
|
||||
({ isActive }) => isActive === true
|
||||
)
|
||||
const activeTranche = tranches[activeTrancheIndex]
|
||||
activeTranche.taux = convertNodeToUnit(
|
||||
parseUnit('%'),
|
||||
evaluate(activeTranche.taux)
|
||||
)
|
||||
|
||||
const previousTranche = tranches[activeTrancheIndex - 1]
|
||||
if (previousTranche) {
|
||||
previousTranche.taux = convertNodeToUnit(
|
||||
parseUnit('%'),
|
||||
evaluate(previousTranche.taux)
|
||||
)
|
||||
previousTranche.isActive = true
|
||||
}
|
||||
const previousTaux = previousTranche
|
||||
? previousTranche.taux
|
||||
: activeTranche.taux
|
||||
const calculationValues = [previousTaux, activeTranche.taux, activeTranche]
|
||||
if (calculationValues.some((n) => n.nodeValue === null)) {
|
||||
activeTranche.nodeValue = null
|
||||
activeTranche.missingVariables = mergeAllMissing(calculationValues)
|
||||
return {
|
||||
...evaluatedNode,
|
||||
nodeValue: null,
|
||||
missingVariables: activeTranche.missingVariables,
|
||||
}
|
||||
}
|
||||
|
||||
const lowerTaux = previousTaux.nodeValue
|
||||
const upperTaux = activeTranche.taux.nodeValue
|
||||
const plancher = activeTranche.plancherValue
|
||||
const plafond = activeTranche.plafondValue
|
||||
const coeff = (upperTaux - lowerTaux) / (plafond - plancher)
|
||||
const nodeValue = lowerTaux + (assiette.nodeValue - plancher) * coeff
|
||||
activeTranche.nodeValue = nodeValue
|
||||
return {
|
||||
...evaluatedNode,
|
||||
nodeValue,
|
||||
missingVariables: {},
|
||||
}
|
||||
}
|
||||
|
||||
registerEvaluationFunction('taux progressif', evaluate)
|
|
@ -1,128 +0,0 @@
|
|||
import Engine from '..'
|
||||
import { ASTNode, Evaluation } from '../AST/types'
|
||||
import { evaluationError, warning } from '../error'
|
||||
import { mergeAllMissing } from '../evaluation'
|
||||
import parse from '../parse'
|
||||
import { convertUnit, inferUnit } from '../units'
|
||||
|
||||
type TrancheNode = { taux: ASTNode } | { montant: ASTNode }
|
||||
export type TrancheNodes = Array<TrancheNode & { plafond?: ASTNode }>
|
||||
|
||||
export const parseTranches = (tranches, context): TrancheNodes => {
|
||||
return tranches
|
||||
.map((t, i) => {
|
||||
if (!t.plafond && i > tranches.length) {
|
||||
throw new SyntaxError(
|
||||
`La tranche n°${i} du barème n'a pas de plafond précisé. Seule la dernière tranche peut ne pas être plafonnée`
|
||||
)
|
||||
}
|
||||
return { ...t, plafond: t.plafond ?? Infinity }
|
||||
})
|
||||
.map((node) => ({
|
||||
...node,
|
||||
...(node.taux !== undefined ? { taux: parse(node.taux, context) } : {}),
|
||||
...(node.montant !== undefined
|
||||
? { montant: parse(node.montant, context) }
|
||||
: {}),
|
||||
plafond: parse(node.plafond, context),
|
||||
}))
|
||||
}
|
||||
|
||||
export function evaluatePlafondUntilActiveTranche(
|
||||
this: Engine,
|
||||
{ multiplicateur, assiette, parsedTranches }
|
||||
) {
|
||||
return parsedTranches.reduce(
|
||||
([tranches, activeTrancheFound], parsedTranche, i: number) => {
|
||||
if (activeTrancheFound) {
|
||||
return [
|
||||
[...tranches, { ...parsedTranche, isAfterActive: true }],
|
||||
activeTrancheFound,
|
||||
]
|
||||
}
|
||||
|
||||
const plafond = this.evaluate(parsedTranche.plafond)
|
||||
const plancher = tranches[i - 1]
|
||||
? tranches[i - 1].plafond
|
||||
: { nodeValue: 0 }
|
||||
|
||||
let plafondValue: Evaluation<number> =
|
||||
plafond.nodeValue === null || multiplicateur.nodeValue === null
|
||||
? null
|
||||
: plafond.nodeValue * multiplicateur.nodeValue
|
||||
|
||||
try {
|
||||
plafondValue =
|
||||
plafondValue === Infinity || plafondValue === 0
|
||||
? plafondValue
|
||||
: convertUnit(
|
||||
inferUnit('*', [plafond.unit, multiplicateur.unit]),
|
||||
assiette.unit,
|
||||
plafondValue
|
||||
)
|
||||
} catch (e) {
|
||||
warning(
|
||||
this.options.logger,
|
||||
this.cache._meta.evaluationRuleStack[0],
|
||||
`L'unité du plafond de la tranche n°${
|
||||
i + 1
|
||||
} n'est pas compatible avec celle l'assiette`,
|
||||
e
|
||||
)
|
||||
}
|
||||
const plancherValue = tranches[i - 1] ? tranches[i - 1].plafondValue : 0
|
||||
const isAfterActive =
|
||||
plancherValue === null || assiette.nodeValue === null
|
||||
? null
|
||||
: plancherValue > assiette.nodeValue
|
||||
|
||||
const calculationValues = [plafond, assiette, multiplicateur, plancher]
|
||||
if (calculationValues.some((node) => node.nodeValue === null)) {
|
||||
return [
|
||||
[
|
||||
...tranches,
|
||||
{
|
||||
...parsedTranche,
|
||||
plafond,
|
||||
plafondValue,
|
||||
plancherValue,
|
||||
nodeValue: null,
|
||||
isActive: null,
|
||||
isAfterActive,
|
||||
missingVariables: mergeAllMissing(calculationValues),
|
||||
},
|
||||
],
|
||||
false,
|
||||
]
|
||||
}
|
||||
|
||||
if (
|
||||
!!tranches[i - 1] &&
|
||||
!!plancherValue &&
|
||||
(plafondValue as number) <= plancherValue
|
||||
) {
|
||||
evaluationError(
|
||||
this.options.logger,
|
||||
this.cache._meta.evaluationRuleStack[0],
|
||||
`Le plafond de la tranche n°${
|
||||
i + 1
|
||||
} a une valeur inférieure à celui de la tranche précédente`
|
||||
)
|
||||
}
|
||||
|
||||
const tranche = {
|
||||
...parsedTranche,
|
||||
plafond,
|
||||
plancherValue,
|
||||
plafondValue,
|
||||
isAfterActive,
|
||||
isActive:
|
||||
assiette.nodeValue >= plancherValue &&
|
||||
assiette.nodeValue < (plafondValue as number),
|
||||
}
|
||||
|
||||
return [[...tranches, tranche], tranche.isActive]
|
||||
},
|
||||
[[], false]
|
||||
)[0]
|
||||
}
|
|
@ -1,53 +0,0 @@
|
|||
import { ASTNode, Unit } from '../AST/types'
|
||||
import { warning } from '../error'
|
||||
import { registerEvaluationFunction } from '../evaluationFunctions'
|
||||
import parse from '../parse'
|
||||
import { convertUnit, parseUnit } from '../units'
|
||||
|
||||
export type UnitéNode = {
|
||||
unit: Unit
|
||||
explanation: ASTNode
|
||||
nodeKind: 'unité'
|
||||
}
|
||||
|
||||
export default function parseUnité(v, context): UnitéNode {
|
||||
const explanation = parse(v.valeur, context)
|
||||
const unit = parseUnit(v.unité, context.getUnitKey)
|
||||
|
||||
return {
|
||||
explanation,
|
||||
unit,
|
||||
nodeKind: parseUnité.nom,
|
||||
}
|
||||
}
|
||||
|
||||
parseUnité.nom = 'unité' as const
|
||||
|
||||
registerEvaluationFunction(parseUnité.nom, function evaluate(node) {
|
||||
const valeur = this.evaluate(node.explanation)
|
||||
|
||||
let nodeValue = valeur.nodeValue
|
||||
if (nodeValue !== false && 'unit' in node) {
|
||||
try {
|
||||
nodeValue = convertUnit(
|
||||
valeur.unit,
|
||||
node.unit,
|
||||
valeur.nodeValue as number
|
||||
)
|
||||
} catch (e) {
|
||||
warning(
|
||||
this.options.logger,
|
||||
this.cache._meta.evaluationRuleStack[0],
|
||||
"Erreur lors de la conversion d'unité explicite",
|
||||
e
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
...node,
|
||||
nodeValue,
|
||||
explanation: valeur,
|
||||
missingVariables: valeur.missingVariables,
|
||||
}
|
||||
})
|
|
@ -1,152 +0,0 @@
|
|||
import { EvaluationFunction } from '..'
|
||||
import { ASTNode, EvaluatedNode, Unit } from '../AST/types'
|
||||
import { warning } from '../error'
|
||||
import { bonus, defaultNode, mergeAllMissing } from '../evaluation'
|
||||
import { registerEvaluationFunction } from '../evaluationFunctions'
|
||||
import { convertNodeToUnit } from '../nodeUnits'
|
||||
import parse from '../parse'
|
||||
|
||||
export type VariationNode = {
|
||||
explanation: Array<{
|
||||
condition: ASTNode
|
||||
consequence: ASTNode
|
||||
satisfied?: boolean
|
||||
}>
|
||||
nodeKind: 'variations'
|
||||
}
|
||||
|
||||
export const devariate = (k, v, context): ASTNode => {
|
||||
if (k === 'valeur') {
|
||||
return parse(v, context)
|
||||
}
|
||||
const { variations, ...factoredKeys } = v
|
||||
const explanation = parse(
|
||||
{
|
||||
variations: variations.map(({ alors, sinon, si }) => {
|
||||
const { attributs, ...otherKeys } = alors ?? sinon
|
||||
return {
|
||||
[alors !== undefined ? 'alors' : 'sinon']: {
|
||||
...attributs,
|
||||
[k]: {
|
||||
...factoredKeys,
|
||||
...otherKeys,
|
||||
},
|
||||
},
|
||||
...(si !== undefined && { si }),
|
||||
}
|
||||
}),
|
||||
},
|
||||
context
|
||||
)
|
||||
return explanation
|
||||
}
|
||||
|
||||
export default function parseVariations(v, context): VariationNode {
|
||||
const explanation = v.map(({ si, alors, sinon }) =>
|
||||
sinon !== undefined
|
||||
? { consequence: parse(sinon, context), condition: defaultNode(true) }
|
||||
: { consequence: parse(alors, context), condition: parse(si, context) }
|
||||
)
|
||||
|
||||
return {
|
||||
explanation,
|
||||
nodeKind: 'variations',
|
||||
}
|
||||
}
|
||||
|
||||
const evaluate: EvaluationFunction<'variations'> = function (node) {
|
||||
const [nodeValue, explanation, unit] = node.explanation.reduce<
|
||||
[
|
||||
EvaluatedNode['nodeValue'],
|
||||
VariationNode['explanation'],
|
||||
Unit | undefined,
|
||||
boolean | null
|
||||
]
|
||||
>(
|
||||
(
|
||||
[evaluation, explanations, unit, previousConditions],
|
||||
{ condition, consequence },
|
||||
i: number
|
||||
) => {
|
||||
if (previousConditions === true) {
|
||||
return [
|
||||
evaluation,
|
||||
[...explanations, { condition, consequence }],
|
||||
unit,
|
||||
previousConditions,
|
||||
]
|
||||
}
|
||||
const evaluatedCondition = this.evaluate(condition)
|
||||
const currentCondition =
|
||||
previousConditions === null
|
||||
? previousConditions
|
||||
: !previousConditions &&
|
||||
(evaluatedCondition.nodeValue === null
|
||||
? null
|
||||
: evaluatedCondition.nodeValue !== false)
|
||||
|
||||
evaluatedCondition.missingVariables = bonus(
|
||||
evaluatedCondition.missingVariables
|
||||
)
|
||||
|
||||
if (currentCondition === false) {
|
||||
return [
|
||||
evaluation,
|
||||
[...explanations, { condition: evaluatedCondition, consequence }],
|
||||
unit,
|
||||
previousConditions,
|
||||
]
|
||||
}
|
||||
let evaluatedConsequence = this.evaluate(consequence)
|
||||
if (unit) {
|
||||
try {
|
||||
evaluatedConsequence = convertNodeToUnit(unit, evaluatedConsequence)
|
||||
} catch (e) {
|
||||
warning(
|
||||
this.options.logger,
|
||||
this.cache._meta.evaluationRuleStack[0],
|
||||
`L'unité de la branche n° ${
|
||||
i + 1
|
||||
} du mécanisme 'variations' n'est pas compatible avec celle d'une branche précédente`,
|
||||
e
|
||||
)
|
||||
}
|
||||
}
|
||||
return [
|
||||
currentCondition && evaluatedConsequence.nodeValue,
|
||||
[
|
||||
...explanations,
|
||||
{
|
||||
condition: evaluatedCondition,
|
||||
satisfied: evaluatedCondition.nodeValue !== false,
|
||||
consequence: evaluatedConsequence,
|
||||
},
|
||||
],
|
||||
unit || evaluatedConsequence.unit,
|
||||
previousConditions || currentCondition,
|
||||
]
|
||||
},
|
||||
[false, [], undefined, false]
|
||||
)
|
||||
|
||||
const missingVariables = mergeAllMissing(
|
||||
explanation.reduce<ASTNode[]>(
|
||||
(values, { condition, satisfied, consequence }) => [
|
||||
...values,
|
||||
condition,
|
||||
...(satisfied ? [consequence] : []),
|
||||
],
|
||||
[]
|
||||
)
|
||||
)
|
||||
|
||||
return {
|
||||
...node,
|
||||
nodeValue,
|
||||
...(unit !== undefined && { unit }),
|
||||
explanation,
|
||||
missingVariables,
|
||||
}
|
||||
}
|
||||
|
||||
registerEvaluationFunction('variations', evaluate)
|
|
@ -1,25 +0,0 @@
|
|||
import { EvaluatedNode, Unit } from './AST/types'
|
||||
import { convertUnit, simplifyUnit } from './units'
|
||||
|
||||
export function simplifyNodeUnit(node) {
|
||||
if (!node.unit) {
|
||||
return node
|
||||
}
|
||||
const unit = simplifyUnit(node.unit)
|
||||
|
||||
return convertNodeToUnit(unit, node)
|
||||
}
|
||||
|
||||
export function convertNodeToUnit<Node extends EvaluatedNode = EvaluatedNode>(
|
||||
to: Unit | undefined,
|
||||
node: Node
|
||||
): Node {
|
||||
return {
|
||||
...node,
|
||||
nodeValue:
|
||||
node.unit && typeof node.nodeValue === 'number'
|
||||
? convertUnit(node.unit, to, node.nodeValue)
|
||||
: node.nodeValue,
|
||||
unit: to,
|
||||
}
|
||||
}
|
|
@ -1,217 +0,0 @@
|
|||
import { Grammar, Parser } from 'nearley'
|
||||
import { ASTNode } from './AST/types'
|
||||
import { EngineError, syntaxError } from './error'
|
||||
import grammar from './grammar.ne'
|
||||
import abattement from './mecanisms/abattement'
|
||||
import applicable from './mecanisms/applicable'
|
||||
import arrondi from './mecanisms/arrondi'
|
||||
import barème from './mecanisms/barème'
|
||||
import { decompose } from './mecanisms/composantes'
|
||||
import { mecanismAllOf } from './mecanisms/condition-allof'
|
||||
import { mecanismOneOf } from './mecanisms/condition-oneof'
|
||||
import durée from './mecanisms/durée'
|
||||
import grille from './mecanisms/grille'
|
||||
import { mecanismInversion } from './mecanisms/inversion'
|
||||
import { mecanismMax } from './mecanisms/max'
|
||||
import { mecanismMin } from './mecanisms/min'
|
||||
import nonApplicable from './mecanisms/nonApplicable'
|
||||
import { mecanismOnePossibility } from './mecanisms/one-possibility'
|
||||
import operations from './mecanisms/operation'
|
||||
import parDéfaut from './mecanisms/parDéfaut'
|
||||
import plafond from './mecanisms/plafond'
|
||||
import plancher from './mecanisms/plancher'
|
||||
import { mecanismProduct } from './mecanisms/product'
|
||||
import { mecanismRecalcul } from './mecanisms/recalcul'
|
||||
import résoudreRéférenceCirculaire from './mecanisms/résoudre-référence-circulaire'
|
||||
import situation from './mecanisms/situation'
|
||||
import { mecanismSum } from './mecanisms/sum'
|
||||
import { mecanismSynchronisation } from './mecanisms/synchronisation'
|
||||
import tauxProgressif from './mecanisms/tauxProgressif'
|
||||
import unité from './mecanisms/unité'
|
||||
import variations, { devariate } from './mecanisms/variations'
|
||||
import { Context } from './parsePublicodes'
|
||||
import parseReference from './reference'
|
||||
import parseRule from './rule'
|
||||
|
||||
export default function parse(rawNode, context: Context): ASTNode {
|
||||
if (rawNode == null) {
|
||||
syntaxError(
|
||||
context.dottedName,
|
||||
`
|
||||
Une des valeurs de la formule est vide.
|
||||
Vérifiez que tous les champs à droite des deux points sont remplis`
|
||||
)
|
||||
}
|
||||
if (typeof rawNode === 'boolean') {
|
||||
syntaxError(
|
||||
context.dottedName,
|
||||
`
|
||||
Les valeurs booléennes true / false ne sont acceptées.
|
||||
Utilisez leur contrepartie française : 'oui' / 'non'`
|
||||
)
|
||||
}
|
||||
const node =
|
||||
typeof rawNode === 'object' ? rawNode : parseExpression(rawNode, context)
|
||||
if ('nom' in node) {
|
||||
return parseRule(node, context)
|
||||
}
|
||||
|
||||
return {
|
||||
...parseChainedMecanisms(node, context),
|
||||
rawNode,
|
||||
}
|
||||
}
|
||||
|
||||
const compiledGrammar = Grammar.fromCompiled(grammar)
|
||||
|
||||
function parseExpression(
|
||||
rawNode,
|
||||
context: Context
|
||||
): Record<string, unknown> | undefined {
|
||||
/* Strings correspond to infix expressions.
|
||||
* Indeed, a subset of expressions like simple arithmetic operations `3 + (quantity * 2)` or like `salary [month]` are more explicit that their prefixed counterparts.
|
||||
* This function makes them prefixed operations. */
|
||||
try {
|
||||
const [parseResult] = new Parser(compiledGrammar).feed(rawNode + '').results
|
||||
return parseResult
|
||||
} catch (e) {
|
||||
syntaxError(
|
||||
context.dottedName,
|
||||
`\`${rawNode}\` n'est pas une expression valide`,
|
||||
e
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
function parseMecanism(rawNode, context: Context) {
|
||||
if (Array.isArray(rawNode)) {
|
||||
syntaxError(
|
||||
context.dottedName,
|
||||
`
|
||||
Il manque le nom du mécanisme pour le tableau : [${rawNode
|
||||
.map((x) => `'${x}'`)
|
||||
.join(', ')}]
|
||||
Les mécanisme possibles sont : 'somme', 'le maximum de', 'le minimum de', 'toutes ces conditions', 'une de ces conditions'.
|
||||
`
|
||||
)
|
||||
}
|
||||
|
||||
const keys = Object.keys(rawNode)
|
||||
if (keys.length > 1) {
|
||||
syntaxError(
|
||||
context.dottedName,
|
||||
`
|
||||
Les mécanismes suivants se situent au même niveau : ${keys
|
||||
.map((x) => `'${x}'`)
|
||||
.join(', ')}
|
||||
Cela vient probablement d'une erreur dans l'indentation
|
||||
`
|
||||
)
|
||||
}
|
||||
if (keys.length === 0) {
|
||||
return { nodeKind: 'constant', nodeValue: null }
|
||||
}
|
||||
|
||||
const mecanismName = Object.keys(rawNode)[0]
|
||||
const values = rawNode[mecanismName]
|
||||
const parseFn = parseFunctions[mecanismName]
|
||||
|
||||
if (!parseFn) {
|
||||
syntaxError(
|
||||
context.dottedName,
|
||||
`
|
||||
Le mécanisme ${mecanismName} est inconnu.
|
||||
Vérifiez qu'il n'y ait pas d'erreur dans l'orthographe du nom.`
|
||||
)
|
||||
}
|
||||
try {
|
||||
// Mécanisme de composantes. Voir mécanismes.md/composantes
|
||||
if (values?.composantes) {
|
||||
return decompose(mecanismName, values, context)
|
||||
}
|
||||
if (values?.variations && Object.values(values).length > 1) {
|
||||
return devariate(mecanismName, values, context)
|
||||
}
|
||||
return parseFn(values, context)
|
||||
} catch (e) {
|
||||
if (e instanceof EngineError) {
|
||||
throw e
|
||||
}
|
||||
syntaxError(
|
||||
context.dottedName,
|
||||
mecanismName
|
||||
? `➡️ Dans le mécanisme ${mecanismName}
|
||||
${e.message}`
|
||||
: e.message
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Chainable mecanisme in their composition order (first one is applyied first)
|
||||
const chainableMecanisms = [
|
||||
applicable,
|
||||
nonApplicable,
|
||||
arrondi,
|
||||
unité,
|
||||
plancher,
|
||||
plafond,
|
||||
parDéfaut,
|
||||
situation,
|
||||
résoudreRéférenceCirculaire,
|
||||
abattement,
|
||||
]
|
||||
function parseChainedMecanisms(rawNode, context: Context): ASTNode {
|
||||
const parseFn = chainableMecanisms.find((fn) => fn.nom in rawNode)
|
||||
if (!parseFn) {
|
||||
return parseMecanism(rawNode, context)
|
||||
}
|
||||
const { [parseFn.nom]: param, ...valeur } = rawNode
|
||||
return parseMecanism(
|
||||
{
|
||||
[parseFn.nom]: {
|
||||
valeur,
|
||||
[parseFn.nom]: param,
|
||||
},
|
||||
},
|
||||
context
|
||||
)
|
||||
}
|
||||
|
||||
const parseFunctions = {
|
||||
...operations,
|
||||
...chainableMecanisms.reduce((acc, fn) => ({ [fn.nom]: fn, ...acc }), {}),
|
||||
'une possibilité': mecanismOnePossibility,
|
||||
'inversion numérique': mecanismInversion,
|
||||
recalcul: mecanismRecalcul,
|
||||
variable: parseReference,
|
||||
'une de ces conditions': mecanismOneOf,
|
||||
'toutes ces conditions': mecanismAllOf,
|
||||
somme: mecanismSum,
|
||||
multiplication: mecanismProduct,
|
||||
produit: mecanismProduct,
|
||||
barème,
|
||||
grille,
|
||||
'taux progressif': tauxProgressif,
|
||||
durée,
|
||||
'le maximum de': mecanismMax,
|
||||
'le minimum de': mecanismMin,
|
||||
variations,
|
||||
synchronisation: mecanismSynchronisation,
|
||||
valeur: parse,
|
||||
objet: (v) => ({
|
||||
type: 'objet',
|
||||
nodeValue: v,
|
||||
nodeKind: 'constant',
|
||||
}),
|
||||
constant: (v) => ({
|
||||
type: v.type,
|
||||
// In the documentation we want to display constants defined in the source
|
||||
// with their full precision. This is especially useful for percentages like
|
||||
// APEC 0,036 %.
|
||||
fullPrecision: true,
|
||||
nodeValue: v.nodeValue,
|
||||
nodeKind: 'constant',
|
||||
}),
|
||||
}
|
||||
|
||||
export const mecanismKeys = Object.keys(parseFunctions)
|
|
@ -1,122 +0,0 @@
|
|||
import yaml from 'yaml'
|
||||
import { ParsedRules, Logger } from '.'
|
||||
import { makeASTTransformer, traverseParsedRules } from './AST'
|
||||
import parse from './parse'
|
||||
import { getReplacements, inlineReplacements } from './replacement'
|
||||
import { Rule, RuleNode } from './rule'
|
||||
import { disambiguateRuleReference } from './ruleUtils'
|
||||
import { getUnitKey } from './units'
|
||||
|
||||
export type Context = {
|
||||
dottedName: string
|
||||
parsedRules: Record<string, RuleNode>
|
||||
ruleTitle?: string
|
||||
getUnitKey?: getUnitKey
|
||||
logger: Logger
|
||||
}
|
||||
|
||||
type RawRule = Omit<Rule, 'nom'> | string | number
|
||||
export type RawPublicodes = Record<string, RawRule>
|
||||
|
||||
export default function parsePublicodes(
|
||||
rawRules: RawPublicodes | string,
|
||||
partialContext: Partial<Context> = {}
|
||||
): ParsedRules<string> {
|
||||
// STEP 1: parse Yaml
|
||||
let rules =
|
||||
typeof rawRules === 'string'
|
||||
? (yaml.parse(('' + rawRules).replace(/\t/g, ' ')) as RawPublicodes)
|
||||
: { ...rawRules }
|
||||
|
||||
// STEP 2: transpile [ref] writing
|
||||
rules = transpileRef(rules)
|
||||
|
||||
// STEP 3: Rules parsing
|
||||
const context: Context = {
|
||||
dottedName: partialContext.dottedName ?? '',
|
||||
parsedRules: partialContext.parsedRules ?? {},
|
||||
logger: partialContext.logger ?? console,
|
||||
getUnitKey: partialContext.getUnitKey ?? ((x) => x),
|
||||
}
|
||||
|
||||
Object.entries(rules).forEach(([dottedName, rule]) => {
|
||||
if (typeof rule === 'string' || typeof rule === 'number') {
|
||||
rule = {
|
||||
formule: `${rule}`,
|
||||
}
|
||||
}
|
||||
if (typeof rule !== 'object') {
|
||||
throw new SyntaxError(
|
||||
`Rule ${dottedName} is incorrectly written. Please give it a proper value.`
|
||||
)
|
||||
}
|
||||
parse({ nom: dottedName, ...rule }, context)
|
||||
})
|
||||
let parsedRules = context.parsedRules
|
||||
|
||||
// STEP 4: Disambiguate reference
|
||||
parsedRules = traverseParsedRules(
|
||||
disambiguateReference(parsedRules),
|
||||
parsedRules
|
||||
)
|
||||
|
||||
// STEP 5: Inline replacements
|
||||
const replacements = getReplacements(parsedRules)
|
||||
parsedRules = traverseParsedRules(
|
||||
inlineReplacements(replacements, context.logger),
|
||||
parsedRules
|
||||
)
|
||||
|
||||
// TODO STEP 6: check for cycle
|
||||
|
||||
// TODO STEP 7: type check
|
||||
|
||||
return parsedRules
|
||||
}
|
||||
|
||||
// We recursively traverse the YAML tree in order to transform named parameters
|
||||
// into rules.
|
||||
function transpileRef(object: Record<string, any> | string | Array<any>) {
|
||||
if (Array.isArray(object)) {
|
||||
return object.map(transpileRef)
|
||||
}
|
||||
if (!object || typeof object !== 'object') {
|
||||
return object
|
||||
}
|
||||
object
|
||||
return Object.entries(object).reduce((obj, [key, value]) => {
|
||||
const match = /\[ref( (.+))?\]$/.exec(key)
|
||||
|
||||
if (!match) {
|
||||
return { ...obj, [key]: transpileRef(value) }
|
||||
}
|
||||
|
||||
const argumentType = key.replace(match[0], '').trim()
|
||||
const argumentName = match[2]?.trim() || argumentType
|
||||
|
||||
return {
|
||||
...obj,
|
||||
[argumentType]: {
|
||||
nom: argumentName,
|
||||
valeur: transpileRef(value),
|
||||
},
|
||||
}
|
||||
}, {})
|
||||
}
|
||||
|
||||
export const disambiguateReference = (parsedRules: Record<string, RuleNode>) =>
|
||||
makeASTTransformer((node) => {
|
||||
if (node.nodeKind === 'reference') {
|
||||
const dottedName = disambiguateRuleReference(
|
||||
parsedRules,
|
||||
node.contextDottedName,
|
||||
node.name
|
||||
)
|
||||
return {
|
||||
...node,
|
||||
dottedName,
|
||||
title: parsedRules[dottedName].title,
|
||||
acronym: parsedRules[dottedName].rawNode.acronyme,
|
||||
}
|
||||
}
|
||||
})
|
|
@ -1,35 +0,0 @@
|
|||
import { InternalError } from './error'
|
||||
import { registerEvaluationFunction } from './evaluationFunctions'
|
||||
import { Context } from './parsePublicodes'
|
||||
|
||||
export type ReferenceNode = {
|
||||
nodeKind: 'reference'
|
||||
name: string
|
||||
contextDottedName: string
|
||||
dottedName?: string
|
||||
}
|
||||
|
||||
export default function parseReference(
|
||||
v: string,
|
||||
context: Context
|
||||
): ReferenceNode {
|
||||
return {
|
||||
nodeKind: 'reference',
|
||||
name: v,
|
||||
contextDottedName: context.dottedName,
|
||||
}
|
||||
}
|
||||
|
||||
registerEvaluationFunction('reference', function evaluateReference(node) {
|
||||
if (!node.dottedName) {
|
||||
throw new InternalError(node)
|
||||
}
|
||||
|
||||
const explanation = this.evaluate(this.parsedRules[node.dottedName])
|
||||
return {
|
||||
...node,
|
||||
missingVariables: explanation.missingVariables,
|
||||
nodeValue: explanation.nodeValue,
|
||||
...('unit' in explanation && { unit: explanation.unit }),
|
||||
}
|
||||
})
|
|
@ -1,214 +0,0 @@
|
|||
import { Logger } from '.'
|
||||
import { makeASTTransformer } from './AST'
|
||||
import { ASTNode } from './AST/types'
|
||||
import { InternalError, warning } from './error'
|
||||
import { defaultNode } from './evaluation'
|
||||
import parse from './parse'
|
||||
import { Context } from './parsePublicodes'
|
||||
import { Rule, RuleNode } from './rule'
|
||||
|
||||
export type ReplacementRule = {
|
||||
nodeKind: 'replacementRule'
|
||||
definitionRule: ASTNode & { nodeKind: 'reference' }
|
||||
replacedReference: ASTNode & { nodeKind: 'reference' }
|
||||
replacementNode: ASTNode
|
||||
whiteListedNames: Array<ASTNode & { nodeKind: 'reference' }>
|
||||
rawNode: any
|
||||
blackListedNames: Array<ASTNode & { nodeKind: 'reference' }>
|
||||
remplacementRuleId: number
|
||||
}
|
||||
|
||||
// Replacements depend on the context and their evaluation implies using
|
||||
// "variations" node everywhere there is a reference to the original rule.
|
||||
// However for performance reason we want to mutualize identical "variations"
|
||||
// nodes instead of duplicating them, to avoid wasteful computations.
|
||||
//
|
||||
// The implementation works by first attributing an identifier for each
|
||||
// replacementRule. We then use this identifier to create a cache key that
|
||||
// represents the combinaison of applicables replacements for a given reference.
|
||||
// For example if replacements 12, 13 et 643 are applicable we use the key
|
||||
// `12-13-643` as the cache identifier in the `inlineReplacements` function.
|
||||
let remplacementRuleId = 0
|
||||
const cache = {}
|
||||
|
||||
export function parseReplacements(
|
||||
replacements: Rule['remplace'],
|
||||
context: Context
|
||||
): Array<ReplacementRule> {
|
||||
if (!replacements) {
|
||||
return []
|
||||
}
|
||||
return (Array.isArray(replacements) ? replacements : [replacements]).map(
|
||||
(replacement) => {
|
||||
if (typeof replacement === 'string') {
|
||||
replacement = { règle: replacement }
|
||||
}
|
||||
|
||||
const replacedReference = parse(replacement.règle, context)
|
||||
const replacementNode = parse(
|
||||
replacement.par ?? context.dottedName,
|
||||
context
|
||||
)
|
||||
|
||||
const [whiteListedNames, blackListedNames] = [
|
||||
replacement.dans ?? [],
|
||||
replacement['sauf dans'] ?? [],
|
||||
]
|
||||
.map((dottedName) =>
|
||||
Array.isArray(dottedName) ? dottedName : [dottedName]
|
||||
)
|
||||
.map((refs) => refs.map((ref) => parse(ref, context)))
|
||||
|
||||
return {
|
||||
nodeKind: 'replacementRule',
|
||||
rawNode: replacement,
|
||||
definitionRule: parse(context.dottedName, context),
|
||||
replacedReference,
|
||||
replacementNode,
|
||||
whiteListedNames,
|
||||
blackListedNames,
|
||||
remplacementRuleId: remplacementRuleId++,
|
||||
} as ReplacementRule
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
export function parseRendNonApplicable(
|
||||
rules: Rule['rend non applicable'],
|
||||
context: Context
|
||||
): Array<ReplacementRule> {
|
||||
return parseReplacements(rules, context).map(
|
||||
(replacement) =>
|
||||
({
|
||||
...replacement,
|
||||
replacementNode: defaultNode(false),
|
||||
} as ReplacementRule)
|
||||
)
|
||||
}
|
||||
|
||||
export function getReplacements(
|
||||
parsedRules: Record<string, RuleNode>
|
||||
): Record<string, Array<ReplacementRule>> {
|
||||
return Object.values(parsedRules)
|
||||
.flatMap((rule) => rule.replacements)
|
||||
.reduce((acc, r: ReplacementRule) => {
|
||||
if (!r.replacedReference.dottedName) {
|
||||
throw new InternalError(r)
|
||||
}
|
||||
const key = r.replacedReference.dottedName
|
||||
return { ...acc, [key]: [...(acc[key] ?? []), r] }
|
||||
}, {})
|
||||
}
|
||||
|
||||
export function inlineReplacements(
|
||||
replacements: Record<string, Array<ReplacementRule>>,
|
||||
logger: Logger
|
||||
): (n: ASTNode) => ASTNode {
|
||||
return makeASTTransformer((node, transform) => {
|
||||
if (
|
||||
node.nodeKind === 'replacementRule' ||
|
||||
node.nodeKind === 'inversion' ||
|
||||
node.nodeKind === 'une possibilité'
|
||||
) {
|
||||
return false
|
||||
}
|
||||
if (node.nodeKind === 'recalcul') {
|
||||
// We don't replace references in recalcul keys
|
||||
return {
|
||||
...node,
|
||||
explanation: {
|
||||
...node.explanation,
|
||||
recalcul: transform(node.explanation.recalcul),
|
||||
amendedSituation: node.explanation.amendedSituation.map(
|
||||
([name, value]) => [name, transform(value)]
|
||||
),
|
||||
},
|
||||
}
|
||||
}
|
||||
if (node.nodeKind === 'reference') {
|
||||
if (!node.dottedName) {
|
||||
throw new InternalError(node)
|
||||
}
|
||||
return replace(node, replacements[node.dottedName] ?? [], logger)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function replace(
|
||||
node: ASTNode & { nodeKind: 'reference' }, //& { dottedName: string },
|
||||
replacements: Array<ReplacementRule>,
|
||||
logger: Logger
|
||||
): ASTNode {
|
||||
// TODO : handle transitivité
|
||||
|
||||
const applicableReplacements = replacements
|
||||
.filter(
|
||||
({ definitionRule }) =>
|
||||
definitionRule.dottedName !== node.contextDottedName
|
||||
)
|
||||
.filter(
|
||||
({ whiteListedNames }) =>
|
||||
!whiteListedNames.length ||
|
||||
whiteListedNames.some((name) =>
|
||||
node.contextDottedName.startsWith(name.dottedName as string)
|
||||
)
|
||||
)
|
||||
.filter(
|
||||
({ blackListedNames }) =>
|
||||
!blackListedNames.length ||
|
||||
blackListedNames.every(
|
||||
(name) =>
|
||||
!node.contextDottedName.startsWith(name.dottedName as string)
|
||||
)
|
||||
)
|
||||
.sort((r1, r2) => {
|
||||
// Replacement with whitelist conditions have precedence over the others
|
||||
const criterion1 =
|
||||
+!!r2.whiteListedNames.length - +!!r1.whiteListedNames.length
|
||||
// Replacement with blacklist condition have precedence over the others
|
||||
const criterion2 =
|
||||
+!!r2.blackListedNames.length - +!!r1.blackListedNames.length
|
||||
return criterion1 || criterion2
|
||||
})
|
||||
if (!applicableReplacements.length) {
|
||||
return node
|
||||
}
|
||||
if (applicableReplacements.length > 1) {
|
||||
const displayVerboseWarning = false
|
||||
if (displayVerboseWarning) {
|
||||
warning(
|
||||
logger,
|
||||
node.contextDottedName,
|
||||
`
|
||||
Il existe plusieurs remplacements pour la référence '${node.dottedName}'.
|
||||
Lors de l'execution, ils seront résolus dans l'odre suivant :
|
||||
${applicableReplacements.map(
|
||||
(replacement) =>
|
||||
`\n\t- Celui définit dans la règle '${replacement.definitionRule.dottedName}'`
|
||||
)}
|
||||
`
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const applicableReplacementsCacheKey = applicableReplacements
|
||||
.map((n) => n.remplacementRuleId)
|
||||
.join('-')
|
||||
|
||||
cache[applicableReplacementsCacheKey] ??= {
|
||||
nodeKind: 'variations',
|
||||
visualisationKind: 'replacement',
|
||||
rawNode: node.rawNode,
|
||||
explanation: [
|
||||
...applicableReplacements.map((replacement) => ({
|
||||
condition: replacement.definitionRule,
|
||||
consequence: replacement.replacementNode,
|
||||
})),
|
||||
{
|
||||
condition: defaultNode(true),
|
||||
consequence: node,
|
||||
},
|
||||
],
|
||||
}
|
||||
return cache[applicableReplacementsCacheKey]
|
||||
}
|
|
@ -1,183 +0,0 @@
|
|||
import { ASTNode, EvaluatedNode } from './AST/types'
|
||||
import { warning } from './error'
|
||||
import { bonus, mergeMissing } from './evaluation'
|
||||
import { registerEvaluationFunction } from './evaluationFunctions'
|
||||
import { capitalise0 } from './format'
|
||||
import parse, { mecanismKeys } from './parse'
|
||||
import { Context } from './parsePublicodes'
|
||||
import { ReferenceNode } from './reference'
|
||||
import {
|
||||
parseRendNonApplicable,
|
||||
parseReplacements,
|
||||
ReplacementRule,
|
||||
} from './replacement'
|
||||
import { nameLeaf, ruleParents } from './ruleUtils'
|
||||
|
||||
export type Rule = {
|
||||
formule?: Record<string, unknown> | string
|
||||
question?: string
|
||||
description?: string
|
||||
unité?: string
|
||||
acronyme?: string
|
||||
exemples?: any
|
||||
nom: string
|
||||
résumé?: string
|
||||
icônes?: string
|
||||
titre?: string
|
||||
sévérité?: string
|
||||
cotisation?: {
|
||||
branche: string
|
||||
}
|
||||
type?: string
|
||||
note?: string
|
||||
remplace?: RendNonApplicable | Array<RendNonApplicable>
|
||||
'rend non applicable'?: Remplace | Array<string>
|
||||
suggestions?: Record<string, string | number | Record<string, unknown>>
|
||||
références?: { [source: string]: string }
|
||||
API?: string
|
||||
'identifiant court'?: string
|
||||
}
|
||||
|
||||
type Remplace =
|
||||
| {
|
||||
règle: string
|
||||
par?: Record<string, unknown> | string | number
|
||||
dans?: Array<string> | string
|
||||
'sauf dans'?: Array<string> | string
|
||||
}
|
||||
| string
|
||||
type RendNonApplicable = Exclude<Remplace, { par: any }>
|
||||
|
||||
export type RuleNode = {
|
||||
dottedName: string
|
||||
title: string
|
||||
nodeKind: 'rule'
|
||||
virtualRule: boolean
|
||||
rawNode: Rule
|
||||
replacements: Array<ReplacementRule>
|
||||
explanation: {
|
||||
parent: ASTNode | false
|
||||
valeur: ASTNode
|
||||
}
|
||||
suggestions: Record<string, ASTNode>
|
||||
'identifiant court'?: string
|
||||
}
|
||||
|
||||
export default function parseRule(
|
||||
rawRule: Rule,
|
||||
context: Context
|
||||
): ReferenceNode {
|
||||
const dottedName = [context.dottedName, rawRule.nom]
|
||||
.filter(Boolean)
|
||||
.join(' . ')
|
||||
|
||||
const name = nameLeaf(dottedName)
|
||||
const ruleTitle = capitalise0(
|
||||
rawRule['titre'] ??
|
||||
(context.ruleTitle ? `${context.ruleTitle} (${name})` : name)
|
||||
)
|
||||
|
||||
if (context.parsedRules[dottedName]) {
|
||||
throw new Error(`La référence '${dottedName}' a déjà été définie`)
|
||||
}
|
||||
|
||||
const ruleValue = {
|
||||
...Object.fromEntries(
|
||||
Object.entries(rawRule).filter(([key]) => mecanismKeys.includes(key))
|
||||
),
|
||||
...('formule' in rawRule && { valeur: rawRule.formule }),
|
||||
'nom dans la situation': dottedName,
|
||||
}
|
||||
|
||||
const ruleContext = { ...context, dottedName, ruleTitle }
|
||||
|
||||
const [parent] = ruleParents(dottedName)
|
||||
const explanation = {
|
||||
valeur: parse(ruleValue, ruleContext),
|
||||
parent: !!parent && parse(parent, context),
|
||||
}
|
||||
context.parsedRules[dottedName] = {
|
||||
dottedName,
|
||||
replacements: [
|
||||
...parseRendNonApplicable(rawRule['rend non applicable'], ruleContext),
|
||||
...parseReplacements(rawRule.remplace, ruleContext),
|
||||
],
|
||||
title: ruleTitle,
|
||||
suggestions: Object.fromEntries(
|
||||
Object.entries(rawRule.suggestions ?? {}).map(([name, node]) => [
|
||||
name,
|
||||
parse(node, ruleContext),
|
||||
])
|
||||
),
|
||||
nodeKind: 'rule',
|
||||
explanation,
|
||||
rawNode: rawRule,
|
||||
virtualRule: !!context.dottedName,
|
||||
} as RuleNode
|
||||
|
||||
// We return the parsedReference
|
||||
return parse(rawRule.nom, context) as ReferenceNode
|
||||
}
|
||||
|
||||
registerEvaluationFunction('rule', function evaluate(node) {
|
||||
const explanation = { ...node.explanation }
|
||||
let parent: EvaluatedNode | null = null
|
||||
if (explanation.parent) {
|
||||
if (this.cache._meta.parentRuleStack.includes(node.dottedName)) {
|
||||
parent = { nodeValue: null } as EvaluatedNode
|
||||
} else {
|
||||
this.cache._meta.parentRuleStack.unshift(node.dottedName)
|
||||
parent = this.evaluate(explanation.parent) as EvaluatedNode
|
||||
this.cache._meta.parentRuleStack.shift()
|
||||
}
|
||||
explanation.parent = parent
|
||||
}
|
||||
let valeur: EvaluatedNode | null = null
|
||||
if (!parent || parent.nodeValue !== false) {
|
||||
if (
|
||||
this.cache._meta.evaluationRuleStack.filter(
|
||||
(dottedName) => dottedName === node.dottedName
|
||||
).length > 15
|
||||
// I don't know why this magic number, but below, cycle are
|
||||
// detected "too early", which leads to blank value in brut-net simulator
|
||||
) {
|
||||
warning(
|
||||
this.options.logger,
|
||||
node.dottedName,
|
||||
`
|
||||
Un cycle a été détecté dans lors de l'évaluation de cette règle.
|
||||
Par défaut cette règle sera évaluée à 'null'.
|
||||
|
||||
Pour indiquer au moteur de résoudre la référence circulaire en trouvant le point fixe
|
||||
de la fonction, il vous suffit d'ajouter l'attribut suivant niveau de la règle :
|
||||
|
||||
${node.dottedName}:
|
||||
"résoudre la référence circulaire: oui"
|
||||
...
|
||||
|
||||
`
|
||||
)
|
||||
|
||||
valeur = { nodeValue: null } as EvaluatedNode
|
||||
} else {
|
||||
this.cache._meta.evaluationRuleStack.unshift(node.dottedName)
|
||||
valeur = this.evaluate(explanation.valeur) as EvaluatedNode
|
||||
this.cache._meta.evaluationRuleStack.shift()
|
||||
}
|
||||
|
||||
explanation.valeur = valeur
|
||||
}
|
||||
|
||||
const evaluation = {
|
||||
...node,
|
||||
explanation,
|
||||
nodeValue: valeur && 'nodeValue' in valeur ? valeur.nodeValue : false,
|
||||
missingVariables: mergeMissing(
|
||||
valeur?.missingVariables,
|
||||
bonus(parent?.missingVariables)
|
||||
),
|
||||
...(valeur && 'unit' in valeur && { unit: valeur.unit }),
|
||||
}
|
||||
|
||||
return evaluation
|
||||
})
|
|
@ -1,56 +0,0 @@
|
|||
import { syntaxError } from './error'
|
||||
import { RuleNode } from './rule'
|
||||
|
||||
const splitName = (str: string) => str.split(' . ')
|
||||
const joinName = (strs: Array<string>) => strs.join(' . ')
|
||||
export const nameLeaf = (name: string) => splitName(name).slice(-1)?.[0]
|
||||
export const encodeRuleName = (name: string): string =>
|
||||
name
|
||||
?.replace(/\s\.\s/g, '/')
|
||||
.replace(/-/g, '\u2011') // replace with a insecable tiret to differenciate from space
|
||||
.replace(/\s/g, '-')
|
||||
export const decodeRuleName = (name: string): string =>
|
||||
name
|
||||
.replace(/\//g, ' . ')
|
||||
.replace(/-/g, ' ')
|
||||
.replace(/\u2011/g, '-')
|
||||
export function ruleParents(dottedName: string): Array<string> {
|
||||
const fragments = splitName(dottedName) // dottedName ex. [CDD . événements . rupture]
|
||||
return Array(fragments.length - 1)
|
||||
.fill(0)
|
||||
.map((f, i) => fragments.slice(0, i + 1))
|
||||
.map(joinName)
|
||||
.reverse()
|
||||
}
|
||||
|
||||
export function disambiguateRuleReference<R extends Record<string, RuleNode>>(
|
||||
rules: R,
|
||||
contextName = '',
|
||||
partialName: string
|
||||
): keyof R {
|
||||
const possibleDottedName = [contextName, ...ruleParents(contextName), '']
|
||||
.map((x) => (x ? x + ' . ' + partialName : partialName))
|
||||
// Rules can reference themselves, but it should be the last thing to check
|
||||
.sort((a, b) => (a === contextName ? 1 : b === contextName ? -1 : 0))
|
||||
|
||||
const dottedName = possibleDottedName.find((name) => name in rules)
|
||||
if (!dottedName) {
|
||||
syntaxError(
|
||||
contextName,
|
||||
`La référence '${partialName}' est introuvable.
|
||||
Vérifiez que l'orthographe et l'espace de nom sont corrects`
|
||||
)
|
||||
throw new Error()
|
||||
}
|
||||
return dottedName
|
||||
}
|
||||
|
||||
export function ruleWithDedicatedDocumentationPage(rule) {
|
||||
return (
|
||||
rule.virtualRule !== true &&
|
||||
rule.type !== 'groupe' &&
|
||||
rule.type !== 'texte' &&
|
||||
rule.type !== 'paragraphe' &&
|
||||
rule.type !== 'notification'
|
||||
)
|
||||
}
|
|
@ -1,18 +0,0 @@
|
|||
import { EvaluatedNode } from './index'
|
||||
import { serializeUnit } from './units'
|
||||
export default function serializeEvaluation(
|
||||
node: EvaluatedNode
|
||||
): string | undefined {
|
||||
if (typeof node.nodeValue === 'number') {
|
||||
const serializedUnit = serializeUnit(node.unit)
|
||||
return (
|
||||
'' +
|
||||
node.nodeValue +
|
||||
(serializedUnit ? serializedUnit.replace(/\s/g, '') : '')
|
||||
)
|
||||
} else if (typeof node.nodeValue === 'boolean') {
|
||||
return node.nodeValue ? 'oui' : 'non'
|
||||
} else if (typeof node.nodeValue === 'string') {
|
||||
return `'${node.nodeValue}'`
|
||||
}
|
||||
}
|
|
@ -1,4 +0,0 @@
|
|||
declare module '*.md' {
|
||||
const content: string
|
||||
export default content
|
||||
}
|
|
@ -1,4 +0,0 @@
|
|||
declare module '*.ne' {
|
||||
const content: any
|
||||
export default content
|
||||
}
|
|
@ -1,6 +0,0 @@
|
|||
// TODO: We could have better types for yaml imports (it works automatically for JSON modules)
|
||||
|
||||
declare module '*.yaml' {
|
||||
const content: any
|
||||
export default content
|
||||
}
|
|
@ -1,120 +0,0 @@
|
|||
// We use a JavaScript implementation of the Brent method to find the root (the
|
||||
// "zero") of a monotone function. There are other methods like the
|
||||
// Newton-Raphson method, but they take the derivative of the function as an
|
||||
// input, wich in our case is costly to calculate. The Brent method doesn't
|
||||
// need to calculate the derivative.
|
||||
// An interesting description of the algorithm can be found here:
|
||||
// https://blogs.mathworks.com/cleve/2015/10/26/zeroin-part-2-brents-version/
|
||||
|
||||
/**
|
||||
* Copied from https://gist.github.com/borgar/3317728
|
||||
*
|
||||
* Searches the interval from <tt>lowerLimit</tt> to <tt>upperLimit</tt>
|
||||
* for a root (i.e., zero) of the function <tt>func</tt> with respect to
|
||||
* its first argument using Brent's method root-finding algorithm.
|
||||
*
|
||||
* Translated from zeroin.c in http://www.netlib.org/c/brent.shar.
|
||||
*
|
||||
* Copyright (c) 2012 Borgar Thorsteinsson <borgar@borgar.net>
|
||||
* MIT License, http://www.opensource.org/licenses/mit-license.php
|
||||
*
|
||||
* @param func function for which the root is sought.
|
||||
* @param lowerLimit the lower point of the interval to be searched.
|
||||
* @param upperLimit the upper point of the interval to be searched.
|
||||
* @param errorTol the desired accuracy (convergence tolerance).
|
||||
* @param maxIter the maximum number of iterations.
|
||||
* @param acceptableErrorTol return a result even if errorTol isn't reached after maxIter.
|
||||
* @returns an estimate for the root within accuracy.
|
||||
*
|
||||
*/
|
||||
export default function uniroot(
|
||||
func: (x: number) => number,
|
||||
lowerLimit: number,
|
||||
upperLimit: number,
|
||||
errorTol = 0,
|
||||
maxIter = 100,
|
||||
acceptableErrorTol = 0
|
||||
) {
|
||||
let a = lowerLimit,
|
||||
b = upperLimit,
|
||||
c = a,
|
||||
fa = func(a),
|
||||
fb = func(b),
|
||||
fc = fa,
|
||||
actualTolerance: number,
|
||||
newStep: number, // Step at this iteration
|
||||
prevStep: number, // Distance from the last but one to the last approximation
|
||||
p: number, // Interpolation step is calculated in the form p/q; division is delayed until the last moment
|
||||
q: number,
|
||||
fallback: number | undefined = undefined
|
||||
|
||||
while (maxIter-- > 0) {
|
||||
prevStep = b - a
|
||||
|
||||
if (Math.abs(fc) < Math.abs(fb)) {
|
||||
// Swap data for b to be the best approximation
|
||||
;(a = b), (b = c), (c = a)
|
||||
;(fa = fb), (fb = fc), (fc = fa)
|
||||
}
|
||||
|
||||
actualTolerance = 1e-15 * Math.abs(b) + errorTol / 2
|
||||
newStep = (c - b) / 2
|
||||
|
||||
if (Math.abs(newStep) <= actualTolerance || fb === 0) {
|
||||
return b // Acceptable approx. is found
|
||||
}
|
||||
|
||||
// Decide if the interpolation can be tried
|
||||
if (Math.abs(prevStep) >= actualTolerance && Math.abs(fa) > Math.abs(fb)) {
|
||||
// If prevStep was large enough and was in true direction, Interpolatiom may be tried
|
||||
let t1: number, t2: number
|
||||
const cb = c - b
|
||||
if (a === c) {
|
||||
// If we have only two distinct points linear interpolation can only be applied
|
||||
t1 = fb / fa
|
||||
p = cb * t1
|
||||
q = 1.0 - t1
|
||||
} else {
|
||||
// Quadric inverse interpolation
|
||||
;(q = fa / fc), (t1 = fb / fc), (t2 = fb / fa)
|
||||
p = t2 * (cb * q * (q - t1) - (b - a) * (t1 - 1))
|
||||
q = (q - 1) * (t1 - 1) * (t2 - 1)
|
||||
}
|
||||
|
||||
if (p > 0) {
|
||||
q = -q // p was calculated with the opposite sign; make p positive
|
||||
} else {
|
||||
p = -p // and assign possible minus to q
|
||||
}
|
||||
|
||||
if (
|
||||
p < 0.75 * cb * q - Math.abs(actualTolerance * q) / 2 &&
|
||||
p < Math.abs((prevStep * q) / 2)
|
||||
) {
|
||||
// If (b + p / q) falls in [b,c] and isn't too large it is accepted
|
||||
newStep = p / q
|
||||
}
|
||||
|
||||
// If p/q is too large then the bissection procedure can reduce [b,c] range to more extent
|
||||
}
|
||||
|
||||
if (Math.abs(newStep) < actualTolerance) {
|
||||
// Adjust the step to be not less than tolerance
|
||||
newStep = newStep > 0 ? actualTolerance : -actualTolerance
|
||||
}
|
||||
|
||||
;(a = b), (fa = fb) // Save the previous approx.
|
||||
;(b += newStep), (fb = func(b)) // Do step to a new approxim.
|
||||
|
||||
if ((fb > 0 && fc > 0) || (fb < 0 && fc < 0)) {
|
||||
;(c = a), (fc = fa) // Adjust c for it to have a sign opposite to that of b
|
||||
}
|
||||
if (Math.abs(fb) < errorTol) {
|
||||
return b
|
||||
}
|
||||
if (Math.abs(fb) < acceptableErrorTol) {
|
||||
fallback = b
|
||||
}
|
||||
}
|
||||
return fallback
|
||||
}
|
|
@ -1,299 +0,0 @@
|
|||
import { Evaluation, Unit } from './AST/types'
|
||||
|
||||
export type getUnitKey = (writtenUnit: string) => string
|
||||
export type formatUnit = (unit: string, count: number) => string
|
||||
|
||||
export const parseUnit = (
|
||||
string: string,
|
||||
getUnitKey: getUnitKey = (x) => x
|
||||
): Unit => {
|
||||
const [a, ...b] = string.split('/').map((u) => u.trim()),
|
||||
result = {
|
||||
numerators: a
|
||||
.split('.')
|
||||
.filter(Boolean)
|
||||
.map((unit) => getUnitKey(unit)),
|
||||
denominators: b.map((unit) => getUnitKey(unit)),
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
const printUnits = (
|
||||
units: Array<string>,
|
||||
count: number,
|
||||
formatUnit: formatUnit = (x) => x
|
||||
): string =>
|
||||
units
|
||||
.sort()
|
||||
.map((unit) => formatUnit(unit, count))
|
||||
.join('.')
|
||||
|
||||
const plural = 2
|
||||
export const serializeUnit = (
|
||||
rawUnit: Unit | undefined | string,
|
||||
count: number = plural,
|
||||
formatUnit: formatUnit = (x) => x
|
||||
): string | undefined => {
|
||||
if (rawUnit === null || typeof rawUnit !== 'object') {
|
||||
return typeof rawUnit === 'string' ? formatUnit(rawUnit, count) : rawUnit
|
||||
}
|
||||
const unit = simplify(rawUnit),
|
||||
{ numerators = [], denominators = [] } = unit
|
||||
|
||||
const n = numerators.length > 0
|
||||
const d = denominators.length > 0
|
||||
const string =
|
||||
!n && !d
|
||||
? ''
|
||||
: n && !d
|
||||
? printUnits(numerators, count, formatUnit)
|
||||
: !n && d
|
||||
? `par ${printUnits(denominators, 1, formatUnit)}`
|
||||
: `${printUnits(numerators, plural, formatUnit)} / ${printUnits(
|
||||
denominators,
|
||||
1,
|
||||
formatUnit
|
||||
)}`
|
||||
|
||||
return string
|
||||
}
|
||||
|
||||
type SupportedOperators = '*' | '/' | '+' | '-'
|
||||
|
||||
const noUnit = { numerators: [], denominators: [] }
|
||||
export const inferUnit = (
|
||||
operator: SupportedOperators,
|
||||
rawUnits: Array<Unit | undefined>
|
||||
): Unit | undefined => {
|
||||
if (operator === '/') {
|
||||
if (rawUnits.length !== 2)
|
||||
throw new Error('Infer units of a division with units.length !== 2)')
|
||||
return inferUnit('*', [
|
||||
rawUnits[0] || noUnit,
|
||||
{
|
||||
numerators: (rawUnits[1] || noUnit).denominators,
|
||||
denominators: (rawUnits[1] || noUnit).numerators,
|
||||
},
|
||||
])
|
||||
}
|
||||
const units = rawUnits.filter(Boolean)
|
||||
if (units.length <= 1) {
|
||||
return units[0]
|
||||
}
|
||||
if (operator === '*')
|
||||
return simplify({
|
||||
numerators: units.flatMap((u) => u?.numerators ?? []),
|
||||
denominators: units.flatMap((u) => u?.denominators ?? []),
|
||||
})
|
||||
|
||||
if (operator === '-' || operator === '+') {
|
||||
return rawUnits.find((u) => u)
|
||||
}
|
||||
|
||||
return undefined
|
||||
}
|
||||
|
||||
const equals = <T>(a: T, b: T) => {
|
||||
if (Array.isArray(a) && Array.isArray(b)) {
|
||||
return a.length === b.length && a.every((_, i) => a[i] === b[i])
|
||||
} else {
|
||||
return a === b
|
||||
}
|
||||
}
|
||||
|
||||
export const removeOnce =
|
||||
<T>(element: T, eqFn: (a: T, b: T) => boolean = equals) =>
|
||||
(list: Array<T>): Array<T> => {
|
||||
const index = list.findIndex((e) => eqFn(e, element))
|
||||
return list.filter((_, i) => i !== index)
|
||||
}
|
||||
|
||||
const simplify = (
|
||||
unit: Unit,
|
||||
eqFn: (a: string, b: string) => boolean = equals
|
||||
): Unit =>
|
||||
[...unit.numerators, ...unit.denominators].reduce(
|
||||
({ numerators, denominators }, next) =>
|
||||
numerators.find((u) => eqFn(next, u)) &&
|
||||
denominators.find((u) => eqFn(next, u))
|
||||
? {
|
||||
numerators: removeOnce(next, eqFn)(numerators),
|
||||
denominators: removeOnce(next, eqFn)(denominators),
|
||||
}
|
||||
: { numerators, denominators },
|
||||
unit
|
||||
)
|
||||
|
||||
const convertTable: ConvertTable = {
|
||||
'mois/an': 12,
|
||||
'jour/an': 365,
|
||||
'jour/mois': 365 / 12,
|
||||
'trimestre/an': 4,
|
||||
'mois/trimestre': 3,
|
||||
'jour/trimestre': (365 / 12) * 3,
|
||||
'€/k€': 10 ** 3,
|
||||
'g/kg': 10 ** 3,
|
||||
'mg/g': 10 ** 3,
|
||||
'mg/kg': 10 ** 6,
|
||||
}
|
||||
function singleUnitConversionFactor(
|
||||
from: string,
|
||||
to: string
|
||||
): number | undefined {
|
||||
return (
|
||||
convertTable[`${to}/${from}`] ||
|
||||
(convertTable[`${from}/${to}`] && 1 / convertTable[`${from}/${to}`])
|
||||
)
|
||||
}
|
||||
function unitsConversionFactor(from: string[], to: string[]): number {
|
||||
let factor =
|
||||
100 **
|
||||
// Factor is mutliplied or divided 100 for each '%' in units
|
||||
(to.filter((unit) => unit === '%').length -
|
||||
from.filter((unit) => unit === '%').length)
|
||||
;[factor] = from.reduce(
|
||||
([value, toUnits], fromUnit) => {
|
||||
const index = toUnits.findIndex(
|
||||
(toUnit) => !!singleUnitConversionFactor(fromUnit, toUnit)
|
||||
)
|
||||
const factor = singleUnitConversionFactor(fromUnit, toUnits[index]) || 1
|
||||
return [
|
||||
value * factor,
|
||||
[...toUnits.slice(0, index + 1), ...toUnits.slice(index + 1)],
|
||||
]
|
||||
},
|
||||
[factor, to]
|
||||
)
|
||||
return factor
|
||||
}
|
||||
|
||||
export function convertUnit(
|
||||
from: Unit | undefined,
|
||||
to: Unit | undefined,
|
||||
value: number
|
||||
): number
|
||||
export function convertUnit(
|
||||
from: Unit | undefined,
|
||||
to: Unit | undefined,
|
||||
value: Evaluation<number>
|
||||
): Evaluation<number>
|
||||
export function convertUnit(
|
||||
from: Unit | undefined,
|
||||
to: Unit | undefined,
|
||||
value: number | Evaluation<number>
|
||||
) {
|
||||
if (!areUnitConvertible(from, to)) {
|
||||
throw new Error(
|
||||
`Impossible de convertir l'unité '${serializeUnit(
|
||||
from
|
||||
)}' en '${serializeUnit(to)}'`
|
||||
)
|
||||
}
|
||||
if (!value) {
|
||||
return value
|
||||
}
|
||||
if (from === undefined) {
|
||||
return value
|
||||
}
|
||||
const [fromSimplified, factorTo] = simplifyUnitWithValue(from || noUnit)
|
||||
const [toSimplified, factorFrom] = simplifyUnitWithValue(to || noUnit)
|
||||
return round(
|
||||
((value * factorTo) / factorFrom) *
|
||||
unitsConversionFactor(
|
||||
fromSimplified.numerators,
|
||||
toSimplified.numerators
|
||||
) *
|
||||
unitsConversionFactor(
|
||||
toSimplified.denominators,
|
||||
fromSimplified.denominators
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
const convertibleUnitClasses = unitClasses(convertTable)
|
||||
type unitClasses = Array<Set<string>>
|
||||
type ConvertTable = { readonly [index: string]: number }
|
||||
|
||||
// Reduce the convertTable provided by the user into a list of compatibles
|
||||
// classes.
|
||||
function unitClasses(convertTable: ConvertTable) {
|
||||
return Object.keys(convertTable).reduce(
|
||||
(classes: unitClasses, ratio: string) => {
|
||||
const [a, b] = ratio.split('/')
|
||||
const ia = classes.findIndex((units) => units.has(a))
|
||||
const ib = classes.findIndex((units) => units.has(b))
|
||||
if (ia > -1 && ib > -1 && ia !== ib) {
|
||||
throw Error(`Invalid ratio ${ratio}`)
|
||||
} else if (ia === -1 && ib === -1) {
|
||||
classes.push(new Set([a, b]))
|
||||
} else if (ia > -1) {
|
||||
classes[ia].add(b)
|
||||
} else if (ib > -1) {
|
||||
classes[ib].add(a)
|
||||
}
|
||||
return classes
|
||||
},
|
||||
[]
|
||||
)
|
||||
}
|
||||
|
||||
function areSameClass(a: string, b: string) {
|
||||
return (
|
||||
a === b ||
|
||||
convertibleUnitClasses.some(
|
||||
(unitsClass) => unitsClass.has(a) && unitsClass.has(b)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
function round(value: number) {
|
||||
return +value.toFixed(16)
|
||||
}
|
||||
export function simplifyUnit(unit: Unit): Unit {
|
||||
const { numerators, denominators } = simplify(unit, areSameClass)
|
||||
if (numerators.length && numerators.every((symb) => symb === '%')) {
|
||||
return { numerators: ['%'], denominators }
|
||||
}
|
||||
return removePercentages({ numerators, denominators })
|
||||
}
|
||||
function simplifyUnitWithValue(unit: Unit, value = 1): [Unit, number] {
|
||||
const factor = unitsConversionFactor(unit.numerators, unit.denominators)
|
||||
return [
|
||||
simplify(removePercentages(unit), areSameClass),
|
||||
value ? round(value * factor) : value,
|
||||
]
|
||||
}
|
||||
|
||||
const removePercentages = (unit: Unit): Unit => ({
|
||||
numerators: unit.numerators.filter((e) => e !== '%'),
|
||||
denominators: unit.denominators.filter((e) => e !== '%'),
|
||||
})
|
||||
|
||||
export function areUnitConvertible(a: Unit | undefined, b: Unit | undefined) {
|
||||
if (a == null || b == null) {
|
||||
return true
|
||||
}
|
||||
|
||||
const countByUnitClass = (units: Array<string>) =>
|
||||
units.reduce((counters, unit) => {
|
||||
const classIndex = convertibleUnitClasses.findIndex((unitClass) =>
|
||||
unitClass.has(unit)
|
||||
)
|
||||
const key = classIndex === -1 ? unit : '' + classIndex
|
||||
return { ...counters, [key]: 1 + (counters[key] ?? 0) }
|
||||
}, {})
|
||||
|
||||
const [numA, denomA, numB, denomB] = [
|
||||
a.numerators,
|
||||
a.denominators,
|
||||
b.numerators,
|
||||
b.denominators,
|
||||
].map(countByUnitClass)
|
||||
const uniq = <T>(arr: Array<T>): Array<T> => [...new Set(arr)]
|
||||
const unitClasses = [numA, denomA, numB, denomB].map(Object.keys).flat()
|
||||
return uniq(unitClasses).every(
|
||||
(unitClass) =>
|
||||
(numA[unitClass] || 0) - (denomA[unitClass] || 0) ===
|
||||
(numB[unitClass] || 0) - (denomB[unitClass] || 0) || unitClass === '%'
|
||||
)
|
||||
}
|
|
@ -1,2 +0,0 @@
|
|||
env:
|
||||
mocha: true
|
|
@ -1,66 +0,0 @@
|
|||
import { expect } from 'chai'
|
||||
import dedent from 'dedent-js'
|
||||
import { cyclesInDependenciesGraph } from '../source/AST/graph'
|
||||
|
||||
describe('Cyclic dependencies detectron 3000 ™', () => {
|
||||
it('should detect the trivial formule cycle', () => {
|
||||
const rules = dedent`
|
||||
a:
|
||||
formule: a + 1
|
||||
`
|
||||
const cycles = cyclesInDependenciesGraph(rules)
|
||||
expect(cycles).to.deep.equal([['a']])
|
||||
})
|
||||
|
||||
it('should detect nested and parallel formule cycles', () => {
|
||||
const rules = dedent`
|
||||
a:
|
||||
formule: b + 1
|
||||
b:
|
||||
formule: c + d + 1
|
||||
c:
|
||||
formule: a + 1
|
||||
d:
|
||||
formule: b + 1
|
||||
`
|
||||
const cycles = cyclesInDependenciesGraph(rules)
|
||||
expect(cycles).to.deep.equal([['a', 'b', 'c', 'd']])
|
||||
})
|
||||
|
||||
it('should not detect formule cycles due to parent dependency', () => {
|
||||
const rules = dedent`
|
||||
a:
|
||||
formule: b + 1
|
||||
a . b:
|
||||
formule: 3
|
||||
`
|
||||
const cycles = cyclesInDependenciesGraph(rules)
|
||||
expect(cycles).to.deep.equal([])
|
||||
})
|
||||
|
||||
it('should not detect cycles due to replacements', () => {
|
||||
const rules = dedent`
|
||||
a:
|
||||
formule: b + 1
|
||||
a . b:
|
||||
formule: 3
|
||||
a . c:
|
||||
remplace: b
|
||||
formule: a
|
||||
`
|
||||
const cycles = cyclesInDependenciesGraph(rules)
|
||||
expect(cycles).to.deep.equal([])
|
||||
})
|
||||
|
||||
it('should not detect cycles when résoudre référence circulaire', () => {
|
||||
const rules = dedent`
|
||||
fx:
|
||||
200 - x
|
||||
x:
|
||||
résoudre la référence circulaire: oui
|
||||
valeur: fx
|
||||
`
|
||||
const cycles = cyclesInDependenciesGraph(rules)
|
||||
expect(cycles).to.deep.equal([])
|
||||
})
|
||||
})
|
|
@ -1,19 +0,0 @@
|
|||
import { expect } from 'chai'
|
||||
import { getDifferenceInMonths } from '../source/date'
|
||||
|
||||
describe('Date : getDifferenceInMonths', () => {
|
||||
it('should compute the difference for one full month', () => {
|
||||
expect(getDifferenceInMonths('01/01/2020', '31/01/2020')).to.equal(1)
|
||||
})
|
||||
it('should compute the difference for one month and one day', () => {
|
||||
expect(getDifferenceInMonths('01/01/2020', '01/02/2020')).to.equal(
|
||||
1 + 1 / 29
|
||||
)
|
||||
})
|
||||
it('should compute the difference for 2 days between months', () => {
|
||||
expect(getDifferenceInMonths('31/01/2020', '01/02/2020')).to.approximately(
|
||||
1 / 31 + 1 / 29,
|
||||
0.000000000001
|
||||
)
|
||||
})
|
||||
})
|
|
@ -1,62 +0,0 @@
|
|||
import { expect } from 'chai'
|
||||
import { parseUnit } from '../source/units'
|
||||
import { formatValue, capitalise0 } from '../source/format'
|
||||
|
||||
describe('format engine values', () => {
|
||||
it('format currencies', () => {
|
||||
expect(formatValue(12, { displayedUnit: '€' })).to.equal('12 €')
|
||||
expect(formatValue(1200, { displayedUnit: '€' })).to.equal('1 200 €')
|
||||
expect(formatValue(12, { displayedUnit: '€', language: 'en' })).to.equal(
|
||||
'€12'
|
||||
)
|
||||
expect(formatValue(12.1, { displayedUnit: '€', language: 'en' })).to.equal(
|
||||
'€12.10'
|
||||
)
|
||||
expect(
|
||||
formatValue(12.123, { displayedUnit: '€', language: 'en' })
|
||||
).to.equal('€12.12')
|
||||
})
|
||||
|
||||
it('format percentages', () => {
|
||||
expect(formatValue(10, { displayedUnit: '%' })).to.equal('10 %')
|
||||
expect(formatValue(100, { displayedUnit: '%' })).to.equal('100 %')
|
||||
expect(formatValue(10.2, { displayedUnit: '%' })).to.equal('10,2 %')
|
||||
expect(
|
||||
formatValue({
|
||||
nodeValue: 441,
|
||||
unit: parseUnit('%.kgCO2e'),
|
||||
})
|
||||
).to.equal('4,41 kgCO2e')
|
||||
})
|
||||
|
||||
it('format values', () => {
|
||||
expect(formatValue(1200)).to.equal('1 200')
|
||||
})
|
||||
})
|
||||
|
||||
describe('Units handling', () => {
|
||||
it('format displayedUnit', () => {
|
||||
const formatUnit = (unit, count) => unit + (count > 1 ? 's' : '')
|
||||
expect(formatValue(1, { displayedUnit: 'jour', formatUnit })).to.equal(
|
||||
'1 jour'
|
||||
)
|
||||
expect(formatValue(2, { displayedUnit: 'jour', formatUnit })).to.equal(
|
||||
'2 jours'
|
||||
)
|
||||
expect(
|
||||
formatValue(
|
||||
{
|
||||
nodeValue: 7,
|
||||
unit: parseUnit('jour/semaine'),
|
||||
},
|
||||
{ formatUnit }
|
||||
)
|
||||
).to.equal('7 jours / semaine')
|
||||
})
|
||||
})
|
||||
|
||||
describe('capitalise0', function () {
|
||||
it('should turn the first character into its capital', function () {
|
||||
expect(capitalise0('salaire')).to.equal('Salaire')
|
||||
})
|
||||
})
|
|
@ -1,220 +0,0 @@
|
|||
import { expect } from 'chai'
|
||||
import dedent from 'dedent-js'
|
||||
import Engine from '../source/index'
|
||||
|
||||
describe('inversions', () => {
|
||||
it('should handle non inverted example', () => {
|
||||
const rules = dedent`
|
||||
net:
|
||||
formule:
|
||||
produit:
|
||||
assiette: brut
|
||||
taux: 77%
|
||||
|
||||
brut:
|
||||
unité: €
|
||||
`
|
||||
const result = new Engine(rules)
|
||||
.setSituation({ brut: 2300 })
|
||||
.evaluate('net')
|
||||
|
||||
expect(result.nodeValue).to.be.closeTo(1771, 0.001)
|
||||
})
|
||||
|
||||
it('should handle simple inversion', () => {
|
||||
const rules = dedent`
|
||||
net:
|
||||
formule:
|
||||
produit:
|
||||
assiette: brut
|
||||
taux: 77%
|
||||
|
||||
brut:
|
||||
formule:
|
||||
inversion numérique:
|
||||
unité: €
|
||||
avec:
|
||||
- net
|
||||
`
|
||||
const result = new Engine(rules)
|
||||
.setSituation({ net: '2000 €' })
|
||||
.evaluate('brut')
|
||||
|
||||
expect(result.nodeValue).to.be.closeTo(2000 / (77 / 100), 0.0001 * 2000)
|
||||
})
|
||||
|
||||
it('should handle inversion with value at 0', () => {
|
||||
const rules = dedent`
|
||||
net:
|
||||
formule:
|
||||
produit:
|
||||
assiette: brut
|
||||
taux: 77%
|
||||
|
||||
brut:
|
||||
formule:
|
||||
inversion numérique:
|
||||
unité: €
|
||||
avec:
|
||||
- net
|
||||
`
|
||||
const result = new Engine(rules)
|
||||
.setSituation({ net: '0 €' })
|
||||
.evaluate('brut')
|
||||
expect(result.nodeValue).to.be.closeTo(0, 0.0001)
|
||||
})
|
||||
|
||||
it('should ask the input of one of the possible inversions', () => {
|
||||
const rules = dedent`
|
||||
net:
|
||||
formule:
|
||||
produit:
|
||||
assiette: assiette
|
||||
variations:
|
||||
- si: cadre
|
||||
alors:
|
||||
taux: 80%
|
||||
- sinon:
|
||||
taux: 70%
|
||||
|
||||
brut:
|
||||
formule:
|
||||
inversion numérique:
|
||||
unité: €
|
||||
avec:
|
||||
- net
|
||||
cadre:
|
||||
assiette:
|
||||
formule: 67€ + brut
|
||||
|
||||
`
|
||||
const result = new Engine(rules).evaluate('brut')
|
||||
|
||||
expect(result.nodeValue).to.be.null
|
||||
expect(Object.keys(result.missingVariables)).to.include('brut')
|
||||
})
|
||||
|
||||
it('should handle inversions with missing variables', () => {
|
||||
const rules = dedent`
|
||||
net:
|
||||
formule:
|
||||
produit:
|
||||
assiette: assiette
|
||||
variations:
|
||||
- si: cadre
|
||||
alors:
|
||||
taux: 80%
|
||||
- sinon:
|
||||
taux: 70%
|
||||
|
||||
brut:
|
||||
formule:
|
||||
inversion numérique:
|
||||
unité: €
|
||||
avec:
|
||||
- net
|
||||
cadre:
|
||||
assiette:
|
||||
formule:
|
||||
somme:
|
||||
- 1200
|
||||
- brut
|
||||
- taxeOne
|
||||
taxeOne:
|
||||
non applicable si: cadre
|
||||
formule: taxe + taxe
|
||||
taxe:
|
||||
formule:
|
||||
produit:
|
||||
assiette: 1200 €
|
||||
variations:
|
||||
- si: cadre
|
||||
alors:
|
||||
taux: 80%
|
||||
- sinon:
|
||||
taux: 70%
|
||||
`
|
||||
const result = new Engine(rules)
|
||||
.setSituation({ net: '2000 €' })
|
||||
.evaluate('brut')
|
||||
expect(result.nodeValue).to.be.null
|
||||
expect(Object.keys(result.missingVariables)).to.include('cadre')
|
||||
})
|
||||
|
||||
it("shouldn't report a missing salary if another salary was input", () => {
|
||||
const rules = dedent`
|
||||
net:
|
||||
formule:
|
||||
produit:
|
||||
assiette: assiette
|
||||
taux:
|
||||
variations:
|
||||
- si: cadre
|
||||
alors: 80%
|
||||
- sinon: 70%
|
||||
|
||||
total:
|
||||
formule:
|
||||
produit:
|
||||
assiette: assiette
|
||||
taux: 150%
|
||||
|
||||
brut:
|
||||
formule:
|
||||
inversion numérique:
|
||||
unité: €
|
||||
avec:
|
||||
- net
|
||||
- total
|
||||
|
||||
cadre:
|
||||
|
||||
assiette:
|
||||
formule: 67 + brut
|
||||
|
||||
`
|
||||
const result = new Engine(rules)
|
||||
.setSituation({ net: '2000 €', cadre: 'oui' })
|
||||
.evaluate('total')
|
||||
expect(result.nodeValue).to.be.closeTo(3750, 1)
|
||||
expect(Object.keys(result.missingVariables)).to.be.empty
|
||||
})
|
||||
|
||||
it('complex inversion with composantes', () => {
|
||||
const rules = dedent`
|
||||
net:
|
||||
formule:
|
||||
produit:
|
||||
assiette: 67 + brut
|
||||
taux: 80%
|
||||
|
||||
cotisation:
|
||||
formule:
|
||||
produit:
|
||||
assiette: 67 + brut
|
||||
composantes:
|
||||
- attributs:
|
||||
nom: employeur
|
||||
taux: 100%
|
||||
- attributs:
|
||||
nom: salarié
|
||||
taux: 50%
|
||||
|
||||
total:
|
||||
formule: cotisation . employeur + cotisation . salarié
|
||||
|
||||
brut:
|
||||
formule:
|
||||
inversion numérique:
|
||||
unité: €
|
||||
avec:
|
||||
- net
|
||||
- total
|
||||
`
|
||||
const result = new Engine(rules)
|
||||
.setSituation({ net: '2000 €' })
|
||||
.evaluate('total')
|
||||
expect(result.nodeValue).to.be.closeTo(3750, 1)
|
||||
expect(Object.keys(result.missingVariables)).to.be.empty
|
||||
})
|
||||
})
|
|
@ -1,89 +0,0 @@
|
|||
import { expect } from 'chai'
|
||||
import Engine from '../source/index'
|
||||
import co2 from './rules/co2.yaml'
|
||||
|
||||
describe('library', function () {
|
||||
it('should let the user define its own rule', function () {
|
||||
let rules = `
|
||||
yo:
|
||||
formule: 200
|
||||
ya:
|
||||
formule: yo + 1
|
||||
yi:
|
||||
formule: yo + 2
|
||||
`
|
||||
let engine = new Engine(rules)
|
||||
|
||||
expect(engine.evaluate('ya').nodeValue).to.equal(201)
|
||||
expect(engine.evaluate('yi').nodeValue).to.equal(202)
|
||||
})
|
||||
|
||||
it('should let the user define a simplified revenue tax system', function () {
|
||||
let rules = `
|
||||
revenu imposable:
|
||||
question: Quel est votre revenu imposable ?
|
||||
unité: €
|
||||
|
||||
revenu abattu:
|
||||
formule:
|
||||
valeur: revenu imposable
|
||||
abattement: 10%
|
||||
|
||||
impôt sur le revenu:
|
||||
formule:
|
||||
barème:
|
||||
assiette: revenu abattu
|
||||
tranches:
|
||||
- taux: 0%
|
||||
plafond: 9807 €
|
||||
- taux: 14%
|
||||
plafond: 27086 €
|
||||
- taux: 30%
|
||||
plafond: 72617 €
|
||||
- taux: 41%
|
||||
plafond: 153783 €
|
||||
- taux: 45%
|
||||
|
||||
impôt sur le revenu à payer:
|
||||
formule:
|
||||
valeur: impôt sur le revenu
|
||||
abattement:
|
||||
valeur: 1177 - (75% * impôt sur le revenu)
|
||||
plancher: 0
|
||||
`
|
||||
|
||||
let engine = new Engine(rules)
|
||||
engine.setSituation({
|
||||
'revenu imposable': '48000',
|
||||
})
|
||||
let value = engine.evaluate('impôt sur le revenu à payer')
|
||||
expect(value.nodeValue).to.equal(7253.26)
|
||||
})
|
||||
|
||||
it('should let the user define a rule base on a completely different subject', function () {
|
||||
let engine = new Engine(co2)
|
||||
engine.setSituation({
|
||||
'nombre de douches': 30,
|
||||
'chauffage . type': "'gaz'",
|
||||
'durée de la douche': 10,
|
||||
})
|
||||
let value = engine.evaluate('douche . impact')
|
||||
expect(value.nodeValue).to.be.within(20, 21)
|
||||
})
|
||||
|
||||
it('should let the user reference rules in the situation', function () {
|
||||
let rules = `
|
||||
referenced in situation:
|
||||
formule: 200
|
||||
overwrited in situation:
|
||||
formule: 100
|
||||
result:
|
||||
formule: overwrited in situation + 22
|
||||
`
|
||||
let engine = new Engine(rules)
|
||||
engine.setSituation({
|
||||
'overwrited in situation': 'referenced in situation',
|
||||
})
|
||||
expect(engine.evaluate('result').nodeValue).to.equal(222)
|
||||
})
|
||||
})
|
|
@ -1,7 +0,0 @@
|
|||
let directoryLoaderFunction = require.context('./mécanismes', true, /.yaml$/)
|
||||
|
||||
let items = directoryLoaderFunction
|
||||
.keys()
|
||||
.map((key) => [key.replace(/\.\/|\.yaml/g, ''), directoryLoaderFunction(key)])
|
||||
|
||||
export default items
|
|
@ -1,66 +0,0 @@
|
|||
/*
|
||||
Les mécanismes sont testés dans mécanismes/ comme le sont les variables
|
||||
directement dans la base Publicodes. On construit dans chaque fichier une base
|
||||
Publicodes autonome, dans laquelle intervient le mécanisme à tester, puis on
|
||||
teste idéalement tous ses comportements sans en faire intervenir d'autres.
|
||||
*/
|
||||
|
||||
import { expect } from 'chai'
|
||||
import Engine from '../source/index'
|
||||
import { parseUnit } from '../source/units'
|
||||
import testSuites from './load-mecanism-tests'
|
||||
testSuites.forEach(([suiteName, suite]) => {
|
||||
const engine = new Engine(suite)
|
||||
|
||||
describe(`Mécanisme ${suiteName}`, () => {
|
||||
Object.entries(suite)
|
||||
.filter(([, rule]) => rule?.exemples)
|
||||
.forEach(([name, test]) => {
|
||||
const { exemples, 'unité attendue': unit } = test
|
||||
const exemplesArray = Array.isArray(exemples) ? exemples : [exemples]
|
||||
exemplesArray.forEach(
|
||||
(
|
||||
{
|
||||
nom: testName,
|
||||
situation,
|
||||
'unité par défaut': defaultUnit,
|
||||
'valeur attendue': valeur,
|
||||
'variables manquantes': expectedMissing,
|
||||
},
|
||||
i
|
||||
) => {
|
||||
it(
|
||||
name +
|
||||
(testName
|
||||
? ` [${testName}]`
|
||||
: exemples.length > 1
|
||||
? ` (${i + 1})`
|
||||
: ''),
|
||||
() => {
|
||||
const result = engine
|
||||
.setSituation(situation ?? {})
|
||||
.evaluate(name, {
|
||||
unit: defaultUnit,
|
||||
})
|
||||
|
||||
if (typeof valeur === 'number') {
|
||||
expect(result.nodeValue).to.be.closeTo(valeur, 0.001)
|
||||
} else if (valeur !== undefined) {
|
||||
expect(result.nodeValue).to.be.deep.eq(valeur)
|
||||
}
|
||||
if (expectedMissing) {
|
||||
expect(Object.keys(result.missingVariables)).to.eql(
|
||||
expectedMissing
|
||||
)
|
||||
}
|
||||
if (unit) {
|
||||
expect(result.unit).not.to.be.equal(undefined)
|
||||
expect(result.unit).to.deep.equal(parseUnit(unit))
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
|
@ -1,363 +0,0 @@
|
|||
import { expect } from 'chai'
|
||||
import Engine from '../source/index'
|
||||
import { parse } from 'yaml'
|
||||
|
||||
describe('Missing variables', function () {
|
||||
it('should identify missing variables', function () {
|
||||
// Rules in tests can be expressed in YAML like to for more clarity than JS objects
|
||||
const rawRules = parse(`
|
||||
ko: oui
|
||||
sum: oui
|
||||
sum . startHere:
|
||||
formule: 2
|
||||
non applicable si: sum . evt . ko
|
||||
sum . evt:
|
||||
formule:
|
||||
une possibilité:
|
||||
- ko
|
||||
titre: Truc
|
||||
question: '?'
|
||||
sum . evt . ko:
|
||||
`)
|
||||
|
||||
const result = Object.keys(
|
||||
new Engine(rawRules).evaluate('sum . startHere').missingVariables
|
||||
)
|
||||
|
||||
expect(result).to.include('sum . evt')
|
||||
})
|
||||
|
||||
it('should identify missing variables mentioned in expressions', function () {
|
||||
const rawRules = {
|
||||
sum: 'oui',
|
||||
'sum . evt': 'oui',
|
||||
'sum . startHere': {
|
||||
formule: 2,
|
||||
'non applicable si': 'evt . nyet > evt . nope',
|
||||
},
|
||||
'sum . evt . nope': {},
|
||||
'sum . evt . nyet': {},
|
||||
}
|
||||
const result = Object.keys(
|
||||
new Engine(rawRules).evaluate('sum . startHere').missingVariables
|
||||
)
|
||||
|
||||
expect(result).to.include('sum . evt . nyet')
|
||||
expect(result).to.include('sum . evt . nope')
|
||||
})
|
||||
|
||||
it('should ignore missing variables in the formula if not applicable', function () {
|
||||
const rawRules = {
|
||||
sum: 'oui',
|
||||
'sum . startHere': {
|
||||
formule: 'trois',
|
||||
'non applicable si': '3 > 2',
|
||||
},
|
||||
'sum . trois': {},
|
||||
}
|
||||
const result = Object.keys(
|
||||
new Engine(rawRules).evaluate('sum . startHere').missingVariables
|
||||
)
|
||||
|
||||
expect(result).to.be.empty
|
||||
})
|
||||
|
||||
it('should not report missing variables when "one of these" short-circuits', function () {
|
||||
const rawRules = {
|
||||
sum: 'oui',
|
||||
'sum . startHere': {
|
||||
formule: 'trois',
|
||||
'non applicable si': {
|
||||
'une de ces conditions': ['3 > 2', 'trois'],
|
||||
},
|
||||
},
|
||||
'sum . trois': {},
|
||||
}
|
||||
const result = Object.keys(
|
||||
new Engine(rawRules).evaluate('sum . startHere').missingVariables
|
||||
)
|
||||
|
||||
expect(result).to.be.empty
|
||||
})
|
||||
|
||||
it('should report "une possibilité" as a missing variable even though it has a formula', function () {
|
||||
const rawRules = {
|
||||
top: 'oui',
|
||||
ko: 'oui',
|
||||
'top . startHere': { formule: 'trois' },
|
||||
'top . trois': {
|
||||
formule: { 'une possibilité': ['ko'] },
|
||||
},
|
||||
}
|
||||
const result = Object.keys(
|
||||
new Engine(rawRules).evaluate('top . startHere').missingVariables
|
||||
)
|
||||
|
||||
expect(result).to.include('top . trois')
|
||||
})
|
||||
|
||||
it('should not report missing variables when "une possibilité" is inapplicable', function () {
|
||||
const rawRules = {
|
||||
top: 'oui',
|
||||
ko: 'oui',
|
||||
'top . startHere': { formule: 'trois' },
|
||||
'top . trois': {
|
||||
formule: { 'une possibilité': ['ko'] },
|
||||
'non applicable si': 'oui',
|
||||
},
|
||||
}
|
||||
const result = Object.keys(
|
||||
new Engine(rawRules).evaluate('top . startHere').missingVariables
|
||||
)
|
||||
|
||||
expect(result).to.be.empty
|
||||
null
|
||||
})
|
||||
|
||||
it('should not report missing variables when "une possibilité" was answered', function () {
|
||||
const rawRules = {
|
||||
top: 'oui',
|
||||
ko: 'oui',
|
||||
'top . startHere': { formule: 'trois' },
|
||||
'top . trois': {
|
||||
formule: { 'une possibilité': ['ko'] },
|
||||
},
|
||||
}
|
||||
const result = Object.keys(
|
||||
new Engine(rawRules)
|
||||
.setSituation({ 'top . trois': "'ko'" })
|
||||
.evaluate('top . startHere').missingVariables
|
||||
)
|
||||
|
||||
expect(result).to.be.empty
|
||||
})
|
||||
|
||||
it('should report missing variables in simple variations', function () {
|
||||
const rawRules = parse(`
|
||||
|
||||
somme: a + b
|
||||
a: 10
|
||||
b:
|
||||
formule:
|
||||
variations:
|
||||
- si: a > 100
|
||||
alors: c
|
||||
- sinon: 0
|
||||
c:
|
||||
question: Alors ?`)
|
||||
const result = Object.keys(
|
||||
new Engine(rawRules).evaluate('somme').missingVariables
|
||||
)
|
||||
|
||||
expect(result).to.have.lengthOf(0)
|
||||
})
|
||||
|
||||
// TODO : réparer ce test
|
||||
it('should report missing variables in variations', function () {
|
||||
const rawRules = parse(`
|
||||
startHere:
|
||||
formule:
|
||||
somme:
|
||||
- variations
|
||||
variations:
|
||||
formule:
|
||||
variations:
|
||||
- si: dix
|
||||
alors:
|
||||
barème:
|
||||
assiette: 2008
|
||||
multiplicateur: deux
|
||||
tranches:
|
||||
- plafond: 1
|
||||
taux: 0.1
|
||||
- plafond: 2
|
||||
taux: trois
|
||||
- taux: 10
|
||||
- si: 3 > 4
|
||||
alors:
|
||||
barème:
|
||||
assiette: 2008
|
||||
multiplicateur: quatre
|
||||
tranches:
|
||||
- plafond: 1
|
||||
taux: 0.1
|
||||
- plafond: 2
|
||||
taux: 1.8
|
||||
- au-dessus de: 2
|
||||
taux: 10
|
||||
|
||||
dix: {}
|
||||
deux: {}
|
||||
trois: {}
|
||||
quatre: {}
|
||||
|
||||
`)
|
||||
const result = Object.keys(
|
||||
new Engine(rawRules).evaluate('startHere').missingVariables
|
||||
)
|
||||
|
||||
expect(result).to.include('dix')
|
||||
expect(result).to.include('deux')
|
||||
expect(result).to.include('trois')
|
||||
expect(result).not.to.include('quatre')
|
||||
})
|
||||
})
|
||||
|
||||
describe('nextSteps', function () {
|
||||
it('should generate questions for simple situations', function () {
|
||||
const rawRules = {
|
||||
top: 'oui',
|
||||
'top . sum': { formule: 'deux' },
|
||||
'top . deux': {
|
||||
'non applicable si': 'top . sum . evt',
|
||||
formule: 2,
|
||||
},
|
||||
'top . sum . evt': {
|
||||
titre: 'Truc',
|
||||
question: '?',
|
||||
},
|
||||
}
|
||||
|
||||
const result = Object.keys(
|
||||
new Engine(rawRules).evaluate('top . sum').missingVariables
|
||||
)
|
||||
|
||||
expect(result).to.have.lengthOf(1)
|
||||
expect(result[0]).to.equal('top . sum . evt')
|
||||
})
|
||||
it('should generate questions', function () {
|
||||
const rawRules = {
|
||||
top: 'oui',
|
||||
'top . sum': { formule: 'deux' },
|
||||
'top . deux': {
|
||||
formule: 'sum . evt',
|
||||
},
|
||||
'top . sum . evt': {
|
||||
question: '?',
|
||||
},
|
||||
}
|
||||
|
||||
const result = Object.keys(
|
||||
new Engine(rawRules).evaluate('top . sum').missingVariables
|
||||
)
|
||||
|
||||
expect(result).to.have.lengthOf(1)
|
||||
expect(result[0]).to.equal('top . sum . evt')
|
||||
})
|
||||
|
||||
it('should generate questions with more intricate situation', function () {
|
||||
const rawRules = {
|
||||
top: 'oui',
|
||||
'top . sum': { formule: { somme: [2, 'deux'] } },
|
||||
'top . deux': {
|
||||
formule: 2,
|
||||
'non applicable si': "top . sum . evt = 'ko'",
|
||||
},
|
||||
'top . sum . evt': {
|
||||
formule: { 'une possibilité': ['ko'] },
|
||||
titre: 'Truc',
|
||||
question: '?',
|
||||
},
|
||||
'top . sum . evt . ko': {},
|
||||
}
|
||||
const result = Object.keys(
|
||||
new Engine(rawRules).evaluate('top . sum').missingVariables
|
||||
)
|
||||
|
||||
expect(result).to.eql(['top . sum . evt'])
|
||||
})
|
||||
|
||||
it("Parent's other descendands in sums should not be included as missing variables", function () {
|
||||
// See https://github.com/betagouv/publicodes/issues/33
|
||||
const rawRules = parse(`
|
||||
transport:
|
||||
somme:
|
||||
- voiture
|
||||
- avion
|
||||
|
||||
transport . voiture:
|
||||
formule: empreinte * km
|
||||
|
||||
transport . voiture . empreinte: 0.12
|
||||
transport . voiture . km:
|
||||
question: COMBIENKM
|
||||
par défaut: 1000
|
||||
|
||||
transport . avion:
|
||||
applicable si: usager
|
||||
formule: empreinte * km
|
||||
|
||||
transport . avion . km:
|
||||
question: COMBIENKM
|
||||
par défaut: 10000
|
||||
|
||||
transport . avion . empreinte: 0.300
|
||||
|
||||
transport . avion . usager:
|
||||
question: Prenez-vous l'avion ?
|
||||
par défaut: oui
|
||||
`)
|
||||
const result = Object.keys(
|
||||
new Engine(rawRules).evaluate('transport . avion').missingVariables
|
||||
)
|
||||
|
||||
expect(result).deep.to.equal([
|
||||
'transport . avion . km',
|
||||
'transport . avion . usager',
|
||||
])
|
||||
expect(result).to.have.lengthOf(2)
|
||||
})
|
||||
it("Parent's other descendands in sums should not be included as missing variables - 2", function () {
|
||||
// See https://github.com/betagouv/publicodes/issues/33
|
||||
const rawRules = parse(`
|
||||
avion:
|
||||
question: prenez-vous l'avion ?
|
||||
par défaut: oui
|
||||
|
||||
avion . impact:
|
||||
formule:
|
||||
somme:
|
||||
- au sol
|
||||
- en vol
|
||||
|
||||
avion . impact . en vol:
|
||||
question: Combien de temps passé en vol ?
|
||||
par défaut: 10
|
||||
|
||||
avion . impact . au sol: 5
|
||||
`)
|
||||
const result = Object.keys(
|
||||
new Engine(rawRules).evaluate('avion . impact . au sol').missingVariables
|
||||
)
|
||||
|
||||
expect(result).deep.to.equal(['avion'])
|
||||
expect(result).to.have.lengthOf(1)
|
||||
})
|
||||
|
||||
it("Parent's other descendands in sums in applicability should be included as missing variables", function () {
|
||||
// See https://github.com/betagouv/publicodes/issues/33
|
||||
const rawRules = parse(`
|
||||
a:
|
||||
applicable si: d > 3
|
||||
valeur: oui
|
||||
|
||||
d:
|
||||
formule:
|
||||
somme:
|
||||
- e
|
||||
- 8
|
||||
|
||||
e:
|
||||
question: Vous venez à combien à la soirée ?
|
||||
par défaut: 3
|
||||
|
||||
a . b: 20 + 9
|
||||
`)
|
||||
const result = Object.keys(
|
||||
new Engine(rawRules).evaluate('a . b').missingVariables
|
||||
)
|
||||
|
||||
expect(result).deep.to.equal(['e'])
|
||||
expect(result).to.have.lengthOf(1)
|
||||
})
|
||||
})
|
|
@ -1,28 +0,0 @@
|
|||
montant:
|
||||
unité: €
|
||||
|
||||
montant abattu:
|
||||
unité: €
|
||||
formule:
|
||||
valeur: montant
|
||||
abattement: 20507
|
||||
exemples:
|
||||
- situation:
|
||||
montant: 10000
|
||||
valeur attendue: 0
|
||||
- situation:
|
||||
montant: 80000
|
||||
valeur attendue: 59493
|
||||
|
||||
montant abattu en pourcentage:
|
||||
unité: €
|
||||
formule:
|
||||
valeur: montant
|
||||
abattement: 15%
|
||||
exemples:
|
||||
- situation:
|
||||
montant: 10000
|
||||
valeur attendue: 8500
|
||||
- situation:
|
||||
montant: 80000
|
||||
valeur attendue: 68000
|
|
@ -1,29 +0,0 @@
|
|||
statut cadre:
|
||||
|
||||
prévoyance obligatoire cadre:
|
||||
applicable si: statut cadre
|
||||
formule:
|
||||
produit:
|
||||
assiette: 1500
|
||||
taux: 1.5%
|
||||
exemples:
|
||||
- nom: Applicabilité
|
||||
situation:
|
||||
statut cadre: oui
|
||||
valeur attendue: 22.5
|
||||
- nom: Non Applicabilité
|
||||
situation:
|
||||
statut cadre: non
|
||||
valeur attendue: false
|
||||
|
||||
variable:
|
||||
par défaut: oui
|
||||
applicable comme mécanisme chainé:
|
||||
formule:
|
||||
applicable si: variable
|
||||
valeur: 5
|
||||
exemples:
|
||||
- valeur attendue: 5
|
||||
- situation:
|
||||
variable: non
|
||||
valeur attendue: false
|
|
@ -1,86 +0,0 @@
|
|||
arrondi oui:
|
||||
formule:
|
||||
valeur: 30.4167 jours
|
||||
arrondi: oui
|
||||
exemples:
|
||||
- valeur attendue: 30
|
||||
unité attendue: jours
|
||||
|
||||
arrondi non:
|
||||
formule:
|
||||
valeur: 30.4167 jours
|
||||
arrondi: non
|
||||
exemples:
|
||||
- valeur attendue: 30.4167
|
||||
|
||||
arrondi décimales:
|
||||
formule:
|
||||
valeur: 30.4167 jours
|
||||
arrondi: 2 décimales
|
||||
exemples:
|
||||
- valeur attendue: 30.42
|
||||
|
||||
demie part:
|
||||
formule:
|
||||
valeur: 0.5 * 100.2€
|
||||
arrondi: oui
|
||||
exemples:
|
||||
- valeur attendue: 50
|
||||
|
||||
demie part avec pourcentage:
|
||||
formule:
|
||||
valeur: 50% * 100.2€
|
||||
arrondi: oui
|
||||
exemples:
|
||||
- valeur attendue: 50
|
||||
|
||||
arrondi de pourcentage:
|
||||
formule:
|
||||
valeur: 50.5%
|
||||
arrondi: oui
|
||||
exemples:
|
||||
- valeur attendue: 51
|
||||
unité attendue: '%'
|
||||
|
||||
cotisation retraite:
|
||||
|
||||
Arrondi:
|
||||
formule:
|
||||
valeur: cotisation retraite
|
||||
arrondi: oui
|
||||
|
||||
exemples:
|
||||
- nom: arrondi en dessous
|
||||
situation:
|
||||
cotisation retraite: 1200.21
|
||||
valeur attendue: 1200
|
||||
- nom: arrondi au-dessous
|
||||
situation:
|
||||
cotisation retraite: 1200.50
|
||||
valeur attendue: 1201
|
||||
|
||||
nombre de décimales:
|
||||
|
||||
Arrondi avec precision:
|
||||
formule:
|
||||
valeur: cotisation retraite
|
||||
arrondi: nombre de décimales
|
||||
exemples:
|
||||
- nom: pas de décimales
|
||||
situation:
|
||||
cotisation retraite: 1200.21
|
||||
nombre de décimales: 0
|
||||
valeur attendue: 1200
|
||||
- nom: deux décimales
|
||||
situation:
|
||||
cotisation retraite: 1200.21
|
||||
nombre de décimales: 2
|
||||
valeur attendue: 1200.21
|
||||
|
||||
arrondi avec conversion d'unités:
|
||||
formule:
|
||||
valeur: 12.5 €/mois
|
||||
unité: €/an
|
||||
arrondi: oui
|
||||
exemples:
|
||||
- valeur attendue: 150
|
|
@ -1,141 +0,0 @@
|
|||
assiette:
|
||||
unité: €/mois
|
||||
|
||||
base:
|
||||
unité: €/mois
|
||||
|
||||
Barème en taux marginaux:
|
||||
formule:
|
||||
barème:
|
||||
assiette: assiette
|
||||
multiplicateur: base
|
||||
tranches:
|
||||
- taux: 4.65%
|
||||
plafond: 1
|
||||
- taux: 3%
|
||||
plafond: 3
|
||||
- taux: 1%
|
||||
unité attendue: €/mois
|
||||
exemples:
|
||||
- nom: 'petite assiette'
|
||||
situation:
|
||||
assiette: 3000
|
||||
base: 5000
|
||||
valeur attendue: 139.5
|
||||
- nom: 'moyenne assiette'
|
||||
situation:
|
||||
assiette: 6000
|
||||
base: 5000
|
||||
valeur attendue: 262.5
|
||||
- nom: 'grande assiette'
|
||||
situation:
|
||||
assiette: 30000
|
||||
base: 5000
|
||||
valeur attendue: 682.5
|
||||
|
||||
Barème à composantes:
|
||||
formule:
|
||||
barème:
|
||||
assiette: assiette
|
||||
multiplicateur: base
|
||||
composantes:
|
||||
- tranches:
|
||||
- taux: 2%
|
||||
plafond: 1
|
||||
- taux: 0%
|
||||
- tranches:
|
||||
- taux: 9%
|
||||
plafond: 2
|
||||
- taux: 29%
|
||||
exemples:
|
||||
- nom:
|
||||
situation:
|
||||
assiette: 12000
|
||||
base: 5000
|
||||
valeur attendue: 1580
|
||||
unité attendue: €/mois
|
||||
|
||||
ma condition:
|
||||
|
||||
taux variable:
|
||||
formule:
|
||||
variations:
|
||||
- si: ma condition
|
||||
alors: 29%
|
||||
- sinon: 56%
|
||||
unité attendue: '%'
|
||||
exemples: []
|
||||
|
||||
deuxième barème:
|
||||
titre: Barème à taux variable
|
||||
unité: €/mois
|
||||
formule:
|
||||
barème:
|
||||
assiette: assiette
|
||||
multiplicateur: base
|
||||
tranches:
|
||||
- taux: taux variable
|
||||
plafond: 1
|
||||
- taux: 90%
|
||||
|
||||
unité attendue: '€/mois'
|
||||
|
||||
exemples:
|
||||
- nom: taux faible
|
||||
situation:
|
||||
assiette: 200
|
||||
base: 100
|
||||
ma condition: oui
|
||||
valeur attendue: 119
|
||||
- nom: taux fort
|
||||
situation:
|
||||
assiette: 200
|
||||
base: 100
|
||||
ma condition: non
|
||||
valeur attendue: 146
|
||||
- nom: assiette manquante
|
||||
situation:
|
||||
base: 100
|
||||
ma condition: oui
|
||||
variables manquantes:
|
||||
- assiette
|
||||
- nom: condition manquante
|
||||
situation:
|
||||
assiette: 40
|
||||
base: 100
|
||||
variables manquantes:
|
||||
- ma condition
|
||||
|
||||
tranche 1:
|
||||
formule: 100 €/mois
|
||||
tranche 2:
|
||||
unité: €/an
|
||||
tranches variables:
|
||||
formule:
|
||||
barème:
|
||||
assiette: assiette
|
||||
tranches:
|
||||
- taux: 10%
|
||||
plafond: tranche 1
|
||||
- taux: 50%
|
||||
plafond: tranche 2
|
||||
exemples:
|
||||
- nom: tranche 2 manquante non active
|
||||
situation:
|
||||
assiette: 40
|
||||
valeur attendue: 4
|
||||
- nom: tranche 2 manquante active
|
||||
situation:
|
||||
assiette: 200
|
||||
variables manquantes:
|
||||
- tranche 2
|
||||
- nom: tranche 2 active
|
||||
situation:
|
||||
assiette: 200
|
||||
tranche 2: 12000
|
||||
valeur attendue: 60 # 10% * 100 + 50% * 100
|
||||
- nom: tranche 2 dépassée
|
||||
situation:
|
||||
assiette: 2000
|
||||
tranche 2: 12000
|
||||
valeur attendue: 460 # 10% * 100 + 50% * 900
|
|
@ -1,19 +0,0 @@
|
|||
Composantes:
|
||||
formule: impôts
|
||||
unité attendue: crédits
|
||||
exemples:
|
||||
- nom:
|
||||
situation:
|
||||
valeur attendue: 10
|
||||
|
||||
richesse:
|
||||
unité: crédits
|
||||
formule: 100
|
||||
|
||||
impôts:
|
||||
formule:
|
||||
produit:
|
||||
assiette: richesse
|
||||
composantes:
|
||||
- taux: 8%
|
||||
- taux: 2%
|
|
@ -1,201 +0,0 @@
|
|||
# This is not a mecanism test, but we make use of the simplicity of declaring tests in YAML, only available for mecanisms for now
|
||||
|
||||
douches par mois:
|
||||
question: Combien prenez-vous de douches par mois ?
|
||||
unité: douche/mois
|
||||
|
||||
Conversion de reference:
|
||||
formule: douches par mois
|
||||
unité: douche/an
|
||||
exemples:
|
||||
- situation:
|
||||
douches par mois: 30
|
||||
valeur attendue: 360
|
||||
|
||||
Conversion de reference 2:
|
||||
unité: douche/an
|
||||
formule: douches par mois
|
||||
exemples:
|
||||
- situation:
|
||||
douches par mois: 30
|
||||
valeur attendue: 360
|
||||
|
||||
Conversion de variable:
|
||||
formule: 1.5 kCo2/douche * douches par mois
|
||||
exemples:
|
||||
- situation:
|
||||
douches par mois: 30
|
||||
valeur attendue: 45
|
||||
unité attendue: kCo2/mois
|
||||
|
||||
Conversion de variable et expressions:
|
||||
unité: kCo2/an
|
||||
formule: 1 kCo2/douche * 10 douche/mois
|
||||
exemples:
|
||||
- valeur attendue: 120
|
||||
|
||||
Conversion de pourcentage:
|
||||
unité: €/an
|
||||
formule: 1000€ * 1% /mois
|
||||
exemples:
|
||||
- valeur attendue: 120
|
||||
|
||||
Conversion en pourcentage:
|
||||
unité: '%'
|
||||
formule: 28h / 35h
|
||||
exemples:
|
||||
- valeur attendue: 80
|
||||
|
||||
Conversion dans un mécanisme:
|
||||
unité: €/an
|
||||
formule:
|
||||
le minimum de:
|
||||
- 100 €/mois
|
||||
- 1120 €/an
|
||||
exemples:
|
||||
- valeur attendue: 1120
|
||||
|
||||
assiette mensuelle:
|
||||
unité: €/mois
|
||||
|
||||
Conversion de mécanisme 1:
|
||||
unité: €/an
|
||||
formule:
|
||||
barème:
|
||||
assiette: assiette mensuelle
|
||||
tranches:
|
||||
- taux: 4.65%
|
||||
plafond: 30000 €/an
|
||||
- taux: 3%
|
||||
plafond: 90000 €/an
|
||||
- taux: 1%
|
||||
|
||||
exemples:
|
||||
- situation:
|
||||
assiette mensuelle: 3000
|
||||
valeur attendue: 1575
|
||||
|
||||
assiette annuelle:
|
||||
unité: €/an
|
||||
|
||||
Conversion de mécanisme 2:
|
||||
formule:
|
||||
barème:
|
||||
assiette: assiette annuelle
|
||||
tranches:
|
||||
- taux: 4.65%
|
||||
plafond: 2500 €/mois
|
||||
- taux: 3%
|
||||
plafond: 7500 €/mois
|
||||
- taux: 1%
|
||||
unité: €/mois
|
||||
exemples:
|
||||
- situation:
|
||||
assiette annuelle: 36000
|
||||
valeur attendue: 131.25
|
||||
|
||||
Conversion dans une expression:
|
||||
unité: €/an
|
||||
formule: 80 €/mois + 1120 €/an + 20 €/mois
|
||||
exemples:
|
||||
- valeur attendue: 2320
|
||||
|
||||
Conversion dans une comparaison:
|
||||
formule: 100€/mois = 1.2k€/an
|
||||
exemples:
|
||||
- valeur attendue: true
|
||||
|
||||
mutuelle:
|
||||
formule: 30 €/mois
|
||||
|
||||
retraite:
|
||||
formule:
|
||||
produit:
|
||||
assiette: assiette annuelle
|
||||
taux: 10%
|
||||
plafond: 12 k€/an
|
||||
|
||||
Conversion dans une somme compliquée:
|
||||
formule:
|
||||
somme:
|
||||
- mutuelle
|
||||
- retraite
|
||||
unité: €/mois
|
||||
exemples:
|
||||
- situation:
|
||||
assiette annuelle: 20000
|
||||
valeur attendue: 130
|
||||
|
||||
maladie:
|
||||
formule:
|
||||
produit:
|
||||
assiette: assiette annuelle
|
||||
composantes:
|
||||
- attributs:
|
||||
nom: employeur
|
||||
taux: 15%
|
||||
- attributs:
|
||||
nom: salarié
|
||||
taux: 5%
|
||||
plafond: 1000 €/mois
|
||||
|
||||
Conversion avec composantes:
|
||||
unité: €/mois
|
||||
formule:
|
||||
somme:
|
||||
- maladie . salarié
|
||||
- retraite
|
||||
- mutuelle
|
||||
exemples:
|
||||
- situation:
|
||||
assiette annuelle: 20000
|
||||
valeur attendue: 180
|
||||
|
||||
Conversion dans un abattement:
|
||||
formule:
|
||||
valeur: 1000€/an
|
||||
abattement: 10€/mois
|
||||
unité: €/an
|
||||
exemples:
|
||||
valeur attendue: 880
|
||||
|
||||
Conversion dans avec un abattement en %:
|
||||
unité: €/an
|
||||
formule:
|
||||
valeur: 1000€/an
|
||||
abattement: 10%
|
||||
exemples:
|
||||
- valeur attendue: 900
|
||||
|
||||
assiette cotisations:
|
||||
formule:
|
||||
valeur: assiette mensuelle
|
||||
abattement: 1200 €/an
|
||||
|
||||
prévoyance cadre:
|
||||
formule:
|
||||
produit:
|
||||
assiette: assiette cotisations
|
||||
taux: 1.5%
|
||||
|
||||
Conversion avec plusieurs échelons:
|
||||
formule:
|
||||
somme:
|
||||
- prévoyance cadre
|
||||
- 35€/mois
|
||||
unité: €/an
|
||||
exemples:
|
||||
situation:
|
||||
assiette mensuelle: 1100
|
||||
valeur attendue: 600
|
||||
|
||||
Conversion de situation:
|
||||
formule:
|
||||
somme:
|
||||
- retraite
|
||||
- mutuelle
|
||||
exemples:
|
||||
unité par défaut: €/an
|
||||
situation:
|
||||
retraite: 4000 €/an
|
||||
valeur attendue: 4360
|
|
@ -1,37 +0,0 @@
|
|||
Parse correctement les dates:
|
||||
formule: 08/02/2015
|
||||
exemples:
|
||||
- valeur attendue: 08/02/2015
|
||||
|
||||
Défaut au premier jour du mois:
|
||||
formule: 01/02/2015
|
||||
exemples:
|
||||
- valeur attendue: 01/02/2015
|
||||
|
||||
Comparaison sur les dates:
|
||||
formule: 02/03/2019 > 21/02/2019
|
||||
exemples:
|
||||
- valeur attendue: true
|
||||
|
||||
date de création:
|
||||
|
||||
Comparaison sur les dates avec référence:
|
||||
formule: date de création < 01/01/2010
|
||||
exemples:
|
||||
- situation:
|
||||
date de création: 01/03/1992
|
||||
valeur attendue: true
|
||||
- situation:
|
||||
date de création: 09/02/2019
|
||||
valeur attendue: false
|
||||
|
||||
Applicable si:
|
||||
applicable si: date de création < 01/01/2010
|
||||
formule: 10 €
|
||||
exemples:
|
||||
- situation:
|
||||
date de création: 01/03/1992
|
||||
valeur attendue: 10
|
||||
- situation:
|
||||
date de création: 09/02/2019
|
||||
valeur attendue: false
|
|
@ -1,23 +0,0 @@
|
|||
date de création:
|
||||
type: date
|
||||
question: quelle est la date de création ?
|
||||
|
||||
durée entre deux dates:
|
||||
unité: jours
|
||||
formule:
|
||||
durée:
|
||||
depuis: date de création
|
||||
jusqu'à: 01/01/2020
|
||||
exemples:
|
||||
- nom: Un an
|
||||
situation:
|
||||
date de création: 01/01/2019
|
||||
valeur attendue: 365
|
||||
- nom: Longtemps
|
||||
situation:
|
||||
date de création: 06/11/2012
|
||||
valeur attendue: 2612
|
||||
- nom: Un mois
|
||||
situation:
|
||||
date de création: 01/12/2019
|
||||
valeur attendue: 31
|
|
@ -1,58 +0,0 @@
|
|||
plafonnement:
|
||||
formule:
|
||||
valeur: 1000 €
|
||||
plafond: 250 €
|
||||
|
||||
exemples:
|
||||
- valeur attendue: 250
|
||||
|
||||
plafond nouvelle ecriture:
|
||||
formule:
|
||||
valeur: 1000 €
|
||||
plafond: 250 €
|
||||
|
||||
exemples:
|
||||
- valeur attendue: 250
|
||||
|
||||
plancher nouvelle ecriture:
|
||||
formule:
|
||||
valeur: 1000 €
|
||||
plancher: 2000 €
|
||||
|
||||
exemples:
|
||||
- valeur attendue: 2000
|
||||
|
||||
plafonnement inactif:
|
||||
formule:
|
||||
valeur: 1000 €
|
||||
plafond: non
|
||||
|
||||
exemples:
|
||||
- valeur attendue: 1000
|
||||
|
||||
plafonnement reference inactive:
|
||||
formule:
|
||||
valeur: 1000 €
|
||||
plafond: plafond
|
||||
|
||||
exemples:
|
||||
- valeur attendue: 1000
|
||||
|
||||
plafonnement reference inactive . plafond: non
|
||||
plancher:
|
||||
formule:
|
||||
valeur: 1000 €
|
||||
plancher: 2500 €
|
||||
|
||||
exemples:
|
||||
- valeur attendue: 2500
|
||||
|
||||
encadrement inférieur et supérieur:
|
||||
formule:
|
||||
somme:
|
||||
- 500
|
||||
- 400
|
||||
plafond: 800
|
||||
plancher: 200
|
||||
exemples:
|
||||
- valeur attendue: 800
|
|
@ -1,321 +0,0 @@
|
|||
entier:
|
||||
formule: 5
|
||||
exemples:
|
||||
- valeur attendue: 5
|
||||
|
||||
nombre décimal:
|
||||
formule: 5.4
|
||||
exemples:
|
||||
- valeur attendue: 5.4
|
||||
|
||||
addition de nombres:
|
||||
formule: 28 + 1.1
|
||||
exemples:
|
||||
- valeur attendue: 29.1
|
||||
|
||||
addition de plusieurs nombres:
|
||||
formule: 27 + 1.1 + 0.9
|
||||
exemples:
|
||||
- valeur attendue: 29
|
||||
|
||||
addition et produit:
|
||||
formule: 27 + 1 * 2
|
||||
exemples:
|
||||
- valeur attendue: 29
|
||||
|
||||
parenthèses:
|
||||
formule: 14.5 * (6 - 4)
|
||||
exemples:
|
||||
- valeur attendue: 29
|
||||
|
||||
salaire de base:
|
||||
unité: $
|
||||
|
||||
contrat: oui
|
||||
contrat . salaire de base:
|
||||
|
||||
produit:
|
||||
formule: salaire de base * 3
|
||||
unité attendue: $
|
||||
exemples:
|
||||
- situation:
|
||||
salaire de base: 1000
|
||||
valeur attendue: 3000
|
||||
|
||||
multiplication et variable avec espace:
|
||||
formule: contrat . salaire de base * 3
|
||||
exemples:
|
||||
- situation:
|
||||
contrat . salaire de base: 1000
|
||||
valeur attendue: 3000
|
||||
|
||||
taux:
|
||||
unité: '%'
|
||||
|
||||
soustraction:
|
||||
unité: '%'
|
||||
formule: 100% - taux
|
||||
unité attendue: '%'
|
||||
exemples:
|
||||
- situation:
|
||||
taux: 89
|
||||
valeur attendue: 11
|
||||
|
||||
addition:
|
||||
formule: salaire de base + 2000
|
||||
unité attendue: $
|
||||
exemples:
|
||||
- situation:
|
||||
salaire de base: 3000
|
||||
valeur attendue: 5000
|
||||
|
||||
revenus fonciers:
|
||||
|
||||
addition bis:
|
||||
formule: salaire de base + revenus fonciers
|
||||
exemples:
|
||||
- situation:
|
||||
salaire de base: 3000
|
||||
revenus fonciers: 2000
|
||||
valeur attendue: 5000
|
||||
|
||||
division:
|
||||
formule: salaire de base / 3
|
||||
exemples:
|
||||
- situation:
|
||||
salaire de base: 3000
|
||||
valeur attendue: 1000
|
||||
|
||||
division deux:
|
||||
formule: 2000 / salaire de base
|
||||
unité attendue: /$
|
||||
exemples:
|
||||
- situation:
|
||||
salaire de base: 3000
|
||||
valeur attendue: 0.66667
|
||||
|
||||
nombre de personnes:
|
||||
unité: personne
|
||||
|
||||
division trois:
|
||||
formule: salaire de base / nombre de personnes
|
||||
unité attendue: $/personne
|
||||
exemples:
|
||||
- situation:
|
||||
salaire de base: 3000
|
||||
nombre de personnes: 10
|
||||
valeur attendue: 300
|
||||
|
||||
comparaison stricte:
|
||||
formule: salaire de base < 3001
|
||||
exemples:
|
||||
- nom: inférieur
|
||||
situation:
|
||||
salaire de base: 3000
|
||||
valeur attendue: true
|
||||
- nom: égal
|
||||
situation:
|
||||
salaire de base: 3001
|
||||
valeur attendue: false
|
||||
- nom: supérieur
|
||||
situation:
|
||||
salaire de base: 3002
|
||||
valeur attendue: false
|
||||
|
||||
comparaison non stricte:
|
||||
formule: salaire de base <= 3000
|
||||
exemples:
|
||||
- nom: inférieur
|
||||
situation:
|
||||
salaire de base: 2999.999
|
||||
valeur attendue: true
|
||||
- nom: égal
|
||||
situation:
|
||||
salaire de base: 3000
|
||||
valeur attendue: true
|
||||
- nom: supérieur
|
||||
situation:
|
||||
salaire de base: 3000.1
|
||||
valeur attendue: false
|
||||
|
||||
plafond sécurité sociale:
|
||||
unité: $
|
||||
|
||||
CDD:
|
||||
|
||||
CDD . poursuivi en CDI:
|
||||
|
||||
variable booléene:
|
||||
formule: CDD . poursuivi en CDI
|
||||
exemples:
|
||||
- situation:
|
||||
CDD . poursuivi en CDI: oui
|
||||
valeur attendue: true
|
||||
- situation:
|
||||
CDD . poursuivi en CDI: non
|
||||
valeur attendue: false
|
||||
|
||||
booléen:
|
||||
formule: oui
|
||||
exemples:
|
||||
- valeur attendue: true
|
||||
|
||||
négation:
|
||||
formule: CDD . poursuivi en CDI != oui
|
||||
exemples:
|
||||
- situation:
|
||||
CDD . poursuivi en CDI: oui
|
||||
valeur attendue: false
|
||||
- situation:
|
||||
CDD . poursuivi en CDI: non
|
||||
valeur attendue: true
|
||||
|
||||
pourcentage:
|
||||
formule: 38.1%
|
||||
exemples:
|
||||
- valeur attendue: 38.1
|
||||
unité attendue: '%'
|
||||
#- test: variable modifiée temporellement
|
||||
|
||||
multiplication et pourcentage:
|
||||
formule: 38.1% * salaire de base
|
||||
unité: $
|
||||
exemples:
|
||||
- situation:
|
||||
salaire de base: 1000
|
||||
valeur attendue: 381
|
||||
unité attendue: $
|
||||
|
||||
litéral avec unité:
|
||||
formule: 1 jour
|
||||
unité attendue: jour
|
||||
|
||||
litéral avec unité €:
|
||||
formule: 2 €
|
||||
unité attendue: €
|
||||
|
||||
litéral avec unité complexe:
|
||||
formule: 1 €/jour
|
||||
unité attendue: €/jour
|
||||
|
||||
inférence d'unité littéraux:
|
||||
formule: 2 €/jour * 2 jour
|
||||
valeur attendue: 4
|
||||
unité attendue: €
|
||||
|
||||
catégorie d'activité:
|
||||
formule:
|
||||
une possibilité:
|
||||
possibilités:
|
||||
- commerciale
|
||||
- artisanale
|
||||
|
||||
catégorie d'activité . artisanale:
|
||||
catégorie d'activité . commerciale:
|
||||
|
||||
test de possibilités:
|
||||
formule: catégorie d'activité = 'artisanale'
|
||||
exemples:
|
||||
- situation:
|
||||
catégorie d'activité: "'artisanale'"
|
||||
valeur attendue: true
|
||||
- situation:
|
||||
catégorie d'activité: "'commerciale'"
|
||||
valeur attendue: false
|
||||
|
||||
revenu:
|
||||
unité: €/mois
|
||||
|
||||
unité de variable modifiée:
|
||||
formule: revenu
|
||||
unité: k€/an
|
||||
exemples:
|
||||
- situation:
|
||||
revenu: 1000
|
||||
valeur attendue: 12
|
||||
|
||||
opérations multiples:
|
||||
formule: 4 * plafond sécurité sociale * 10%
|
||||
unité: $
|
||||
exemples:
|
||||
- situation:
|
||||
plafond sécurité sociale: 1000
|
||||
valeur attendue: 400
|
||||
|
||||
comparaison et opération:
|
||||
formule: salaire de base < 4 * plafond sécurité sociale
|
||||
exemples:
|
||||
- situation:
|
||||
salaire de base: 1000
|
||||
plafond sécurité sociale: 3500
|
||||
valeur attendue: true
|
||||
|
||||
nombres négatifs:
|
||||
formule: -5 * -10
|
||||
exemples:
|
||||
- valeur attendue: 50
|
||||
|
||||
négation de variable:
|
||||
formule: '- salaire de base'
|
||||
exemples:
|
||||
- situation:
|
||||
salaire de base: 3000
|
||||
valeur attendue: -3000
|
||||
|
||||
négation d'expressions:
|
||||
formule: '- (10 * 3 + 5)'
|
||||
exemples:
|
||||
- valeur attendue: -35
|
||||
|
||||
variables négatives dans expression:
|
||||
formule: 10% * (- salaire de base)
|
||||
unité: $
|
||||
exemples:
|
||||
- situation:
|
||||
salaire de base: 3000
|
||||
valeur attendue: -300
|
||||
|
||||
expression dans situation:
|
||||
formule: 10% * salaire de base
|
||||
unité: $
|
||||
exemples:
|
||||
- situation:
|
||||
salaire de base: 12 * 100
|
||||
unité attendue: $
|
||||
valeur attendue: 120
|
||||
|
||||
salaire:
|
||||
unité: €/mois
|
||||
expression dans situation 2:
|
||||
formule: 10% * salaire
|
||||
unité: €/mois
|
||||
exemples:
|
||||
- situation:
|
||||
salaire: 48k€/an
|
||||
unité attendue: €/mois
|
||||
valeur attendue: 400
|
||||
|
||||
chaine de charactère:
|
||||
formule: "'loup y es-tu ? 🐺'"
|
||||
exemples:
|
||||
- valeur attendue: loup y es-tu ? 🐺
|
||||
- situation:
|
||||
chaine de charactère: "'je suis là !'"
|
||||
valeur attendue: je suis là !
|
||||
- situation:
|
||||
chaine de charactère: "'je t'y vois'"
|
||||
valeur attendue: je t'y vois
|
||||
|
||||
a: oui
|
||||
b: 5
|
||||
a . b: b + 5
|
||||
a . c: b + 5
|
||||
désambiguation du nom de règle 1:
|
||||
formule: a . b
|
||||
exemples:
|
||||
- valeur attendue: 10
|
||||
|
||||
désambiguation du nom de règle 2:
|
||||
formule: a . c
|
||||
exemples:
|
||||
- valeur attendue: 15
|
|
@ -1,65 +0,0 @@
|
|||
assiette:
|
||||
unité: €
|
||||
|
||||
Grille:
|
||||
formule:
|
||||
unité: €
|
||||
grille:
|
||||
assiette: assiette
|
||||
tranches:
|
||||
- montant: 50
|
||||
plafond: 1000 €
|
||||
- montant: 170
|
||||
plafond: 2000 €
|
||||
- montant: 400
|
||||
|
||||
unité attendue: €
|
||||
exemples:
|
||||
- nom: 'petite assiette'
|
||||
situation:
|
||||
assiette: 200
|
||||
valeur attendue: 50
|
||||
- nom: 'moyenne assiette'
|
||||
situation:
|
||||
assiette: 1500
|
||||
valeur attendue: 170
|
||||
- nom: 'grande assiette'
|
||||
situation:
|
||||
assiette: 10000
|
||||
valeur attendue: 400
|
||||
- nom: 'assiette limite'
|
||||
situation:
|
||||
assiette: 999.3
|
||||
valeur attendue: 50
|
||||
|
||||
plafond:
|
||||
unité: €
|
||||
Grille avec valeur manquante:
|
||||
formule:
|
||||
unité: €
|
||||
grille:
|
||||
assiette: assiette
|
||||
tranches:
|
||||
- montant: 100
|
||||
plafond: plafond
|
||||
- montant: 200
|
||||
plafond: 2000 €
|
||||
- montant: 300
|
||||
plafond: 4000 €
|
||||
|
||||
unité attendue: €
|
||||
exemples:
|
||||
- nom: 'variable manquante'
|
||||
situation:
|
||||
assiette: 1000
|
||||
variables manquantes:
|
||||
- plafond
|
||||
valeur attendue: null
|
||||
- nom: 'assiette non concernée par variable manquante'
|
||||
situation:
|
||||
assiette: 3000
|
||||
valeur attendue: 300
|
||||
- nom: 'assiette au delà du plafond'
|
||||
situation:
|
||||
assiette: 5000
|
||||
valeur attendue: false
|
|
@ -1,26 +0,0 @@
|
|||
Maximum:
|
||||
formule:
|
||||
le maximum de:
|
||||
- produit:
|
||||
assiette: 100
|
||||
taux: 1%
|
||||
- produit:
|
||||
assiette: 10
|
||||
taux: 9%
|
||||
|
||||
exemples:
|
||||
- valeur attendue: 1
|
||||
|
||||
a:
|
||||
applicable si: non
|
||||
formule: 20
|
||||
|
||||
Maximum avec valeur non applicable:
|
||||
formule:
|
||||
le maximum de:
|
||||
- a
|
||||
- 10
|
||||
exemples:
|
||||
- valeur attendue: 10
|
||||
# TODO
|
||||
# Pouvoir faire référence à une variable, ou mettre une valeur. Aujourd'hui il est seulement possible de lister des mécanismes numériques
|
|
@ -1,41 +0,0 @@
|
|||
Minimum:
|
||||
formule:
|
||||
le minimum de:
|
||||
- produit:
|
||||
assiette: 100
|
||||
taux: 1%
|
||||
- produit:
|
||||
assiette: 10
|
||||
taux: 9%
|
||||
|
||||
exemples:
|
||||
- valeur attendue: 0.9
|
||||
|
||||
assiette:
|
||||
question: Mon assiette
|
||||
|
||||
Minimum avec variables:
|
||||
formule:
|
||||
le minimum de:
|
||||
- produit:
|
||||
assiette: assiette
|
||||
taux: 1%
|
||||
- produit:
|
||||
assiette: assiette
|
||||
taux: 9%
|
||||
|
||||
exemples:
|
||||
- situation:
|
||||
assiette: 1000
|
||||
valeur attendue: 10
|
||||
|
||||
a: non
|
||||
Minimum avec valeur non applicable:
|
||||
formule:
|
||||
le minimum de:
|
||||
- a
|
||||
- 10
|
||||
exemples:
|
||||
- valeur attendue: 10
|
||||
# TODO
|
||||
# Pouvoir faire référence à une variable, ou mettre une valeur. Aujourd'hui il est seulement possible de lister des mécanismes numériques
|
|
@ -1,110 +0,0 @@
|
|||
mon assiette:
|
||||
unité: €
|
||||
|
||||
Multiplication simple:
|
||||
formule:
|
||||
produit:
|
||||
assiette: mon assiette
|
||||
taux: 3%
|
||||
unité attendue: €
|
||||
exemples:
|
||||
- nom: entier
|
||||
situation:
|
||||
mon assiette: 100
|
||||
valeur attendue: 3
|
||||
- nom: flottant
|
||||
situation:
|
||||
mon assiette: 333.33
|
||||
valeur attendue: 9.999
|
||||
|
||||
Multiplication à taux flottant:
|
||||
formule:
|
||||
produit:
|
||||
assiette: 300
|
||||
taux: 3.3%
|
||||
exemples:
|
||||
- nom:
|
||||
situation:
|
||||
valeur attendue: 9.9
|
||||
|
||||
mon plafond:
|
||||
unité: €
|
||||
|
||||
Multiplication plafonnée:
|
||||
formule:
|
||||
produit:
|
||||
assiette: mon assiette
|
||||
taux: 3%
|
||||
plafond: mon plafond
|
||||
|
||||
exemples:
|
||||
- nom: plafond non atteint
|
||||
situation:
|
||||
mon assiette: 100
|
||||
mon plafond: 200
|
||||
valeur attendue: 3
|
||||
- nom: plafond atteint
|
||||
situation:
|
||||
mon assiette: 100
|
||||
mon plafond: 50
|
||||
valeur attendue: 1.5
|
||||
|
||||
mon facteur:
|
||||
unité: patates
|
||||
|
||||
Multiplication à facteur:
|
||||
formule:
|
||||
produit:
|
||||
assiette: 100
|
||||
facteur: mon facteur
|
||||
|
||||
exemples:
|
||||
- nom:
|
||||
situation:
|
||||
mon facteur: 3
|
||||
unité attendue: patates
|
||||
valeur attendue: 300
|
||||
|
||||
Multiplication complète:
|
||||
formule:
|
||||
produit:
|
||||
assiette: mon assiette
|
||||
facteur: mon facteur
|
||||
taux: 0.5%
|
||||
plafond: mon plafond
|
||||
|
||||
unité attendue: €.patates
|
||||
exemples:
|
||||
- nom:
|
||||
situation:
|
||||
mon assiette: 200
|
||||
mon facteur: 2
|
||||
mon plafond: 100
|
||||
valeur attendue: 1
|
||||
# This should work, but with the use of objectShape & co, the short circuits are not performed
|
||||
#Multiplication complète:
|
||||
# formule:
|
||||
# produit:
|
||||
# assiette: mon assiette
|
||||
# facteur: mon facteur
|
||||
# plafond: mon plafond
|
||||
# taux: 0.5%
|
||||
#
|
||||
# exemples:
|
||||
# - nom: Assiette manquante
|
||||
# situation:
|
||||
# mon facteur: 0
|
||||
# mon plafond: 100
|
||||
# valeur attendue: 0
|
||||
# variables manquantes: []
|
||||
|
||||
a:
|
||||
formule:
|
||||
produit:
|
||||
assiette: 1000
|
||||
taux: 3%
|
||||
|
||||
b:
|
||||
formule: a
|
||||
exemples:
|
||||
- valeur attendue: 30
|
|
@ -1,39 +0,0 @@
|
|||
cotisation:
|
||||
formule:
|
||||
multiplication:
|
||||
assiette [ref]: 1000 €
|
||||
taux [ref taux employeur]: 4%
|
||||
|
||||
test:
|
||||
|
||||
paramètre nommés:
|
||||
formule: test
|
||||
exemples:
|
||||
- situation:
|
||||
test: cotisation . assiette
|
||||
valeur attendue: 1000
|
||||
- situation:
|
||||
test: cotisation . taux employeur
|
||||
valeur attendue: 4
|
||||
|
||||
cotisation 2:
|
||||
formule:
|
||||
multiplication:
|
||||
assiette [ref]:
|
||||
valeur: 1000€
|
||||
plafond [ref]: 100€
|
||||
taux: 5%
|
||||
|
||||
paramètre nommés imbriqués:
|
||||
formule: cotisation 2 . assiette . plafond
|
||||
exemples:
|
||||
- valeur attendue: 100
|
||||
|
||||
paramètre nommé utilisé dans la règle:
|
||||
formule:
|
||||
valeur [ref assiette]: 500€
|
||||
abattement:
|
||||
valeur: assiette * 10%
|
||||
plancher: 100€
|
||||
exemples:
|
||||
- valeur attendue: 400
|
|
@ -1,21 +0,0 @@
|
|||
enfants:
|
||||
|
||||
nombre enfants:
|
||||
applicable si: enfants
|
||||
question: Combien d'enfants avez vous ?
|
||||
par défaut: 4 enfants
|
||||
|
||||
famille nombreuse:
|
||||
titre: question conditionnelle
|
||||
formule: nombre enfants > 3
|
||||
exemples:
|
||||
- nom: question posée
|
||||
situation:
|
||||
enfants: oui
|
||||
variables manquantes: ['nombre enfants']
|
||||
valeur attendue: true
|
||||
- nom: question non posée
|
||||
situation:
|
||||
enfants: non
|
||||
variables manquantes: []
|
||||
valeur attendue: false
|
|
@ -1,25 +0,0 @@
|
|||
salaire brut:
|
||||
formule: 2000€
|
||||
salaire net:
|
||||
formule: 0.5 * salaire brut
|
||||
SMIC brut:
|
||||
formule: 1000€
|
||||
SMIC net:
|
||||
formule:
|
||||
recalcul:
|
||||
règle: salaire net
|
||||
avec:
|
||||
salaire brut: SMIC brut
|
||||
exemples:
|
||||
- valeur attendue: 500
|
||||
|
||||
Recalcule règle courante:
|
||||
unité: €
|
||||
formule:
|
||||
valeur: 10% * salaire brut
|
||||
plafond:
|
||||
recalcul:
|
||||
avec:
|
||||
salaire brut: 100€
|
||||
exemples:
|
||||
- valeur attendue: 10
|
|
@ -1,207 +0,0 @@
|
|||
restaurant: oui
|
||||
restaurant . prix du repas: 10 €/repas
|
||||
restaurant . client gourmand: oui
|
||||
restaurant . client enfant:
|
||||
rend non applicable:
|
||||
- client gourmand
|
||||
formule: non
|
||||
|
||||
restaurant . prix du repas gourmand:
|
||||
applicable si: client gourmand
|
||||
remplace: prix du repas
|
||||
formule: 15 €/repas
|
||||
|
||||
restaurant . menu enfant:
|
||||
formule: oui
|
||||
applicable si: client enfant
|
||||
remplace:
|
||||
règle: prix du repas
|
||||
par: 8 €/repas
|
||||
|
||||
modifie une règle:
|
||||
formule: restaurant . prix du repas
|
||||
exemples:
|
||||
- nom: prix du repas modifié
|
||||
valeur attendue: 15
|
||||
- nom: prix du repas sans modification
|
||||
situation:
|
||||
restaurant . client gourmand: non
|
||||
valeur attendue: 10
|
||||
- nom: prix du repas modifé par règle
|
||||
situation:
|
||||
restaurant . client enfant: oui
|
||||
valeur attendue: 8
|
||||
|
||||
cotisations . assiette:
|
||||
formule: 1000 €
|
||||
|
||||
cotisations:
|
||||
formule:
|
||||
somme:
|
||||
- retraite . salarié
|
||||
- retraite . employeur
|
||||
- chômage
|
||||
- maladie
|
||||
|
||||
cotisations . retraite:
|
||||
formule:
|
||||
produit:
|
||||
composantes:
|
||||
- attributs:
|
||||
nom: employeur
|
||||
taux: 8%
|
||||
- attributs:
|
||||
nom: salarié
|
||||
taux: 2%
|
||||
assiette: assiette
|
||||
|
||||
cotisations . chômage:
|
||||
formule:
|
||||
produit:
|
||||
taux: 10%
|
||||
assiette: assiette
|
||||
|
||||
cotisations . maladie:
|
||||
formule:
|
||||
produit:
|
||||
taux: 10%
|
||||
assiette: assiette
|
||||
|
||||
exemple1:
|
||||
par défaut: non
|
||||
remplace:
|
||||
règle: cotisations . assiette
|
||||
par: 100
|
||||
|
||||
exemple2:
|
||||
remplace:
|
||||
règle: cotisations . assiette
|
||||
par: 500
|
||||
dans: cotisations . retraite
|
||||
par défaut: non
|
||||
|
||||
exemple3:
|
||||
par défaut: non
|
||||
remplace:
|
||||
règle: cotisations . assiette
|
||||
par: 100
|
||||
sauf dans:
|
||||
- cotisations . chômage
|
||||
- cotisations . maladie
|
||||
|
||||
exemple4:
|
||||
par défaut: non
|
||||
exemple4 . cotisations retraite:
|
||||
remplace: cotisations . retraite
|
||||
formule:
|
||||
produit:
|
||||
assiette: cotisations . assiette
|
||||
composantes:
|
||||
- attributs:
|
||||
remplace: cotisations . retraite . employeur
|
||||
nom: employeur
|
||||
taux: 12%
|
||||
- attributs:
|
||||
remplace: cotisations . retraite . salarié
|
||||
nom: salarié
|
||||
taux: 8%
|
||||
|
||||
exemple5:
|
||||
par défaut: non
|
||||
remplace:
|
||||
- règle: cotisations . chômage
|
||||
par: 10€
|
||||
- règle: cotisations . maladie
|
||||
par: 0
|
||||
|
||||
remplacements:
|
||||
formule: cotisations
|
||||
exemples:
|
||||
- nom: sans boucle infinie si il n'y a pas de dépendances cycliques
|
||||
situation:
|
||||
exemple1: oui
|
||||
valeur attendue: 30
|
||||
- nom: contextuel par inclusion
|
||||
situation:
|
||||
exemple2: oui
|
||||
valeur attendue: 250
|
||||
- nom: avec plusieurs remplacements existant pour une même variables
|
||||
# ici, le remplacement de l'exemple 2 doit être effectué car plus précis que celui de l'exemple 1
|
||||
situation:
|
||||
exemple1: oui
|
||||
exemple2: oui
|
||||
valeur attendue: 70
|
||||
- nom: contextuel par exclusion
|
||||
situation:
|
||||
exemple3: oui
|
||||
valeur attendue: 210
|
||||
- nom: variable avec composante
|
||||
situation:
|
||||
exemple4: oui
|
||||
valeur attendue: 400
|
||||
- nom: avec remplacement dans un remplacement
|
||||
situation:
|
||||
exemple4: oui
|
||||
exemple1: oui
|
||||
valeur attendue: 40
|
||||
- nom: plusieurs variables d'un coup
|
||||
situation:
|
||||
exemple5: oui
|
||||
valeur attendue: 110
|
||||
|
||||
A:
|
||||
formule: 1
|
||||
B:
|
||||
remplace: A
|
||||
formule: 2
|
||||
|
||||
C:
|
||||
remplace: B
|
||||
formule: 3
|
||||
# TODO
|
||||
# remplacement associatif:
|
||||
# formule: A
|
||||
# exemples:
|
||||
# - valeur attendue: 3
|
||||
|
||||
x:
|
||||
formule: non
|
||||
|
||||
z:
|
||||
formule: 1
|
||||
|
||||
x . y:
|
||||
remplace: z
|
||||
formule: 20
|
||||
|
||||
remplacement non applicable car branche desactivée:
|
||||
formule: z
|
||||
exemples:
|
||||
- valeur attendue: 1
|
||||
|
||||
# Remplacement effectué dans la bonne variable
|
||||
espace: oui
|
||||
espace . valeur:
|
||||
formule: 20
|
||||
espace . remplacement:
|
||||
remplace: valeur
|
||||
formule: valeur + 10
|
||||
test remplacement effectué dans la variable à remplacer:
|
||||
formule: espace . valeur
|
||||
exemples:
|
||||
- valeur attendue: 30
|
||||
|
||||
frais de repas:
|
||||
formule: 5 €/repas
|
||||
|
||||
convention hôtels cafés restaurants:
|
||||
formule: oui
|
||||
|
||||
convention hôtels cafés restaurants . frais de repas:
|
||||
remplace: frais de repas
|
||||
formule: 6 €/repas
|
||||
|
||||
remplacement d'un nom de variable identique:
|
||||
formule: frais de repas
|
||||
exemples:
|
||||
- valeur attendue: 6
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue