🔥 Utilise les paquets publicodes depuis NPM

pull/1843/head
Maxime Quandalle 2021-11-30 17:47:28 +01:00 committed by Maxime Quandalle
parent cc50f8c3e7
commit d5979264d2
217 changed files with 178 additions and 31757 deletions

View File

@ -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: {

View File

@ -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.

View File

@ -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",

View File

@ -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(

View File

@ -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,
}),
{}
)

View File

@ -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"
],

View File

@ -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

View File

@ -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

12
publicodes/.gitignore vendored
View File

@ -1,12 +0,0 @@
.tags*
.tmp
/tmp
.DS_Store
yarn-error.log
package-lock.json
node_modules/
.env
dist/
# Local Netlify folder
.netlify

View File

@ -1,3 +0,0 @@
bracketSpacing: true
semi: false
singleQuote: true

View File

@ -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

View File

@ -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

View File

@ -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.

View File

@ -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

View File

@ -1 +0,0 @@
*.tgz

View File

@ -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"
]
}

View File

@ -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

View File

@ -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

View File

@ -1,6 +0,0 @@
{
"type": "module",
"engines": {
"node": ">=15.0.0"
}
}

View File

@ -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"
}
}

View File

@ -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]))
)
})
}

View File

@ -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}`
}

View File

@ -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),
})),
})

View File

@ -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>

View File

@ -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
}

View File

@ -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)}
`
)
}
}

View File

@ -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]
})
)
}

View File

@ -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
}

View File

@ -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()
}

View File

@ -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 %}

View File

@ -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),
},
})

View File

@ -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)
)
}

View File

@ -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)

View File

@ -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)

View File

@ -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)

View File

@ -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)

View File

@ -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',
}
}

View File

@ -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)

View File

@ -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)

View File

@ -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)

View File

@ -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)

View File

@ -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)

View File

@ -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)

View File

@ -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)

View File

@ -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)

View File

@ -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 },
}))

View File

@ -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

View File

@ -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)

View File

@ -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)

View File

@ -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)

View File

@ -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)

View File

@ -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)

View File

@ -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
)

View File

@ -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,
}
})

View File

@ -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)
})

View File

@ -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)

View File

@ -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)

View File

@ -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]
}

View File

@ -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,
}
})

View File

@ -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)

View File

@ -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,
}
}

View File

@ -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)

View File

@ -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,
}
}
})

View File

@ -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 }),
}
})

View File

@ -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]
}

View File

@ -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
})

View File

@ -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'
)
}

View File

@ -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}'`
}
}

View File

@ -1,4 +0,0 @@
declare module '*.md' {
const content: string
export default content
}

View File

@ -1,4 +0,0 @@
declare module '*.ne' {
const content: any
export default content
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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 === '%'
)
}

View File

@ -1,2 +0,0 @@
env:
mocha: true

View File

@ -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([])
})
})

View File

@ -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
)
})
})

View File

@ -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')
})
})

View File

@ -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
})
})

View File

@ -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)
})
})

View File

@ -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

View File

@ -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))
}
}
)
}
)
})
})
})

View File

@ -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)
})
})

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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%

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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