Merge branch 'new-top-down'
commit
576dc16a23
File diff suppressed because it is too large
Load Diff
28
package.json
28
package.json
|
@ -14,12 +14,13 @@
|
|||
"babel-runtime": "^6.23.0",
|
||||
"classnames": "^2.2.5",
|
||||
"deep-assign": "^2.0.0",
|
||||
"ignore-loader": "^0.1.2",
|
||||
"install": "^0.10.1",
|
||||
"js-yaml": "^3.8.4",
|
||||
"js-yaml": "^3.9.1",
|
||||
"marked": "^0.3.6",
|
||||
"nearley": "^2.9.2",
|
||||
"npm": "^4.6.1",
|
||||
"ramda": "^0.23.0",
|
||||
"npm": "^5.3.0",
|
||||
"ramda": "0.24.1",
|
||||
"react": "^15.5.4",
|
||||
"react-dom": "^15.5.4",
|
||||
"react-helmet": "^5.1.3",
|
||||
|
@ -27,10 +28,11 @@
|
|||
"react-router-dom": "^4.1.1",
|
||||
"reduce-reducers": "^0.1.2",
|
||||
"redux": "^3.6.0",
|
||||
"redux-form": "^6.7.0",
|
||||
"redux-form": "6.8.0",
|
||||
"redux-saga": "^0.15.3",
|
||||
"reselect": "^3.0.1",
|
||||
"whatwg-fetch": "^2.0.3"
|
||||
"whatwg-fetch": "^2.0.3",
|
||||
"yaml-loader": "^0.5.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"autoprefixer": "^7.1.1",
|
||||
|
@ -50,11 +52,11 @@
|
|||
"chokidar": "^1.7.0",
|
||||
"core-js": "^2.4.1",
|
||||
"css-loader": "^0.28.1",
|
||||
"eslint": "^3.19.0",
|
||||
"eslint": "^4.4.1",
|
||||
"eslint-plugin-react": "^7.0.1",
|
||||
"express": "^4.15.3",
|
||||
"file-loader": "^0.11.1",
|
||||
"html-loader": "^0.4.5",
|
||||
"html-loader": "^0.5.1",
|
||||
"img-loader": "^2.0.0",
|
||||
"jsdom": "^11.0.0",
|
||||
"json-loader": "^0.5.4",
|
||||
|
@ -67,17 +69,17 @@
|
|||
"redux-devtools-dock-monitor": "^1.1.2",
|
||||
"redux-devtools-log-monitor": "^1.3.0",
|
||||
"source-map-support": "^0.4.15",
|
||||
"style-loader": "^0.17.0",
|
||||
"style-loader": "^0.18.2",
|
||||
"url-loader": "^0.5.8",
|
||||
"webpack": "^2.6.1",
|
||||
"webpack-dev-server": "^2.4.5",
|
||||
"yaml-loader": "^0.4.0"
|
||||
"webpack": "^3.5.4",
|
||||
"webpack-dev-server": "^2.4.5"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "node source/server.js",
|
||||
"compile": "NODE_ENV='production' webpack --config source/webpack.config.js",
|
||||
"surge": "npm run compile && surge --domain scientific-wish.surge.sh -p ./ && rm -rf dist/",
|
||||
"test": "mocha-webpack --webpack-config source/webpack.config.js --require source-map-support/register --require test/helpers/browser.js \"test/**/*.test.js\"",
|
||||
"test-fast": "babel-node --presets babel-preset-flow,babel-preset-env --plugins transform-class-properties test/helpers/runner.js -w"
|
||||
"test": "mocha-webpack --webpack-config source/webpack.test.config.js --require source-map-support/register --require test/helpers/browser.js \"test/**/*.test.js\"",
|
||||
"test-watch": "mocha-webpack --webpack-config source/webpack.test.config.js --require source-map-support/register --require test/helpers/browser.js \"test/**/*.test.js\" --watch",
|
||||
"test-meca": "mocha-webpack --webpack-config source/webpack.test.config.js --require source-map-support/register --require test/helpers/browser.js test/mecanisms.test.js"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,55 +0,0 @@
|
|||
Supposons qu'il y ait deux variables dans notre système.
|
||||
|
||||
|
||||
```yaml
|
||||
Variable: motif CDD
|
||||
valeur:
|
||||
une possibilité:
|
||||
- motif B
|
||||
- motif Z
|
||||
- motif H
|
||||
```
|
||||
|
||||
```yaml
|
||||
Variable: CIF CDD
|
||||
valeur:
|
||||
multiplication:
|
||||
assiette: salaire brut
|
||||
taux: 29%
|
||||
```
|
||||
|
||||
Si tu n'as pas à disposition la valeur de cette variable, le moteur peut utiliser la formule de la propriété `valeur` pour la calculer, _et_ s'il lui manque la valeur des dépendances (ex. pour 1. motif B, Z = `non` mais H = `null` donc inconnue; pour 2. salaire brut est `null`), il proposera un formulaire pour la récupérer.
|
||||
|
||||
|
||||
### Demander les valeurs manquantes
|
||||
|
||||
Ajoutons un mécanisme à la formule 2.
|
||||
|
||||
```yaml
|
||||
Variable: CIF CDD
|
||||
formule:
|
||||
non applicable si: motif H
|
||||
multiplication:
|
||||
assiette: salaire brut
|
||||
taux: 29%
|
||||
```
|
||||
|
||||
Pour la calculer, il nous faut maintenant la valeur de motif H, car elle n'est pas renseignée et donc cette variable n'est pas calculable : sa valeur est `null`. Comme dit précédemment, cela nous permet de proposer un formulaire de saisie à l'utilisateur :
|
||||
|
||||
```
|
||||
Motif H est-il vrai pour vous ?
|
||||
[Oui] [Non]
|
||||
```
|
||||
|
||||
C'est une première étape, mais elle n'est pas parfaite : étant donné que `motif H` appartient à une liste de possibilités exclusives dans la variable `Motif CDD`, peut-être qu'il serait préférable de poser cette question :
|
||||
|
||||
|
||||
```
|
||||
Quel est votre motif ?
|
||||
[Motif B] [Motif Z] [Motif H]
|
||||
```
|
||||
|
||||
Le moteur doit pour cela vérifier si motif H intervient dans un mécanisme de type `une possibilité` pour construire son formulaire de saisie.
|
||||
|
||||
|
||||
> la note "éclatement des variables et espaces de noms" continue en complexifiant de modèle.
|
|
@ -1,43 +0,0 @@
|
|||
Supposons que dans notre système, il y ait deux variables. C'est notre point de départ.
|
||||
|
||||
C'est un peu abstrait tout ça, mais ça permet de justifier certains choix de conception.
|
||||
|
||||
La première :
|
||||
|
||||
```yaml
|
||||
Variable: motif CDD
|
||||
contrainte:
|
||||
une possibilité:
|
||||
- motif B
|
||||
- motif Z
|
||||
- motif H
|
||||
```
|
||||
|
||||
La deuxième :
|
||||
|
||||
```yaml
|
||||
Variable: CIF CDD
|
||||
formule de calcul:
|
||||
multiplication:
|
||||
assiette: salaire brut
|
||||
taux: 29%
|
||||
```
|
||||
|
||||
# Contrainte ou formule de calcul ?
|
||||
|
||||
Si tu veux renseigner directement la valeur de cette variable, la propriété `contrainte` cette propriété me permet de contraindre la saisie à 3 possibilités.
|
||||
|
||||
A l'opposé, la variable CIF CDD a une propriété `formule de calcul` qui nous donne un algorithme de calcul utile quand tu ne peux renseigner directement la valeur de la variable.
|
||||
|
||||
Mais en y regardant de plus près, le fait que cette formule soit exprimée sous forme de _donnée_ (facile à _parser_, déclaratif) nous permettraient potentiellement aussi de _contraindre_ la saisie de la valeur de la variable.
|
||||
|
||||
> implicitement, je contraint ton entrée à une valeur compatible avec le mécanisme multiplication. On pourrait même pousser jusqu'à vérifier que la valeur est compatible avec le domaine qu'autorise la multiplication (par exemple si on sait que l'assiette est toujours positive).
|
||||
|
||||
|
||||
Et de même, le mécanisme `une possibilité` (et une seule) nous donne l'information suivant : si l'utilisateur a saisi `motif Z = oui`, alors motif CDD = motif Z. On retrouve donc une notion d'algorithme de calcul.
|
||||
|
||||
|
||||
La frontière est floue entre `contrainte/type` et `formule de calcul`. On va donc pour l'instant utiliser une unique propriété, `valeur`, qui rassemble ces deux usages.
|
||||
|
||||
|
||||
> Voir la note "des règles au formulaire"
|
|
@ -1,101 +0,0 @@
|
|||
> On atteint dans cette note le bout actuel de la réfléxion...
|
||||
|
||||
Pour l'instant, on n'avait considéré seulement des versions simplifiées des variables :
|
||||
|
||||
```yaml
|
||||
Variable: CIF CDD
|
||||
valeur:
|
||||
multiplication:
|
||||
assiette: salaire brut
|
||||
taux: 29%
|
||||
```
|
||||
|
||||
Or il y a pas mal de variables du système qui se ressembleront, et donc partageront des propriétés en commun.
|
||||
|
||||
Par exemples les cotisations, qui sont des obligations de verser une fraction du salaire à des organismes de protection sociale.
|
||||
|
||||
```yaml
|
||||
activité . contrat salarié : AGIRC
|
||||
description: AGIRC
|
||||
références: article B-vingt-douze
|
||||
dû par: salarié
|
||||
branche: retraite
|
||||
applicable si: Contrat . statut cadre
|
||||
valeur:
|
||||
multiplication:
|
||||
assiette: ...
|
||||
taux: ...
|
||||
```
|
||||
|
||||
On peut remarquer que certaines des propriétés de cet objet sont relativement génériques, alors que d'autres dépendent directement du fait que notre objet est une cotisation. On pourrait la réécrire ainsi :
|
||||
|
||||
```yaml
|
||||
activité . contrat salarié : AGIRC
|
||||
meta:
|
||||
description: AGIRC
|
||||
références: article B-vingt-douze
|
||||
cotisation:
|
||||
dû par: salarié
|
||||
branche: retraite
|
||||
valeur:
|
||||
applicable si: Contrat . statut cadre
|
||||
multiplication:
|
||||
assiette: ...
|
||||
taux: ...
|
||||
```
|
||||
|
||||
Finalement ici, on _renseigne_ les données d'une entité de type `Cotisation`. Tout comme quand on va fournir une situation au système, en renseignant les données d'un `Salarié` (âge, expérience...) et de son `Contrat` (appelé pour l'instant `Salariat`, qui a un salaire, ...) qui le lie à son `Entreprise` (effectif, ...).
|
||||
|
||||
Il faudra donc dans (TODO dans le futur) définir le _schéma_ de notre entité `Cotisation`. Pourquoi ne pas réutiliser ce que l'on a fait jusqu'à présent par exemple en définissant le schéma de `CDD . mofif` ?
|
||||
|
||||
```yaml
|
||||
- entité: cotisation
|
||||
|
||||
# Ici on ajoute des propriétés à l'espace de nom `cotisation`.
|
||||
# Comme si on créait et peuplait un _record_ en Haskell/Ocaml, ou encore d'un type produit en maths.
|
||||
- cotisation: dû par
|
||||
valeur:
|
||||
# Ici, ce serait l'équivalent des _variant types_ des ADTs d'Haskell/Ocaml, ou d'une type somme en maths.
|
||||
une possibilité:
|
||||
- salarié
|
||||
- entreprise
|
||||
|
||||
- cotisation . dû par: salarié
|
||||
meta:
|
||||
description: Les salariés ont l'obligation de verser des cotisations, mais c'est normalement l'employeur qui s'en charge.
|
||||
|
||||
# ...
|
||||
|
||||
- cotisation : branche
|
||||
valeur:
|
||||
une possibilité:
|
||||
- maladie
|
||||
- retraite
|
||||
- ...
|
||||
|
||||
```
|
||||
|
||||
Il est très intéressant de noter qu'ici on définit le `schéma cotisation` pour pouvoir en faire des instance avec des données respectant ce schéma (cotisation.branche=maladie, etc.)... tout comme nous avons précédemment défini le `schéma contrat salarié` pour pouvoir par la suite en faire une instance avec des données (contrat salarié . type de contrat = CDD).
|
||||
|
||||
<!-- TODO est-ce clair ? -->
|
||||
|
||||
De la même façon que le schéma cotisation, on définirait le schéma de `meta`, qui ferait lui souvent appel à des types plus élémentaires (et "terminaux"), des _strings_ (chaines de caractères).
|
||||
|
||||
Pour être parfait, le modèle définirait aussi le schéma de `valeur`.
|
||||
|
||||
|
||||
```yaml
|
||||
|
||||
- valeur : applicable si
|
||||
type: # mécanisme ou variable de type booléen
|
||||
|
||||
|
||||
- valeur : formule
|
||||
type:
|
||||
une possibilité: # mécanismes de type numérique
|
||||
- multiplication
|
||||
- barème
|
||||
- ...
|
||||
```
|
||||
|
||||
A ce stade, on aurait finalement besoin d'un langage de programmation complet (par exemple introduisant un certain polymorphisme : une cotisation et une indemnité partagent des propriété !), ce qui est un but non souhaitable pour le moment. `meta` et `valeur` peuvent dans un premier temps rester dynamiques (sans type bien défini) et types par leur implémentation en Javascript.
|
|
@ -1,119 +0,0 @@
|
|||
Supposons qu'il y ait deux variables dans notre système.
|
||||
|
||||
```yaml
|
||||
Variable: motif CDD
|
||||
valeur:
|
||||
une possibilité:
|
||||
- motif B
|
||||
- motif Z
|
||||
- motif H
|
||||
```
|
||||
|
||||
```yaml
|
||||
Variable: CIF CDD
|
||||
valeur:
|
||||
multiplication:
|
||||
assiette: salaire brut
|
||||
taux: 29%
|
||||
```
|
||||
|
||||
Rapprochons-nous encore de la réalité. La formule de `motif CDD` est en fait une imbrication de possibilités :
|
||||
|
||||
```yaml
|
||||
motif CDD:
|
||||
- motif classique
|
||||
- motif remplacement
|
||||
- motif accroissement d'activité
|
||||
- motif saisonnier
|
||||
- motif contrat aidé
|
||||
- motif complément formation
|
||||
- motif issue d'apprentissage
|
||||
```
|
||||
|
||||
Les formules d'autres variables, par exemple `indemnité fin de contrat` ou `CID CDD` peuvent alors dépendre des éléments de cette liste de possibilités imbriquées :
|
||||
|
||||
```yaml
|
||||
formule:
|
||||
non applicable si:
|
||||
une de ces conditions:
|
||||
- motif saisonnier
|
||||
- motif contrat aidé # on cite une catégorie de motifs, mais qui est une variable utilisable en soi
|
||||
```
|
||||
|
||||
Alors on pourrait représenter la variable `motif CDD` ainsi :
|
||||
|
||||
```yaml
|
||||
Variable: motif CDD
|
||||
formule:
|
||||
une possibilité parmi:
|
||||
- Variable: classique
|
||||
formule:
|
||||
une possibilité parmi:
|
||||
- Variable: remplacement
|
||||
titre: Contrat de remplacement
|
||||
|
||||
- Variable: usage
|
||||
titre: Contrat d'usage
|
||||
# formule...
|
||||
- Variable: accroissement d'activité
|
||||
titre: Motif accroissement temporaire d'activité
|
||||
- Variable: contrat aidé
|
||||
formule:
|
||||
une possibilité parmi:
|
||||
- A
|
||||
- B
|
||||
- Variable: complément formation
|
||||
description: le motif complément formation c'est...
|
||||
- Variable: issue d'apprentissage
|
||||
description: le motif d'issue d'apprentissage c'est ...
|
||||
```
|
||||
|
||||
- L'avantage de cette modélisation, c'est que tout est au même endroit; on évite, pour chaque descente de niveau de l'imbrication, de répéter le _chemin_ de la variable : on factorise de l'information.
|
||||
- Le problème, c'est que cela nuit vraiment à la lisibilité de la formule `motif` : on perd complètement la vue très synthétique de notre pseudo modèle de départ :
|
||||
|
||||
```yaml
|
||||
Variable: motif
|
||||
une possibilité parmi:
|
||||
- classique
|
||||
- contrat aidé
|
||||
- complément formation
|
||||
- issue d'apprentissage
|
||||
```
|
||||
|
||||
Et finalement, contrairement au mécanisme de `composantes` utilisé dans le corps des `Cotisations`, l'information factorisée n'est pas très conséquent : ce n'est que le chemin.
|
||||
|
||||
Une autre modélisation, que l'on retient, consiste donc à éclater les variables en utilisant un système d'**espace de nom**:
|
||||
|
||||
```yaml
|
||||
Salariat . CDD . motif: classique
|
||||
# espace de nom : nom de la variable
|
||||
formule:
|
||||
une possibilité:
|
||||
- remplacement
|
||||
- accroissement d'activité
|
||||
- saisonnier
|
||||
```
|
||||
|
||||
```yaml
|
||||
Salariat . CDD . motif : issue d'apprentissage
|
||||
description: |
|
||||
A l'issue d'un contrat d'apprentissage, un contrat de travail à durée déterminée peut être conclu lorsque l'apprenti doit satisfaire aux obligations du service national dans un délai de moins d'un an après l'expiration du contrat d'apprentissage.
|
||||
```
|
||||
|
||||
|
||||
On pourrait même définir l'espace de nom commun `Salariat . CDD . motif` en en-tête du fichier où est stocké la variable... mais cela nous rendrait trop dépendant du système de fichiers, qui deviendront superflus quand l'édition se fera dans une interface Web.
|
||||
|
||||
Pour beaucoup de variables au nom spécifique, il n'est pas souhaitable d'utiliser des espaces de nom, pour ne pas alourdir le code.
|
||||
|
||||
> Par exemple, on pourrait définir la variable CDD comme `activité . contrat salarié . CDD`. Mais CDD est un terme non ambigue dans le contexte français.
|
||||
|
||||
> À l'inverse, `motif` ou `effectif` sont trop spécifiques pour être laissés dans l'espace de nom global. Quand une variable contiendra `motif` dans son sous-espace de nom, c'est celle-ci qui sera utilisée plutôt que celle d'un autre espace, si elle existe.
|
||||
|
||||
Pas de panique, une variable globale pourra quand même être rattachée à quelque chose ! Même si elle n'est pas définie dans un espace de noms, `CDD` sera quand même utilisée dans `contrat salarié` commune une des valeurs de `type de contrat` (ou mieux à l'avenir !). Le moteur se chargera de retrouver son attache.
|
||||
|
||||
|
||||
Quand on créée par exemple la variable `CDD: motif`, donc la variable motif dans l'espace de nom CDD, et qu'on continue avec une variable `CDD . motif . classique` on fait deux choses :
|
||||
- si la variable `motif` a une propriété `valeur`, on créée un espace pour pour stocker la valeur de cette variable
|
||||
- on créée aussi un espace de nom associé à cette variable, qui va héberger des variables qui y sont liées et vont souvent intervenir dans la formule de la valeur de `motif`
|
||||
|
||||
> la note "typer les variables" peut être lue à la suite de celle-ci.
|
|
@ -55,7 +55,7 @@ On peut la voir comme une alternative adaptée à certains endroits (?).
|
|||
formule:
|
||||
assiette: assiette cotisations sociales
|
||||
taux:
|
||||
logique numérique:
|
||||
aiguillage numérique:
|
||||
statut cadre = non:
|
||||
2017: 16%
|
||||
2016: 12%
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
```yaml
|
||||
logique numérique: # première valeur trouvée, sinon 0
|
||||
aiguillage numérique: # première valeur trouvée, sinon 0
|
||||
- poursuite du CDD en CDI: 0%
|
||||
# - Contrat . type : # mécanisme de match à introduire une fois les entités gérées. Exclusivité exprimée dans l'entité Type
|
||||
- conditions exclusives:
|
||||
|
@ -12,7 +12,7 @@ logique numérique: # première valeur trouvée, sinon 0
|
|||
# - True: 0% # Ce mécanisme ajoute automatiquement cette ligne :)
|
||||
|
||||
|
||||
logique numérique 2:
|
||||
aiguillage numérique 2:
|
||||
- poursuite du CDD en CDI: 0%
|
||||
- aiguillage: # signale que les deux propositions sont exclusives
|
||||
sujet: Contrat . type
|
||||
|
|
|
@ -17,7 +17,7 @@
|
|||
assiette: assiette cotisations sociales
|
||||
plafond: 4 * plafond sécurité sociale
|
||||
taux:
|
||||
logique numérique:
|
||||
aiguillage numérique:
|
||||
motif . classique . accroissement activité:
|
||||
durée contrat <= 1: 3% # TODO 1 mois, pas 1 rien, évidemment
|
||||
durée contrat <= 3: 1.5%
|
||||
|
@ -33,7 +33,7 @@
|
|||
durée contrat: 1
|
||||
valeur attendue: 69
|
||||
|
||||
#TODO on ne peut aujourd'hui tester 'classique . usage' : l'évaluation n'aura pas la valeur de la première branche de la logique numérique, motif . classique . accroissement activité, et va donc s'arrêter en renvoyant un 'null'
|
||||
#TODO on ne peut aujourd'hui tester 'classique . usage' : l'évaluation n'aura pas la valeur de la première branche de l'aiguillage numérique, motif . classique . accroissement activité, et va donc s'arrêter en renvoyant un 'null'
|
||||
# solution possible : un mode d'évaluation 'shallow' ou non renseigné = faux (null -> false)
|
||||
|
||||
- nom: durée de contrat de 4 mois -> non applicable
|
||||
|
|
|
@ -66,7 +66,7 @@
|
|||
- l'URSSAF explique longuement la notion de durée du CDD : "Comment déterminer la durée du CDD ?"
|
||||
|
||||
taux: #TODO pseudo code pour l'instant
|
||||
logique numérique: # première valeur trouvée, sinon 0
|
||||
aiguillage numérique: # première valeur trouvée, sinon 0
|
||||
- poursuite du CDD en CDI: 0
|
||||
- CDD type accroissement temporaire d'activité:
|
||||
contrat de travail durée ≤ 1 mois: 0.03
|
||||
|
|
|
@ -88,6 +88,13 @@
|
|||
C'est, en gros, le salaire brut moins les cotisations sociales. Ce salaire est plus important que le brut car c'est ce que le salrié reçoit sur son compte bancaire, et pourtant, le brut est très utilisé lors des négociations salariales.
|
||||
formule: salaire brut - cotisations (salarié)
|
||||
|
||||
exemples:
|
||||
- nom: médian
|
||||
situation:
|
||||
CDD . événement . poursuite du CDD en CDI: oui
|
||||
salaire de base: 2300
|
||||
valeur attendue: 1768
|
||||
|
||||
- espace: contrat salarié
|
||||
nom: coût du travail
|
||||
description: |
|
||||
|
|
|
@ -4,10 +4,11 @@ import classNames from 'classnames'
|
|||
import {Link} from 'react-router-dom'
|
||||
import {connect} from 'react-redux'
|
||||
import { withRouter } from 'react-router'
|
||||
import {formValueSelector} from 'redux-form'
|
||||
|
||||
import './Results.css'
|
||||
import {capitalise0} from '../utils'
|
||||
import {computeRuleValue} from 'Engine/traverse'
|
||||
import {computeRuleValue, clearDict} from 'Engine/traverse'
|
||||
import {encodeRuleName} from 'Engine/rules'
|
||||
import {getObjectives} from 'Engine/generateQuestions'
|
||||
|
||||
|
@ -20,7 +21,8 @@ let humanFigure = decimalDigits => value => fmt(value.toFixed(decimalDigits))
|
|||
pointedOutObjectives: state.pointedOutObjectives,
|
||||
analysedSituation: state.analysedSituation,
|
||||
conversationStarted: !R.isEmpty(state.form),
|
||||
conversationFirstAnswer: R.path(['form', 'conversation', 'values'])(state)
|
||||
conversationFirstAnswer: R.path(['form', 'conversation', 'values'])(state),
|
||||
situationGate: (name => formValueSelector('conversation')(state, name))
|
||||
})
|
||||
)
|
||||
export default class Results extends Component {
|
||||
|
@ -30,9 +32,12 @@ export default class Results extends Component {
|
|||
pointedOutObjectives,
|
||||
conversationStarted,
|
||||
conversationFirstAnswer: showResults,
|
||||
situationGate,
|
||||
location
|
||||
} = this.props,
|
||||
explanation = getObjectives(analysedSituation)
|
||||
} = this.props
|
||||
|
||||
|
||||
let explanation = R.has('root', analysedSituation) && clearDict() && getObjectives(situationGate, analysedSituation.root, analysedSituation.parsedRules)
|
||||
|
||||
if (!explanation) return null
|
||||
|
||||
|
@ -42,13 +47,13 @@ export default class Results extends Component {
|
|||
<section id="results" className={classNames({show: showResults})}>
|
||||
{onRulePage && conversationStarted ?
|
||||
<div id ="results-actions">
|
||||
<Link id="toSimulation" to={"/simu/" + encodeRuleName(analysedSituation.name)}>
|
||||
<Link id="toSimulation" to={"/simu/" + encodeRuleName(analysedSituation.root.name)}>
|
||||
<i className="fa fa-arrow-circle-left" aria-hidden="true"></i>Reprendre la simulation
|
||||
</Link>
|
||||
</div>
|
||||
: <div id="results-titles">
|
||||
<h2>Vos résultats <i className="fa fa-hand-o-right" aria-hidden="true"></i></h2>
|
||||
{do {let text = R.path(['simulateur', 'résultats'])(analysedSituation)
|
||||
{do {let text = R.path(['simulateur', 'résultats'])(analysedSituation.root)
|
||||
text &&
|
||||
<p id="resultText">{text}</p>
|
||||
}}
|
||||
|
|
|
@ -44,7 +44,7 @@ export default class Satisfaction extends Component {
|
|||
let {answer, message, messageSent} = this.state,
|
||||
validMessage = typeof message == 'string' && message.length > 4,
|
||||
onSmileyClick = s => this.sendSatisfaction(s)
|
||||
console.log(messageSent)
|
||||
|
||||
if (!answer)
|
||||
return (
|
||||
<p id="satisfaction">
|
||||
|
|
|
@ -41,18 +41,19 @@ export default class extends React.Component {
|
|||
}
|
||||
}
|
||||
} = this.props,
|
||||
name = decodeRuleName(encodedName)
|
||||
name = decodeRuleName(encodedName),
|
||||
existingConversation = this.props.foldedSteps.length > 0
|
||||
|
||||
this.encodedName = encodedName
|
||||
this.name = name
|
||||
this.rule = findRuleByName(rules, name)
|
||||
|
||||
// C'est ici que la génération du formulaire, et donc la traversée des variables commence
|
||||
if (this.rule.formule)
|
||||
if (!existingConversation)
|
||||
this.props.startConversation(name)
|
||||
}
|
||||
render(){
|
||||
if (!this.rule.formule) return <Redirect to="/404"/>
|
||||
if (!this.rule.formule) return <Redirect to={"/regle/" + this.name}/>
|
||||
|
||||
let
|
||||
started = !this.props.match.params.intro,
|
||||
|
|
|
@ -23,7 +23,9 @@ export default class Explicable extends React.Component {
|
|||
// Rien à expliquer ici, ce n'est pas une règle
|
||||
if (!rule) return <span>{label}</span>
|
||||
|
||||
let ruleLabel = label || rule.titre || rule.name
|
||||
let ruleLabel = (
|
||||
label || rule.titre || rule.name
|
||||
).replace(/\s\?$/g, '\u00a0?') // le possible ' ?' final est rendu insécable
|
||||
|
||||
// Rien à expliquer ici, il n'y a pas de champ description dans la règle
|
||||
if (!rule.description)
|
||||
|
|
|
@ -4,6 +4,14 @@ import R from 'ramda'
|
|||
import {AttachDictionary} from '../AttachDictionary'
|
||||
import knownMecanisms from 'Engine/known-mecanisms.yaml'
|
||||
import marked from 'Engine/marked'
|
||||
import {makeJsx} from 'Engine/evaluation'
|
||||
|
||||
let RuleWithoutFormula = () =>
|
||||
<p>
|
||||
Nous ne connaissons pas la formule de cette règle pour l'instant. Sa valeur
|
||||
doit donc être renseignée directement.
|
||||
</p>
|
||||
|
||||
|
||||
@AttachDictionary(knownMecanisms)
|
||||
export default class Algorithm extends React.Component {
|
||||
|
@ -22,12 +30,12 @@ export default class Algorithm extends React.Component {
|
|||
cond != null &&
|
||||
<section id="declenchement">
|
||||
<h2>Conditions de déclenchement</h2>
|
||||
{cond.jsx}
|
||||
{makeJsx(cond)}
|
||||
</section>
|
||||
}}
|
||||
<section id="formule">
|
||||
<h2>Calcul</h2>
|
||||
{rule['formule'].jsx}
|
||||
{rule['formule'] ? makeJsx(rule['formule']) : <RuleWithoutFormula />}
|
||||
</section>
|
||||
</section>
|
||||
</div>
|
||||
|
|
|
@ -0,0 +1,59 @@
|
|||
import R from 'ramda'
|
||||
|
||||
export let makeJsx = node =>
|
||||
typeof node.jsx == "function"
|
||||
? node.jsx(node.nodeValue, node.explanation)
|
||||
: node.jsx
|
||||
|
||||
export let collectNodeMissing = (node) =>
|
||||
node.collectMissing ? node.collectMissing(node) : []
|
||||
|
||||
export let evaluateNode = (situationGate, parsedRules, node) =>
|
||||
node.evaluate ? node.evaluate(situationGate, parsedRules, node) : node
|
||||
|
||||
export let rewriteNode = (node, nodeValue, explanation, collectMissing) =>
|
||||
({
|
||||
...node,
|
||||
nodeValue,
|
||||
collectMissing,
|
||||
explanation
|
||||
})
|
||||
|
||||
export let evaluateArray = (reducer, start) => (situationGate, parsedRules, node) => {
|
||||
let evaluateOne = child => evaluateNode(situationGate, parsedRules, child),
|
||||
explanation = R.map(evaluateOne, node.explanation),
|
||||
values = R.pluck("nodeValue",explanation),
|
||||
nodeValue = R.any(R.equals(null),values) ? null : R.reduce(reducer, start, values)
|
||||
|
||||
let collectMissing = node => node.nodeValue == null ? R.chain(collectNodeMissing,node.explanation) : []
|
||||
return rewriteNode(node,nodeValue,explanation,collectMissing)
|
||||
}
|
||||
|
||||
export let evaluateArrayWithFilter = (filter, reducer, start) => (situationGate, parsedRules, node) => {
|
||||
let evaluateOne = child => evaluateNode(situationGate, parsedRules, child),
|
||||
explanation = R.map(evaluateOne, R.filter(filter(situationGate),node.explanation)),
|
||||
values = R.pluck("nodeValue",explanation),
|
||||
nodeValue = R.any(R.equals(null),values) ? null : R.reduce(reducer, start, values)
|
||||
|
||||
let collectMissing = node => R.chain(collectNodeMissing,node.explanation)
|
||||
return rewriteNode(node,nodeValue,explanation,collectMissing)
|
||||
}
|
||||
|
||||
export let parseObject = (recurse, objectShape, value) => {
|
||||
let recurseOne = key => defaultValue => {
|
||||
if (!value[key] && ! defaultValue) throw "Il manque une valeur '"+key+"'"
|
||||
return value[key] ? recurse(value[key]) : defaultValue
|
||||
}
|
||||
let transforms = R.fromPairs(R.map(k => [k,recurseOne(k)],R.keys(objectShape)))
|
||||
return R.evolve(transforms,objectShape)
|
||||
}
|
||||
|
||||
export let evaluateObject = (objectShape, effect) => (situationGate, parsedRules, node) => {
|
||||
let evaluateOne = child => evaluateNode(situationGate, parsedRules, child),
|
||||
collectMissing = node => R.chain(collectNodeMissing,R.values(node.explanation))
|
||||
|
||||
let transforms = R.map(k => [k,evaluateOne], R.keys(objectShape)),
|
||||
explanation = R.evolve(R.fromPairs(transforms))(node.explanation),
|
||||
nodeValue = effect(explanation)
|
||||
return rewriteNode(node,nodeValue,explanation,collectMissing)
|
||||
}
|
|
@ -9,7 +9,7 @@ import formValueTypes from 'Components/conversation/formValueTypes'
|
|||
import {analyseSituation} from './traverse'
|
||||
import {formValueSelector} from 'redux-form'
|
||||
import {rules, findRuleByDottedName, findVariantsAndRecords} from './rules'
|
||||
|
||||
import {collectNodeMissing, evaluateNode} from './evaluation'
|
||||
|
||||
|
||||
|
||||
|
@ -41,50 +41,28 @@ export let analyse = rootVariable => R.pipe(
|
|||
*/
|
||||
|
||||
// On peut travailler sur une somme, les objectifs sont alors les variables de cette somme.
|
||||
// Ou sur une variable unique ayant une formule, elle est elle-même le seul objectif
|
||||
export let getObjectives = analysedSituation => {
|
||||
let formuleType = R.path(["formule", "explanation", "name"])(
|
||||
analysedSituation
|
||||
)
|
||||
let result = formuleType == "somme"
|
||||
// Ou sur une variable unique ayant une formule ou une conodition 'non applicable si', elle est elle-même le seul objectif
|
||||
export let getObjectives = (situationGate, root, parsedRules) => {
|
||||
let formuleType = R.path(["formule", "explanation", "name"])(root)
|
||||
|
||||
let targets = formuleType == "somme"
|
||||
? R.pluck(
|
||||
"explanation",
|
||||
R.path(["formule", "explanation", "explanation"])(analysedSituation)
|
||||
"dottedName",
|
||||
R.path(["formule", "explanation", "explanation"])(root)
|
||||
)
|
||||
: formuleType ? [analysedSituation] : null
|
||||
: (root.formule || root['non applicable si']) ? [root.dottedName] : null,
|
||||
names = targets ? R.reject(R.isNil)(targets) : []
|
||||
|
||||
return result ? R.reject(R.isNil)(result) : null;
|
||||
let findAndEvaluate = name => evaluateNode(situationGate,parsedRules,findRuleByDottedName(parsedRules,name))
|
||||
return R.map(findAndEvaluate,names)
|
||||
}
|
||||
|
||||
// FIXME - this relies on side-effects and the recursion is grossly indiscriminate
|
||||
let collectNodeMissingVariables = (root, source=root, results=[]) => {
|
||||
if (
|
||||
source.nodeValue != null ||
|
||||
source.shortCircuit && source.shortCircuit(root)
|
||||
) {
|
||||
// console.log('nodev or shortcircuit root, source', root, source)
|
||||
return []
|
||||
}
|
||||
|
||||
if (source['missingVariables']) {
|
||||
// console.log('root, source', root, source)
|
||||
results.push(source['missingVariables'])
|
||||
}
|
||||
|
||||
for (var prop in source) {
|
||||
if (R.is(Object)(source[prop])) {
|
||||
collectNodeMissingVariables(root, source[prop], results)
|
||||
}
|
||||
}
|
||||
return results
|
||||
}
|
||||
|
||||
export let collectMissingVariables = (groupMethod='groupByMissingVariable') => analysedSituation =>
|
||||
R.pipe(
|
||||
getObjectives,
|
||||
export let collectMissingVariables = (groupMethod='groupByMissingVariable') => (situationGate, {root, parsedRules}) => {
|
||||
return R.pipe(
|
||||
R.curry(getObjectives)(situationGate),
|
||||
R.chain( v =>
|
||||
R.pipe(
|
||||
collectNodeMissingVariables,
|
||||
collectNodeMissing,
|
||||
R.flatten,
|
||||
R.map(mv => [v.dottedName, mv])
|
||||
)(v)
|
||||
|
@ -94,11 +72,12 @@ export let collectMissingVariables = (groupMethod='groupByMissingVariable') => a
|
|||
R.map(R.map(groupMethod == 'groupByMissingVariable' ? R.head : R.last))
|
||||
// below is a hand implementation of above... function composition can be nice sometimes :')
|
||||
// R.reduce( (memo, [mv, dependencyOf]) => ({...memo, [mv]: [...(memo[mv] || []), dependencyOf] }), {})
|
||||
)(analysedSituation)
|
||||
)(root, parsedRules)
|
||||
}
|
||||
|
||||
export let buildNextSteps = (allRules, analysedSituation) => {
|
||||
export let buildNextSteps = (situationGate, flatRules, analysedSituation) => {
|
||||
let missingVariables = collectMissingVariables('groupByMissingVariable')(
|
||||
analysedSituation
|
||||
situationGate, analysedSituation
|
||||
)
|
||||
|
||||
/*
|
||||
|
@ -137,11 +116,11 @@ export let buildNextSteps = (allRules, analysedSituation) => {
|
|||
|
||||
return R.pipe(
|
||||
R.keys,
|
||||
R.curry(findVariantsAndRecords)(allRules),
|
||||
R.curry(findVariantsAndRecords)(flatRules),
|
||||
// on va maintenant construire la liste des composants React qui afficheront les questions à l'utilisateur pour que l'on obtienne les variables manquantes
|
||||
R.evolve({
|
||||
variantGroups: generateGridQuestions(allRules, missingVariables),
|
||||
recordGroups: generateSimpleQuestions(allRules, missingVariables),
|
||||
variantGroups: generateGridQuestions(flatRules, missingVariables),
|
||||
recordGroups: generateSimpleQuestions(flatRules, missingVariables),
|
||||
}),
|
||||
R.values,
|
||||
R.unnest,
|
||||
|
@ -160,7 +139,8 @@ export let constructStepMeta = ({
|
|||
}) => ({
|
||||
// name: dottedName.split(' . ').join('.'),
|
||||
name: dottedName,
|
||||
// question: question || name,
|
||||
// <Explicable/> ajoutera une aide au clic sur un icône [?]
|
||||
// Son texte est la question s'il y en a une à poser. Sinon on prend le titre.
|
||||
question: (
|
||||
<Explicable label={question || name} dottedName={dottedName} lightBackground={true} />
|
||||
),
|
||||
|
|
|
@ -1,6 +1,9 @@
|
|||
# Liste et description des différents mécanismes compris par le moteur.
|
||||
# La description peut être rédigée en markdown :-)
|
||||
|
||||
une possibilité:
|
||||
type: enum
|
||||
|
||||
une de ces conditions:
|
||||
type: boolean
|
||||
description: |
|
||||
|
@ -18,14 +21,14 @@ toutes ces conditions:
|
|||
|
||||
Renvoie vrai si toutes les conditions vraies.
|
||||
|
||||
logique numérique:
|
||||
aiguillage numérique:
|
||||
type: numeric
|
||||
description: |
|
||||
Contient une liste de couples condition-conséquence.
|
||||
|
||||
Couple par couple, si la condition est vraie, alors on choisit la conséquence.
|
||||
|
||||
Cette conséquence peut elle-même être un mécanisme `logique numérique` ou plus simplement un `taux`.
|
||||
Cette conséquence peut elle-même être un mécanisme `aiguillage numérique` ou plus simplement un `taux`.
|
||||
|
||||
Si aucune condition n'est vraie, alors ce mécanisme renvoie implicitement `non applicable` (ce qui peut se traduire par la valeur `0` si nous sommes dans un contexte numérique).
|
||||
|
||||
|
|
|
@ -2,6 +2,9 @@ import R from 'ramda'
|
|||
import React from 'react'
|
||||
import {anyNull, val} from './traverse-common-functions'
|
||||
import {Node, Leaf} from './traverse-common-jsx'
|
||||
import {makeJsx, evaluateNode, rewriteNode, evaluateArray, evaluateArrayWithFilter, evaluateObject, parseObject, collectNodeMissing} from './evaluation'
|
||||
|
||||
let constantNode = constant => ({nodeValue: constant})
|
||||
|
||||
let transformPercentage = s =>
|
||||
R.contains('%')(s) ?
|
||||
|
@ -11,9 +14,7 @@ let transformPercentage = s =>
|
|||
export let decompose = (recurse, k, v) => {
|
||||
let
|
||||
subProps = R.dissoc('composantes')(v),
|
||||
filter = val(recurse("sys . filter")),
|
||||
isRelevant = c => !filter || !c.attributs || c.attributs['dû par'] == filter,
|
||||
composantes = v.composantes.filter(isRelevant).map(c =>
|
||||
explanation = v.composantes.map(c =>
|
||||
({
|
||||
... recurse(
|
||||
R.objOf(k,
|
||||
|
@ -24,23 +25,16 @@ export let decompose = (recurse, k, v) => {
|
|||
),
|
||||
composante: c.nom ? {nom: c.nom} : c.attributs
|
||||
})
|
||||
),
|
||||
nodeValue = anyNull(composantes) ? null
|
||||
: R.reduce(R.add, 0, composantes.map(val))
|
||||
)
|
||||
|
||||
return {
|
||||
nodeValue,
|
||||
category: 'mecanism',
|
||||
name: 'composantes',
|
||||
type: 'numeric',
|
||||
explanation: composantes,
|
||||
jsx: <Node
|
||||
let jsx = (nodeValue, explanation) =>
|
||||
<Node
|
||||
classes="mecanism composantes"
|
||||
name="composantes"
|
||||
value={nodeValue}
|
||||
child={
|
||||
<ul>
|
||||
{ composantes.map((c, i) =>
|
||||
{ explanation.map((c, i) =>
|
||||
[<li className="composante" key={JSON.stringify(c.composante)}>
|
||||
<ul className="composanteAttributes">
|
||||
{R.toPairs(c.composante).map(([k,v]) =>
|
||||
|
@ -51,163 +45,207 @@ export let decompose = (recurse, k, v) => {
|
|||
)}
|
||||
</ul>
|
||||
<div className="content">
|
||||
{c.jsx}
|
||||
{makeJsx(c)}
|
||||
</div>
|
||||
</li>,
|
||||
i < (composantes.length - 1) && <li className="composantesSymbol"><i className="fa fa-plus-circle" aria-hidden="true"></i></li>
|
||||
]
|
||||
)
|
||||
i < (explanation.length - 1) && <li className="composantesSymbol"><i className="fa fa-plus-circle" aria-hidden="true"></i></li>
|
||||
])
|
||||
}
|
||||
</ul>
|
||||
}
|
||||
/>
|
||||
|
||||
let filter = situationGate => c => (!situationGate("sys.filter") || !c.composante || !c.composante['dû par']) || c.composante['dû par'] == situationGate("sys.filter")
|
||||
|
||||
return {
|
||||
explanation,
|
||||
jsx,
|
||||
evaluate: evaluateArrayWithFilter(filter,R.add,0),
|
||||
category: 'mecanism',
|
||||
name: 'composantes',
|
||||
type: 'numeric'
|
||||
}
|
||||
}
|
||||
|
||||
export let mecanismOneOf = (recurse, k, v) => {
|
||||
let result = R.pipe(
|
||||
R.unless(R.is(Array), () => {throw 'should be array'}),
|
||||
R.reduce( (memo, next) => {
|
||||
let {nodeValue, explanation} = memo,
|
||||
child = recurse(next),
|
||||
{nodeValue: nextValue} = child
|
||||
return {...memo,
|
||||
// c'est un OU logique mais avec une préférence pour null sur false
|
||||
nodeValue: nodeValue || nextValue || (
|
||||
nodeValue == null ? null : nextValue
|
||||
),
|
||||
explanation: [...explanation, child]
|
||||
}
|
||||
}, {
|
||||
nodeValue: false,
|
||||
category: 'mecanism',
|
||||
name: 'une de ces conditions',
|
||||
type: 'boolean',
|
||||
explanation: []
|
||||
}) // Reduce but don't use R.reduced to set the nodeValue : we need to treat all the nodes
|
||||
)(v)
|
||||
return {...result,
|
||||
jsx: <Node
|
||||
if (!R.is(Array,v)) throw 'should be array'
|
||||
|
||||
let explanation = R.map(recurse, v)
|
||||
|
||||
let jsx = (nodeValue, explanation) =>
|
||||
<Node
|
||||
classes="mecanism conditions list"
|
||||
name={result.name}
|
||||
value={result.nodeValue}
|
||||
name='une de ces conditions'
|
||||
value={nodeValue}
|
||||
child={
|
||||
<ul>
|
||||
{result.explanation.map(item => <li key={item.name || item.text}>{item.jsx}</li>)}
|
||||
{explanation.map(item => <li key={item.name || item.text}>{makeJsx(item)}</li>)}
|
||||
</ul>
|
||||
}
|
||||
/>
|
||||
|
||||
let evaluate = (situationGate, parsedRules, node) => {
|
||||
let evaluateOne = child => evaluateNode(situationGate, parsedRules, child),
|
||||
explanation = R.map(evaluateOne, node.explanation),
|
||||
values = R.pluck("nodeValue",explanation),
|
||||
nodeValue = R.any(R.equals(true),values) ? true :
|
||||
(R.any(R.equals(null),values) ? null : false)
|
||||
|
||||
let collectMissing = node => node.nodeValue == null ? R.chain(collectNodeMissing,node.explanation) : []
|
||||
return rewriteNode(node,nodeValue,explanation,collectMissing)
|
||||
}
|
||||
|
||||
return {
|
||||
evaluate,
|
||||
jsx,
|
||||
explanation,
|
||||
category: 'mecanism',
|
||||
name: 'une de ces conditions',
|
||||
type: 'boolean'
|
||||
}
|
||||
}
|
||||
|
||||
export let mecanismAllOf = (recurse, k,v) => {
|
||||
return R.pipe(
|
||||
R.unless(R.is(Array), () => {throw 'should be array'}),
|
||||
R.reduce( (memo, next) => {
|
||||
let {nodeValue, explanation} = memo,
|
||||
child = recurse(next),
|
||||
{nodeValue: nextValue} = child
|
||||
return {...memo,
|
||||
// c'est un ET logique avec une possibilité de null
|
||||
nodeValue: ! nodeValue ? nodeValue : nextValue,
|
||||
explanation: [...explanation, child]
|
||||
if (!R.is(Array,v)) throw 'should be array'
|
||||
|
||||
let explanation = R.map(recurse, v)
|
||||
|
||||
let jsx = (nodeValue, explanation) =>
|
||||
<Node
|
||||
classes="mecanism conditions list"
|
||||
name='toutes ces conditions'
|
||||
value={nodeValue}
|
||||
child={
|
||||
<ul>
|
||||
{explanation.map(item => <li key={item.name || item.text}>{makeJsx(item)}</li>)}
|
||||
</ul>
|
||||
}
|
||||
}, {
|
||||
nodeValue: true,
|
||||
category: 'mecanism',
|
||||
name: 'toutes ces conditions',
|
||||
type: 'boolean',
|
||||
explanation: []
|
||||
}) // Reduce but don't use R.reduced to set the nodeValue : we need to treat all the nodes
|
||||
)(v)
|
||||
/>
|
||||
|
||||
return {
|
||||
evaluate: evaluateArray(R.and,true),
|
||||
jsx,
|
||||
explanation,
|
||||
category: 'mecanism',
|
||||
name: 'toutes ces conditions',
|
||||
type: 'boolean'
|
||||
}
|
||||
}
|
||||
|
||||
export let mecanismNumericalLogic = (recurse, k,v) => {
|
||||
return R.ifElse(
|
||||
R.is(String),
|
||||
rate => ({ //TODO unifier ce code
|
||||
nodeValue: transformPercentage(rate),
|
||||
type: 'numeric',
|
||||
category: 'percentage',
|
||||
percentage: rate,
|
||||
explanation: null,
|
||||
jsx:
|
||||
<span className="percentage" >
|
||||
<span className="name">{rate}</span>
|
||||
</span>
|
||||
}),
|
||||
R.pipe(
|
||||
R.unless(
|
||||
v => R.is(Object)(v) && R.keys(v).length >= 1,
|
||||
() => {throw 'Le mécanisme "logique numérique" et ses sous-logiques doivent contenir au moins une proposition'}
|
||||
),
|
||||
R.toPairs,
|
||||
R.reduce( (memo, [condition, consequence]) => {
|
||||
let
|
||||
{nodeValue, explanation} = memo,
|
||||
conditionNode = recurse(condition), // can be a 'comparison', a 'variable', TODO a 'negation'
|
||||
childNumericalLogic = mecanismNumericalLogic(recurse, condition, consequence),
|
||||
nextNodeValue = conditionNode.nodeValue == null ?
|
||||
// Si la proposition n'est pas encore résolvable
|
||||
null
|
||||
// Si la proposition est résolvable
|
||||
: conditionNode.nodeValue == true ?
|
||||
// Si elle est vraie
|
||||
childNumericalLogic.nodeValue
|
||||
// Si elle est fausse
|
||||
: false
|
||||
export let mecanismNumericalSwitch = (recurse, k,v) => {
|
||||
if (R.is(String,v)) {
|
||||
// This seems an undue limitation
|
||||
// Or a logical one if we decide to keep this mecanism specialized as opposed to "variations"
|
||||
return mecanismPercentage(recurse,k,v)
|
||||
}
|
||||
|
||||
return {...memo,
|
||||
nodeValue: nodeValue == null ?
|
||||
null
|
||||
: nodeValue !== false ?
|
||||
nodeValue // l'une des propositions renvoie déjà une valeur numérique donc différente de false
|
||||
: nextNodeValue,
|
||||
explanation: [...explanation, {
|
||||
nodeValue: nextNodeValue,
|
||||
category: 'condition',
|
||||
text: condition,
|
||||
condition: conditionNode,
|
||||
conditionValue: conditionNode.nodeValue,
|
||||
type: 'boolean',
|
||||
explanation: childNumericalLogic,
|
||||
jsx: <div className="condition">
|
||||
{conditionNode.jsx}
|
||||
<div>
|
||||
{childNumericalLogic.jsx}
|
||||
</div>
|
||||
</div>
|
||||
}],
|
||||
}
|
||||
}, {
|
||||
nodeValue: false,
|
||||
category: 'mecanism',
|
||||
name: "logique numérique",
|
||||
type: 'boolean || numeric', // lol !
|
||||
explanation: []
|
||||
}),
|
||||
node => ({...node,
|
||||
jsx: <Node
|
||||
classes="mecanism numericalLogic list"
|
||||
name="logique numérique"
|
||||
value={node.nodeValue}
|
||||
child={
|
||||
<ul>
|
||||
{node.explanation.map(item => <li key={item.name || item.text}>{item.jsx}</li>)}
|
||||
</ul>
|
||||
}
|
||||
/>
|
||||
})
|
||||
))(v)
|
||||
if (!R.is(Object,v) || R.keys(v).length == 0) {
|
||||
throw 'Le mécanisme "aiguillage numérique" et ses sous-logiques doivent contenir au moins une proposition'
|
||||
}
|
||||
|
||||
// les termes sont les coupes (condition, conséquence) de l'aiguillage numérique
|
||||
let terms = R.toPairs(v)
|
||||
|
||||
// la conséquence peut être un 'string' ou un autre aiguillage numérique
|
||||
let parseCondition = ([condition, consequence]) => {
|
||||
let
|
||||
conditionNode = recurse(condition), // can be a 'comparison', a 'variable', TODO a 'negation'
|
||||
consequenceNode = mecanismNumericalSwitch(recurse, condition, consequence)
|
||||
|
||||
let evaluate = (situationGate, parsedRules, node) => {
|
||||
let collectMissing = node => {
|
||||
let missingOnTheLeft = collectNodeMissing(node.explanation.condition),
|
||||
investigate = node.explanation.condition.nodeValue !== false,
|
||||
missingOnTheRight = investigate ? collectNodeMissing(node.explanation.consequence) : []
|
||||
return R.concat(missingOnTheLeft, missingOnTheRight)
|
||||
}
|
||||
|
||||
let explanation = R.evolve({
|
||||
condition: R.curry(evaluateNode)(situationGate, parsedRules),
|
||||
consequence: R.curry(evaluateNode)(situationGate, parsedRules)
|
||||
}, node.explanation)
|
||||
|
||||
return {
|
||||
...node,
|
||||
collectMissing,
|
||||
explanation,
|
||||
nodeValue: explanation.consequence.nodeValue,
|
||||
condValue: explanation.condition.nodeValue
|
||||
}
|
||||
}
|
||||
|
||||
let jsx = (nodeValue, {condition, consequence}) =>
|
||||
<div className="condition">
|
||||
{makeJsx(condition)}
|
||||
<div>
|
||||
{makeJsx(consequence)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
return {
|
||||
evaluate,
|
||||
jsx,
|
||||
explanation: {condition: conditionNode, consequence: consequenceNode},
|
||||
category: 'condition',
|
||||
text: condition,
|
||||
condition: conditionNode,
|
||||
type: 'boolean',
|
||||
}
|
||||
}
|
||||
|
||||
let evaluateTerms = (situationGate, parsedRules, node) => {
|
||||
let
|
||||
evaluateOne = child => evaluateNode(situationGate, parsedRules, child),
|
||||
explanation = R.map(evaluateOne, node.explanation),
|
||||
choice = R.find(node => node.condValue, explanation),
|
||||
nonFalsyTerms = R.filter(node => node.condValue !== false, explanation),
|
||||
getFirst = (prop) => R.pipe(R.head, R.prop(prop))(nonFalsyTerms),
|
||||
nodeValue =
|
||||
// voilà le "numérique" dans le nom de ce mécanisme : il renvoie zéro si aucune condition n'est vérifiée
|
||||
R.isEmpty(nonFalsyTerms) ? 0 :
|
||||
// c'est un 'null', on renvoie null car des variables sont manquantes
|
||||
getFirst('condValue') == null ? null :
|
||||
// c'est un true, on renvoie la valeur de la conséquence
|
||||
getFirst('nodeValue')
|
||||
|
||||
let collectMissing = node => {
|
||||
let choice = R.find(node => node.condValue, node.explanation)
|
||||
return choice ? collectNodeMissing(choice) : R.chain(collectNodeMissing,node.explanation)
|
||||
}
|
||||
|
||||
return rewriteNode(node,nodeValue,explanation,collectMissing)
|
||||
}
|
||||
|
||||
let explanation = R.map(parseCondition,terms)
|
||||
|
||||
let jsx = (nodeValue, explanation) =>
|
||||
<Node
|
||||
classes="mecanism numericalSwitch list"
|
||||
name="aiguillage numérique"
|
||||
value={nodeValue}
|
||||
child={
|
||||
<ul>
|
||||
{explanation.map(item => <li key={item.name || item.text}>{makeJsx(item)}</li>)}
|
||||
</ul>
|
||||
}
|
||||
/>
|
||||
|
||||
return {
|
||||
evaluate: evaluateTerms,
|
||||
jsx,
|
||||
explanation,
|
||||
category: 'mecanism',
|
||||
name: "aiguillage numérique",
|
||||
type: 'boolean || numeric' // lol !
|
||||
}
|
||||
}
|
||||
|
||||
export let mecanismPercentage = (recurse,k,v) => {
|
||||
let reg = /^(\d+(\.\d+)?)\%$/
|
||||
if (R.test(reg)(v))
|
||||
return {
|
||||
category: 'percentage',
|
||||
type: 'numeric',
|
||||
percentage: v,
|
||||
category: 'percentage',
|
||||
nodeValue: R.match(reg)(v)[1]/100,
|
||||
explanation: null,
|
||||
jsx:
|
||||
|
@ -215,59 +253,44 @@ export let mecanismPercentage = (recurse,k,v) => {
|
|||
<span className="name">{v}</span>
|
||||
</span>
|
||||
}
|
||||
// Si c'est une liste historisée de pourcentages
|
||||
// TODO revoir le test avant le bug de l'an 2100
|
||||
else if ( R.is(Array)(v) && R.all(R.test(/(19|20)\d\d(-\d\d)?(-\d\d)?/))(R.keys(v)) ) {
|
||||
//TODO sélectionner la date de la simulation en cours
|
||||
let lazySelection = R.first(R.values(v))
|
||||
return {
|
||||
category: 'percentage',
|
||||
type: 'numeric',
|
||||
percentage: lazySelection,
|
||||
nodeValue: transformPercentage(lazySelection),
|
||||
explanation: null,
|
||||
jsx:
|
||||
<span className="percentage" >
|
||||
<span className="name">{lazySelection}</span>
|
||||
</span>
|
||||
}
|
||||
}
|
||||
else {
|
||||
let node = recurse(v)
|
||||
let evaluate = (situation, parsedRules, node) => evaluateNode(situation, parsedRules, node.explanation)
|
||||
let jsx = (nodeValue,explanation) => makeJsx(explanation)
|
||||
return {
|
||||
evaluate,
|
||||
jsx,
|
||||
type: 'numeric',
|
||||
category: 'percentage',
|
||||
percentage: node.nodeValue,
|
||||
nodeValue: node.nodeValue,
|
||||
explanation: node,
|
||||
jsx: node.jsx
|
||||
explanation: node
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export let mecanismSum = (recurse,k,v) => {
|
||||
let
|
||||
summedVariables = v.map(recurse),
|
||||
nodeValue = summedVariables.reduce(
|
||||
(memo, {nodeValue: nextNodeValue}) => memo == null ? null : nextNodeValue == null ? null : memo + +nextNodeValue,
|
||||
0)
|
||||
let explanation = v.map(recurse)
|
||||
|
||||
return {
|
||||
nodeValue,
|
||||
category: 'mecanism',
|
||||
name: 'somme',
|
||||
type: 'numeric',
|
||||
explanation: summedVariables,
|
||||
jsx: <Node
|
||||
let evaluate = evaluateArray(R.add,0)
|
||||
|
||||
let jsx = (nodeValue, explanation) =>
|
||||
<Node
|
||||
classes="mecanism somme"
|
||||
name="somme"
|
||||
value={nodeValue}
|
||||
child={
|
||||
<ul>
|
||||
{summedVariables.map(v => <li key={v.name || v.text}>{v.jsx}</li>)}
|
||||
{explanation.map(v => <li key={v.name || v.text}>{makeJsx(v)}</li>)}
|
||||
</ul>
|
||||
}
|
||||
/>
|
||||
|
||||
return {
|
||||
evaluate,
|
||||
jsx,
|
||||
explanation,
|
||||
category: 'mecanism',
|
||||
name: 'somme',
|
||||
type: 'numeric'
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -276,34 +299,30 @@ export let mecanismProduct = (recurse,k,v) => {
|
|||
return decompose(recurse,k,v)
|
||||
}
|
||||
|
||||
let
|
||||
mult = (base, rate, facteur, plafond) =>
|
||||
Math.min(base, plafond) * rate * facteur,
|
||||
constantNode = constant => ({nodeValue: constant}),
|
||||
assiette = recurse(v['assiette']),
|
||||
//TODO parser le taux dans le parser ?
|
||||
taux = v['taux'] ? recurse({taux: v['taux']}) : constantNode(1),
|
||||
facteur = v['facteur'] ? recurse(v['facteur']) : constantNode(1),
|
||||
plafond = v['plafond'] ? recurse(v['plafond']) : constantNode(Infinity),
|
||||
//TODO rate == false should be more explicit
|
||||
nodeValue = (val(taux) === 0 || val(taux) === false || val(assiette) === 0 || val(facteur) === 0) ?
|
||||
// Preprocessing step to parse percentages
|
||||
let wrap = x => ({taux: x}),
|
||||
value = R.evolve({taux:wrap},v)
|
||||
|
||||
let objectShape = {
|
||||
assiette:false,
|
||||
taux:constantNode(1),
|
||||
facteur:constantNode(1),
|
||||
plafond:constantNode(Infinity)
|
||||
}
|
||||
let effect = ({assiette,taux,facteur,plafond}) => {
|
||||
let mult = (base, rate, facteur, plafond) => Math.min(base, plafond) * rate * facteur
|
||||
return (val(taux) === 0 || val(taux) === false || val(assiette) === 0 || val(facteur) === 0) ?
|
||||
0
|
||||
: anyNull([taux, assiette, facteur, plafond]) ?
|
||||
null
|
||||
: mult(val(assiette), val(taux), val(facteur), val(plafond))
|
||||
return {
|
||||
nodeValue,
|
||||
category: 'mecanism',
|
||||
name: 'multiplication',
|
||||
type: 'numeric',
|
||||
explanation: {
|
||||
assiette,
|
||||
taux,
|
||||
facteur,
|
||||
plafond
|
||||
//TODO introduire 'prorata' ou 'multiplicateur', pour sémantiser les opérandes ?
|
||||
},
|
||||
jsx: <Node
|
||||
}
|
||||
|
||||
let explanation = parseObject(recurse,objectShape,value),
|
||||
evaluate = evaluateObject(objectShape,effect)
|
||||
|
||||
let jsx = (nodeValue, explanation) =>
|
||||
<Node
|
||||
classes="mecanism multiplication"
|
||||
name="multiplication"
|
||||
value={nodeValue}
|
||||
|
@ -311,26 +330,34 @@ export let mecanismProduct = (recurse,k,v) => {
|
|||
<ul className="properties">
|
||||
<li key="assiette">
|
||||
<span className="key">assiette: </span>
|
||||
<span className="value">{assiette.jsx}</span>
|
||||
<span className="value">{makeJsx(explanation.assiette)}</span>
|
||||
</li>
|
||||
{taux.nodeValue != 1 &&
|
||||
{explanation.taux.nodeValue != 1 &&
|
||||
<li key="taux">
|
||||
<span className="key">taux: </span>
|
||||
<span className="value">{taux.jsx}</span>
|
||||
<span className="value">{makeJsx(explanation.taux)}</span>
|
||||
</li>}
|
||||
{facteur.nodeValue != 1 &&
|
||||
{explanation.facteur.nodeValue != 1 &&
|
||||
<li key="facteur">
|
||||
<span className="key">facteur: </span>
|
||||
<span className="value">{facteur.jsx}</span>
|
||||
<span className="value">{makeJsx(explanation.facteur)}</span>
|
||||
</li>}
|
||||
{plafond.nodeValue != Infinity &&
|
||||
{explanation.plafond.nodeValue != Infinity &&
|
||||
<li key="plafond">
|
||||
<span className="key">plafond: </span>
|
||||
<span className="value">{plafond.jsx}</span>
|
||||
<span className="value">{makeJsx(explanation.plafond)}</span>
|
||||
</li>}
|
||||
</ul>
|
||||
}
|
||||
/>
|
||||
|
||||
return {
|
||||
evaluate,
|
||||
jsx,
|
||||
explanation,
|
||||
category: 'mecanism',
|
||||
name: 'multiplication',
|
||||
type: 'numeric'
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -345,23 +372,30 @@ export let mecanismScale = (recurse,k,v) => {
|
|||
if (v['multiplicateur des tranches'] == null)
|
||||
throw "un barème nécessite pour l'instant une propriété 'multiplicateur des tranches'"
|
||||
|
||||
let
|
||||
assiette = recurse(v['assiette']),
|
||||
multiplicateur = recurse(v['multiplicateur des tranches']),
|
||||
|
||||
/* on réécrit en plus bas niveau les tranches :
|
||||
`en-dessous de: 1`
|
||||
devient
|
||||
```
|
||||
de: 0
|
||||
à: 1
|
||||
```
|
||||
*/
|
||||
tranches = v['tranches'].map(t =>
|
||||
/* on réécrit en plus bas niveau les tranches :
|
||||
`en-dessous de: 1`
|
||||
devient
|
||||
```
|
||||
de: 0
|
||||
à: 1
|
||||
```
|
||||
*/
|
||||
let tranches = v['tranches'].map(t =>
|
||||
R.has('en-dessous de')(t) ? {de: 0, 'à': t['en-dessous de'], taux: t.taux}
|
||||
: R.has('au-dessus de')(t) ? {de: t['au-dessus de'], 'à': Infinity, taux: t.taux}
|
||||
: t
|
||||
),
|
||||
: t)
|
||||
|
||||
let aliased = {
|
||||
...v,
|
||||
multiplicateur: v['multiplicateur des tranches']
|
||||
}
|
||||
|
||||
let objectShape = {
|
||||
assiette:false,
|
||||
multiplicateur:false
|
||||
}
|
||||
|
||||
let effect = ({assiette, multiplicateur, tranches}) => {
|
||||
//TODO appliquer retreat() à de, à, taux pour qu'ils puissent contenir des calculs ou pour les cas où toutes les tranches n'ont pas un multiplicateur commun (ex. plafond sécurité sociale). Il faudra alors vérifier leur nullité comme ça :
|
||||
/*
|
||||
nulled = assiette.nodeValue == null || R.any(
|
||||
|
@ -371,10 +405,9 @@ export let mecanismScale = (recurse,k,v) => {
|
|||
)(tranches),
|
||||
*/
|
||||
// nulled = anyNull([assiette, multiplicateur]),
|
||||
nulled = val(assiette) == null || val(multiplicateur) == null,
|
||||
let nulled = val(assiette) == null || val(multiplicateur) == null
|
||||
|
||||
nodeValue =
|
||||
nulled ?
|
||||
return nulled ?
|
||||
null
|
||||
: tranches.reduce((memo, {de: min, 'à': max, taux}) =>
|
||||
( val(assiette) < ( min * val(multiplicateur) ) )
|
||||
|
@ -383,19 +416,16 @@ export let mecanismScale = (recurse,k,v) => {
|
|||
+ ( Math.min(val(assiette), max * val(multiplicateur)) - (min * val(multiplicateur)) )
|
||||
* transformPercentage(taux)
|
||||
, 0)
|
||||
}
|
||||
|
||||
return {
|
||||
nodeValue,
|
||||
category: 'mecanism',
|
||||
name: 'barème',
|
||||
barème: 'en taux marginaux',
|
||||
type: 'numeric',
|
||||
explanation: {
|
||||
assiette,
|
||||
multiplicateur,
|
||||
tranches
|
||||
},
|
||||
jsx: <Node
|
||||
let explanation = {
|
||||
...parseObject(recurse,objectShape,aliased),
|
||||
tranches
|
||||
},
|
||||
evaluate = evaluateObject(objectShape,effect)
|
||||
|
||||
let jsx = (nodeValue, explanation) =>
|
||||
<Node
|
||||
classes="mecanism barème"
|
||||
name="barème"
|
||||
value={nodeValue}
|
||||
|
@ -403,11 +433,11 @@ export let mecanismScale = (recurse,k,v) => {
|
|||
<ul className="properties">
|
||||
<li key="assiette">
|
||||
<span className="key">assiette: </span>
|
||||
<span className="value">{assiette.jsx}</span>
|
||||
<span className="value">{makeJsx(explanation.assiette)}</span>
|
||||
</li>
|
||||
<li key="multiplicateur">
|
||||
<span className="key">multiplicateur des tranches: </span>
|
||||
<span className="value">{multiplicateur.jsx}</span>
|
||||
<span className="value">{makeJsx(explanation.multiplicateur)}</span>
|
||||
</li>
|
||||
<table className="tranches">
|
||||
<thead>
|
||||
|
@ -415,7 +445,7 @@ export let mecanismScale = (recurse,k,v) => {
|
|||
<th>Tranches de l'assiette</th>
|
||||
<th>Taux</th>
|
||||
</tr>
|
||||
{v['tranches'].map(({'en-dessous de': maxOnly, 'au-dessus de': minOnly, de: min, 'à': max, taux}) =>
|
||||
{explanation.tranches.map(({'en-dessous de': maxOnly, 'au-dessus de': minOnly, de: min, 'à': max, taux}) =>
|
||||
<tr key={min || minOnly || 0}>
|
||||
<td>
|
||||
{ maxOnly ? 'En dessous de ' + maxOnly
|
||||
|
@ -430,37 +460,47 @@ export let mecanismScale = (recurse,k,v) => {
|
|||
</ul>
|
||||
}
|
||||
/>
|
||||
|
||||
return {
|
||||
evaluate,
|
||||
jsx,
|
||||
explanation,
|
||||
category: 'mecanism',
|
||||
name: 'barème',
|
||||
barème: 'en taux marginaux',
|
||||
type: 'numeric'
|
||||
}
|
||||
}
|
||||
|
||||
export let mecanismMax = (recurse,k,v) => {
|
||||
let contenders = v.map(recurse),
|
||||
contenderValues = R.pluck('nodeValue')(contenders),
|
||||
stopEverything = R.contains(null, contenderValues),
|
||||
maxValue = R.max(...contenderValues),
|
||||
nodeValue = stopEverything ? null : maxValue
|
||||
let explanation = v.map(recurse)
|
||||
|
||||
return {
|
||||
type: 'numeric',
|
||||
category: 'mecanism',
|
||||
name: 'le maximum de',
|
||||
nodeValue,
|
||||
explanation: contenders,
|
||||
jsx: <Node
|
||||
let evaluate = evaluateArray(R.max,Number.NEGATIVE_INFINITY)
|
||||
|
||||
let jsx = (nodeValue, explanation) =>
|
||||
<Node
|
||||
classes="mecanism list maximum"
|
||||
name="le maximum de"
|
||||
value={nodeValue}
|
||||
child={
|
||||
<ul>
|
||||
{contenders.map((item, i) =>
|
||||
{explanation.map((item, i) =>
|
||||
<li key={i}>
|
||||
<div className="description">{v[i].description}</div>
|
||||
{item.jsx}
|
||||
{makeJsx(item)}
|
||||
</li>
|
||||
)}
|
||||
</ul>
|
||||
}
|
||||
/>
|
||||
|
||||
return {
|
||||
evaluate,
|
||||
jsx,
|
||||
explanation,
|
||||
type: 'numeric',
|
||||
category: 'mecanism',
|
||||
name: 'le maximum de'
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -469,36 +509,31 @@ export let mecanismComplement = (recurse,k,v) => {
|
|||
return decompose(recurse,k,v)
|
||||
}
|
||||
|
||||
if (v['cible'] == null)
|
||||
throw "un complément nécessite une propriété 'cible'"
|
||||
|
||||
let cible = recurse(v['cible']),
|
||||
mini = recurse(v['montant']),
|
||||
nulled = val(cible) == null,
|
||||
nodeValue = nulled ? null : R.subtract(val(mini), R.min(val(cible), val(mini)))
|
||||
let objectShape = {cible:false,montant:false}
|
||||
let effect = ({cible,montant}) => {
|
||||
let nulled = val(cible) == null
|
||||
return nulled ? null : R.subtract(val(montant), R.min(val(cible), val(montant)))
|
||||
}
|
||||
let explanation = parseObject(recurse,objectShape,v)
|
||||
|
||||
return {
|
||||
evaluate: evaluateObject(objectShape,effect),
|
||||
explanation,
|
||||
type: 'numeric',
|
||||
category: 'mecanism',
|
||||
name: 'complément pour atteindre',
|
||||
nodeValue,
|
||||
explanation: {
|
||||
cible,
|
||||
mini
|
||||
},
|
||||
jsx: <Node
|
||||
classes="mecanism list complement"
|
||||
name="complément pour atteindre"
|
||||
value={nodeValue}
|
||||
child={
|
||||
<ul className="properties">
|
||||
<li key="cible">
|
||||
<span className="key">montant calculé: </span>
|
||||
<span className="value">{cible.jsx}</span>
|
||||
<span className="value">{makeJsx(explanation.cible)}</span>
|
||||
</li>
|
||||
<li key="mini">
|
||||
<span className="key">montant à atteindre: </span>
|
||||
<span className="value">{mini.jsx}</span>
|
||||
<span className="value">{makeJsx(explanation.montant)}</span>
|
||||
</li>
|
||||
</ul>
|
||||
}
|
||||
|
|
|
@ -83,14 +83,8 @@ export let searchRules = searchInput =>
|
|||
JSON.stringify(rule).toLowerCase().indexOf(searchInput) > -1)
|
||||
.map(enrichRule)
|
||||
|
||||
export let findRuleByDottedName = (allRules, dottedName) => {
|
||||
let found = dottedName && allRules.find(rule => rule.dottedName.toLowerCase() == dottedName.toLowerCase()),
|
||||
result = dottedName && dottedName.startsWith("sys .") ?
|
||||
found || {dottedName: dottedName, nodeValue: null} :
|
||||
found
|
||||
|
||||
return result
|
||||
}
|
||||
export let findRuleByDottedName = (allRules, dottedName) =>
|
||||
dottedName && allRules.find(rule => rule.dottedName.toLowerCase() == dottedName.toLowerCase())
|
||||
|
||||
/*********************************
|
||||
Autres */
|
||||
|
|
|
@ -6,8 +6,9 @@ import knownMecanisms from './known-mecanisms.yaml'
|
|||
import { Parser } from 'nearley'
|
||||
import Grammar from './grammar.ne'
|
||||
import {Node, Leaf} from './traverse-common-jsx'
|
||||
import {mecanismOneOf,mecanismAllOf,mecanismNumericalLogic,mecanismSum,mecanismProduct,
|
||||
import {mecanismOneOf,mecanismAllOf,mecanismNumericalSwitch,mecanismSum,mecanismProduct,
|
||||
mecanismPercentage,mecanismScale,mecanismMax,mecanismError, mecanismComplement} from "./mecanisms"
|
||||
import {evaluateNode, rewriteNode, collectNodeMissing, makeJsx} from './evaluation'
|
||||
|
||||
let nearley = () => new Parser(Grammar.ParserRules, Grammar.ParserStart)
|
||||
|
||||
|
@ -48,80 +49,120 @@ par exemple ainsi : https://github.com/Engelberg/instaparse#transforming-the-tre
|
|||
|
||||
*/
|
||||
|
||||
// Creates a synthetic variable in the system namespace to signal filtering on components
|
||||
let withFilter = (rules, filter) =>
|
||||
R.concat(rules,[{name:"filter", nodeValue:filter, ns:"sys", dottedName: "sys . filter"}])
|
||||
|
||||
let fillVariableNode = (rules, rule, situationGate) => (parseResult) => {
|
||||
let
|
||||
{fragments} = parseResult,
|
||||
variablePartialName = fragments.join(' . '),
|
||||
dottedName = disambiguateRuleReference(rules, rule, variablePartialName),
|
||||
variable = findRuleByDottedName(rules, dottedName),
|
||||
variableIsCalculable = variable.formule != null,
|
||||
//TODO perf : mettre un cache sur les variables !
|
||||
// On le fait pas pour l'instant car ça peut compliquer les fonctionnalités futures
|
||||
// et qu'il n'y a aucun problème de perf aujourd'hui
|
||||
parsedRule = variableIsCalculable && treatRuleRoot(
|
||||
situationGate,
|
||||
rules,
|
||||
variable
|
||||
),
|
||||
|
||||
situationValue = evaluateVariable(situationGate, dottedName, variable),
|
||||
nodeValue2 = situationValue
|
||||
!= null ? situationValue
|
||||
: !variableIsCalculable
|
||||
? null
|
||||
: parsedRule.nodeValue,
|
||||
nodeValue = dottedName.startsWith("sys .") ? variable.nodeValue : nodeValue2,
|
||||
explanation = parsedRule,
|
||||
missingVariables = variableIsCalculable ? [] : (nodeValue == null ? [dottedName] : [])
|
||||
|
||||
let fillFilteredVariableNode = (rules, rule) => (filter, parseResult) => {
|
||||
let evaluateFiltered = originalEval => (situation, parsedRules, node) => {
|
||||
let newSituation = name => name == "sys.filter" ? filter : situation(name)
|
||||
return originalEval(newSituation, parsedRules, node)
|
||||
}
|
||||
let node = fillVariableNode(rules, rule)(parseResult)
|
||||
return {
|
||||
nodeValue,
|
||||
category: 'variable',
|
||||
fragments: fragments,
|
||||
dottedName,
|
||||
type: 'boolean | numeric',
|
||||
explanation: parsedRule,
|
||||
missingVariables,
|
||||
jsx: <Leaf
|
||||
...node,
|
||||
evaluate: evaluateFiltered(node.evaluate)
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: dirty, dirty
|
||||
// ne pas laisser trop longtemps cette "optimisation" qui tue l'aspect fonctionnel de l'algo
|
||||
var dict;
|
||||
|
||||
export let clearDict = () => dict = {}
|
||||
|
||||
let fillVariableNode = (rules, rule) => (parseResult) => {
|
||||
let evaluate = (situation, parsedRules, node) => {
|
||||
let dottedName = node.dottedName,
|
||||
// On va vérifier dans le cache courant, dict, si la variable n'a pas été déjà évaluée
|
||||
// En effet, l'évaluation dans le cas d'une variable qui a une formule, est coûteuse !
|
||||
cached = dict[dottedName],
|
||||
// make parsedRules a dict object, that also serves as a cache of evaluation ?
|
||||
variable = cached ? cached : findRuleByDottedName(parsedRules, dottedName),
|
||||
variableIsCalculable = variable.formule != null,
|
||||
|
||||
parsedRule = variableIsCalculable && (cached ? cached : evaluateNode(
|
||||
situation,
|
||||
parsedRules,
|
||||
variable
|
||||
)),
|
||||
// evaluateVariable renvoit la valeur déduite de la situation courante renseignée par l'utilisateur
|
||||
situationValue = evaluateVariable(situation, dottedName, variable),
|
||||
nodeValue = situationValue
|
||||
!= null ? situationValue // cette variable a été directement renseignée
|
||||
: !variableIsCalculable
|
||||
? null // pas moyen de calculer car il n'y a pas de formule, elle restera donc nulle
|
||||
: parsedRule.nodeValue, // la valeur du calcul fait foi
|
||||
explanation = parsedRule,
|
||||
missingVariables = variableIsCalculable ? [] : (nodeValue == null ? [dottedName] : [])
|
||||
|
||||
let collectMissing = node =>
|
||||
variableIsCalculable ? collectNodeMissing(parsedRule) : node.missingVariables
|
||||
|
||||
let result = cached ? cached : {
|
||||
...rewriteNode(node,nodeValue,explanation,collectMissing),
|
||||
missingVariables,
|
||||
}
|
||||
dict[dottedName] = result
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
let {fragments} = parseResult,
|
||||
variablePartialName = fragments.join(' . '),
|
||||
dottedName = disambiguateRuleReference(rules, rule, variablePartialName)
|
||||
|
||||
let jsx = (nodeValue, explanation) =>
|
||||
<Leaf
|
||||
classes="variable"
|
||||
name={fragments.join(' . ')}
|
||||
value={nodeValue}
|
||||
/>
|
||||
|
||||
return {
|
||||
evaluate,
|
||||
jsx,
|
||||
name: variablePartialName,
|
||||
category: 'variable',
|
||||
fragments,
|
||||
dottedName,
|
||||
type: 'boolean | numeric'
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
let buildNegatedVariable = variable => {
|
||||
let nodeValue = variable.nodeValue == null ? null : !variable.nodeValue
|
||||
return {
|
||||
nodeValue,
|
||||
category: 'mecanism',
|
||||
name: 'négation',
|
||||
type: 'boolean',
|
||||
explanation: variable,
|
||||
jsx: <Node
|
||||
let evaluate = (situation, parsedRules, node) => {
|
||||
let explanation = evaluateNode(situation, parsedRules, node.explanation),
|
||||
nodeValue = explanation.nodeValue == null ? null : !explanation.nodeValue
|
||||
let collectMissing = node => collectNodeMissing(node.explanation)
|
||||
return rewriteNode(node,nodeValue,explanation,collectMissing)
|
||||
}
|
||||
|
||||
let jsx = (nodeValue, explanation) =>
|
||||
<Node
|
||||
classes="inlineExpression negation"
|
||||
value={nodeValue}
|
||||
value={node.nodeValue}
|
||||
child={
|
||||
<span className="nodeContent">
|
||||
<span className="operator">¬</span>
|
||||
{variable.jsx}
|
||||
{makeJsx(explanation)}
|
||||
</span>
|
||||
}
|
||||
/>
|
||||
|
||||
return {
|
||||
evaluate,
|
||||
jsx,
|
||||
category: 'mecanism',
|
||||
name: 'négation',
|
||||
type: 'boolean',
|
||||
explanation: variable
|
||||
}
|
||||
}
|
||||
|
||||
let treat = (situationGate, rules, rule) => rawNode => {
|
||||
let treat = (rules, rule) => rawNode => {
|
||||
// inner functions
|
||||
let reTreat = treat(situationGate, rules, rule),
|
||||
let
|
||||
reTreat = treat(rules, rule),
|
||||
treatString = rawNode => {
|
||||
/* On a à faire à un string, donc à une expression infixe.
|
||||
Elle sera traité avec le parser obtenu grâce à NearleyJs et notre grammaire.
|
||||
/* On a affaire à un string, donc à une expression infixe.
|
||||
Elle sera traité avec le parser obtenu grâce à NearleyJs et notre grammaire `grammar.ne`.
|
||||
On obtient un objet de type Variable (avec potentiellement un 'modifier', par exemple temporel (TODO)), CalcExpression ou Comparison.
|
||||
Cet objet est alors rebalancé à 'treat'.
|
||||
*/
|
||||
|
@ -135,105 +176,78 @@ let treat = (situationGate, rules, rule) => rawNode => {
|
|||
throw "Attention ! Erreur de traitement de l'expression : " + rawNode
|
||||
|
||||
if (parseResult.category == 'variable')
|
||||
return fillVariableNode(rules, rule, situationGate)(parseResult)
|
||||
return fillVariableNode(rules, rule)(parseResult)
|
||||
if (parseResult.category == 'filteredVariable') {
|
||||
let newRules = withFilter(rules,parseResult.filter)
|
||||
return fillVariableNode(newRules, rule, situationGate)(parseResult.variable)
|
||||
return fillFilteredVariableNode(rules, rule)(parseResult.filter,parseResult.variable)
|
||||
}
|
||||
if (parseResult.category == 'negatedVariable')
|
||||
return buildNegatedVariable(
|
||||
fillVariableNode(rules, rule, situationGate)(parseResult.variable)
|
||||
fillVariableNode(rules, rule)(parseResult.variable)
|
||||
)
|
||||
|
||||
if (parseResult.category == 'calcExpression') {
|
||||
let
|
||||
fillVariable = fillVariableNode(rules, rule, situationGate),
|
||||
fillFiltered = parseResult => fillVariableNode(withFilter(rules,parseResult.filter), rule, situationGate)(parseResult.variable),
|
||||
if (parseResult.category == 'calcExpression' || parseResult.category == 'comparison') {
|
||||
let evaluate = (situation, parsedRules, node) => {
|
||||
let
|
||||
operatorFunctionName = {
|
||||
'*': 'multiply',
|
||||
'/': 'divide',
|
||||
'+': 'add',
|
||||
'-': 'subtract',
|
||||
'<': 'lt',
|
||||
'<=': 'lte',
|
||||
'>': 'gt',
|
||||
'>=': 'gte'
|
||||
}[node.operator],
|
||||
explanation = R.map(R.curry(evaluateNode)(situation,parsedRules),node.explanation),
|
||||
value1 = explanation[0].nodeValue,
|
||||
value2 = explanation[1].nodeValue,
|
||||
operatorFunction = R[operatorFunctionName],
|
||||
nodeValue = value1 == null || value2 == null ?
|
||||
null
|
||||
: operatorFunction(value1, value2)
|
||||
|
||||
let collectMissing = node => R.chain(collectNodeMissing,node.explanation)
|
||||
|
||||
return rewriteNode(node,nodeValue,explanation,collectMissing)
|
||||
}
|
||||
|
||||
let fillFiltered = parseResult => fillFilteredVariableNode(rules, rule)(parseResult.filter,parseResult.variable)
|
||||
let fillVariable = fillVariableNode(rules, rule),
|
||||
filledExplanation = parseResult.explanation.map(
|
||||
R.cond([
|
||||
[R.propEq('category', 'variable'), fillVariable],
|
||||
[R.propEq('category', 'filteredVariable'), fillFiltered],
|
||||
[R.propEq('category', 'value'), node =>
|
||||
R.assoc('jsx', <span className="value">
|
||||
{node.nodeValue}
|
||||
</span>)(node)
|
||||
({
|
||||
evaluate: (situation, parsedRules, me) => ({...me, nodeValue: parseInt(node.nodeValue)}),
|
||||
jsx: nodeValue => <span className="value">{nodeValue}</span>
|
||||
})
|
||||
]
|
||||
])
|
||||
),
|
||||
[{nodeValue: value1}, {nodeValue: value2}] = filledExplanation,
|
||||
operatorFunctionName = {
|
||||
'*': 'multiply',
|
||||
'/': 'divide',
|
||||
'+': 'add',
|
||||
'-': 'subtract'
|
||||
}[parseResult.operator],
|
||||
operatorFunction = R[operatorFunctionName],
|
||||
nodeValue = value1 == null || value2 == null ?
|
||||
null
|
||||
: operatorFunction(value1, value2)
|
||||
operator = parseResult.operator
|
||||
|
||||
return {
|
||||
text: rawNode,
|
||||
nodeValue,
|
||||
category: 'calcExpression',
|
||||
type: 'numeric',
|
||||
explanation: filledExplanation,
|
||||
jsx: <Node
|
||||
classes="inlineExpression calcExpression"
|
||||
let jsx = (nodeValue, explanation) =>
|
||||
<Node
|
||||
classes={"inlineExpression "+parseResult.category}
|
||||
value={nodeValue}
|
||||
child={
|
||||
<span className="nodeContent">
|
||||
{filledExplanation[0].jsx}
|
||||
{makeJsx(explanation[0])}
|
||||
<span className="operator">{parseResult.operator}</span>
|
||||
{filledExplanation[1].jsx}
|
||||
{makeJsx(explanation[1])}
|
||||
</span>
|
||||
}
|
||||
/>
|
||||
}
|
||||
}
|
||||
if (parseResult.category == 'comparison') {
|
||||
//TODO mutualise code for 'comparison' & 'calclExpression'. Harmonise their names
|
||||
let
|
||||
filledExplanation = parseResult.explanation.map(
|
||||
R.cond([
|
||||
[R.propEq('category', 'variable'), fillVariableNode(rules, rule, situationGate)],
|
||||
[R.propEq('category', 'value'), node =>
|
||||
R.assoc('jsx', <span className="value">
|
||||
{node.nodeValue}
|
||||
</span>)(node)
|
||||
]
|
||||
])
|
||||
),
|
||||
[{nodeValue: value1}, {nodeValue: value2}] = filledExplanation,
|
||||
comparatorFunctionName = {
|
||||
'<': 'lt',
|
||||
'<=': 'lte',
|
||||
'>': 'gt',
|
||||
'>=': 'gte'
|
||||
//TODO '='
|
||||
}[parseResult.operator],
|
||||
comparatorFunction = R[comparatorFunctionName],
|
||||
nodeValue = value1 == null || value2 == null ?
|
||||
null
|
||||
: comparatorFunction(value1, value2)
|
||||
|
||||
return {
|
||||
evaluate,
|
||||
jsx,
|
||||
operator,
|
||||
text: rawNode,
|
||||
nodeValue: nodeValue,
|
||||
category: 'comparison',
|
||||
type: 'boolean',
|
||||
explanation: filledExplanation,
|
||||
jsx: <Node
|
||||
classes="inlineExpression comparison"
|
||||
value={nodeValue}
|
||||
child={
|
||||
<span className="nodeContent">
|
||||
{filledExplanation[0].jsx}
|
||||
<span className="operator">{parseResult.operator}</span>
|
||||
{filledExplanation[1].jsx}
|
||||
</span>
|
||||
}
|
||||
/>
|
||||
category: parseResult.category,
|
||||
type: parseResult.category == 'calcExpression' ? 'numeric' : 'boolean',
|
||||
explanation: filledExplanation
|
||||
}
|
||||
}
|
||||
},
|
||||
|
@ -267,15 +281,16 @@ let treat = (situationGate, rules, rule) => rawNode => {
|
|||
let dispatch = {
|
||||
'une de ces conditions': mecanismOneOf,
|
||||
'toutes ces conditions': mecanismAllOf,
|
||||
'logique numérique': mecanismNumericalLogic,
|
||||
'aiguillage numérique': mecanismNumericalSwitch,
|
||||
'taux': mecanismPercentage,
|
||||
'somme': mecanismSum,
|
||||
'multiplication': mecanismProduct,
|
||||
'barème': mecanismScale,
|
||||
'le maximum de': mecanismMax,
|
||||
'complément': mecanismComplement,
|
||||
'une possibilité': R.always({})
|
||||
},
|
||||
action = R.pathOr(mecanismError,[k],dispatch)
|
||||
action = R.propOr(mecanismError, k, dispatch)
|
||||
|
||||
return action(reTreat,k,v)
|
||||
}
|
||||
|
@ -286,7 +301,12 @@ let treat = (situationGate, rules, rule) => rawNode => {
|
|||
[R.is(Object), treatObject],
|
||||
[R.T, treatOther]
|
||||
])
|
||||
return onNodeType(rawNode)
|
||||
|
||||
let defaultEvaluate = (situationGate, parsedRules, node) => node
|
||||
let parsedNode = onNodeType(rawNode)
|
||||
|
||||
return parsedNode.evaluate ? parsedNode :
|
||||
{...parsedNode, evaluate: defaultEvaluate}
|
||||
}
|
||||
|
||||
//TODO c'est moche :
|
||||
|
@ -301,174 +321,138 @@ export let computeRuleValue = (formuleValue, condValue) =>
|
|||
? 0
|
||||
: formuleValue
|
||||
|
||||
export let treatRuleRoot = (situationGate, rules, rule) => R.pipe(
|
||||
R.evolve({ // -> Voilà les attributs que peut comporter, pour l'instant, une Variable.
|
||||
export let treatRuleRoot = (rules, rule) => {
|
||||
let evaluate = (situationGate, parsedRules, r) => {
|
||||
let
|
||||
evaluated = R.evolve({
|
||||
formule: R.curry(evaluateNode)(situationGate, parsedRules),
|
||||
"non applicable si": R.curry(evaluateNode)(situationGate, parsedRules)
|
||||
},r),
|
||||
formuleValue = evaluated.formule && evaluated.formule.nodeValue,
|
||||
condition = R.prop('non applicable si',evaluated),
|
||||
condValue = condition && condition.nodeValue,
|
||||
nodeValue = computeRuleValue(formuleValue, condValue)
|
||||
|
||||
// 'meta': pas de traitement pour l'instant
|
||||
return {...evaluated, nodeValue}
|
||||
}
|
||||
let collectMissing = node => {
|
||||
let cond = R.prop('non applicable si',node),
|
||||
condMissing = cond ? collectNodeMissing(cond) : [],
|
||||
collectInFormule = (cond && cond.nodeValue != undefined) ? !cond.nodeValue : true,
|
||||
formule = node.formule,
|
||||
formMissing = collectInFormule ? (formule ? collectNodeMissing(formule) : []) : []
|
||||
return R.concat(condMissing,formMissing)
|
||||
}
|
||||
|
||||
// 'cond' : Conditions d'applicabilité de la règle
|
||||
let parsedRoot = R.evolve({ // Voilà les attributs d'une règle qui sont aujourd'hui dynamiques, donc à traiter
|
||||
|
||||
// Les métadonnées d'une règle n'en font pas aujourd'hui partie
|
||||
|
||||
// condition d'applicabilité de la règle
|
||||
'non applicable si': value => {
|
||||
let
|
||||
child = treat(situationGate, rules, rule)(value),
|
||||
nodeValue = child.nodeValue
|
||||
let evaluate = (situationGate, parsedRules, node) => {
|
||||
let collectMissing = node => collectNodeMissing(node.explanation)
|
||||
let explanation = evaluateNode(situationGate, parsedRules, node.explanation),
|
||||
nodeValue = explanation.nodeValue
|
||||
return rewriteNode(node,nodeValue,explanation,collectMissing)
|
||||
}
|
||||
|
||||
return {
|
||||
category: 'ruleProp',
|
||||
rulePropType: 'cond',
|
||||
name: 'non applicable si',
|
||||
type: 'boolean',
|
||||
nodeValue: child.nodeValue,
|
||||
explanation: child,
|
||||
jsx: <Node
|
||||
let child = treat(rules, rule)(value)
|
||||
|
||||
let jsx = (nodeValue, explanation) =>
|
||||
<Node
|
||||
classes="ruleProp mecanism cond"
|
||||
name="non applicable si"
|
||||
value={nodeValue}
|
||||
child={
|
||||
child.category === 'variable' ? <div className="node">{child.jsx}</div>
|
||||
: child.jsx
|
||||
explanation.category === 'variable' ? <div className="node">{makeJsx(explanation)}</div>
|
||||
: makeJsx(explanation)
|
||||
}
|
||||
/>
|
||||
|
||||
return {
|
||||
evaluate,
|
||||
jsx,
|
||||
category: 'ruleProp',
|
||||
rulePropType: 'cond',
|
||||
name: 'non applicable si',
|
||||
type: 'boolean',
|
||||
explanation: child
|
||||
}
|
||||
}
|
||||
,
|
||||
// [n'importe quel mécanisme booléen] : expression booléenne (simple variable, négation, égalité, comparaison numérique, test d'inclusion court / long) || l'une de ces conditions || toutes ces conditions
|
||||
// 'applicable si': // pareil mais inversé !
|
||||
|
||||
// note: pour certaines variables booléennes, ex. appartenance à régime Alsace-Moselle, la formule et le non applicable si se rejoignent
|
||||
// [n'importe quel mécanisme numérique] : multiplication || barème en taux marginaux || le maximum de || le minimum de || ...
|
||||
'formule': value => {
|
||||
let
|
||||
child = treat(situationGate, rules, rule)(value),
|
||||
nodeValue = child.nodeValue
|
||||
let evaluate = (situationGate, parsedRules, node) => {
|
||||
let collectMissing = node => collectNodeMissing(node.explanation)
|
||||
let explanation = evaluateNode(situationGate, parsedRules, node.explanation),
|
||||
nodeValue = explanation.nodeValue
|
||||
return rewriteNode(node,nodeValue,explanation,collectMissing)
|
||||
}
|
||||
|
||||
let child = treat(rules, rule)(value)
|
||||
|
||||
let jsx = (nodeValue, explanation) =>
|
||||
<Node
|
||||
classes="ruleProp mecanism formula"
|
||||
name="formule"
|
||||
value={nodeValue}
|
||||
child={makeJsx(explanation)}
|
||||
/>
|
||||
|
||||
return {
|
||||
evaluate,
|
||||
jsx,
|
||||
category: 'ruleProp',
|
||||
rulePropType: 'formula',
|
||||
name: 'formule',
|
||||
type: 'numeric',
|
||||
nodeValue: nodeValue,
|
||||
explanation: child,
|
||||
shortCircuit: R.pathEq(['non applicable si', 'nodeValue'], true),
|
||||
jsx: <Node
|
||||
classes="ruleProp mecanism formula"
|
||||
name="formule"
|
||||
value={nodeValue}
|
||||
child={
|
||||
child.jsx
|
||||
}
|
||||
/>
|
||||
explanation: child
|
||||
}
|
||||
}
|
||||
,
|
||||
// TODO les mécanismes de composantes et de variations utilisables un peu partout !
|
||||
// TODO 'temporal': information concernant les périodes : à définir !
|
||||
// TODO 'intéractions': certaines variables vont en modifier d'autres : ex. Fillon va réduire voir annuler (set 0) une liste de cotisations
|
||||
// ... ?
|
||||
|
||||
}),
|
||||
/* Calcul de la valeur de la variable en combinant :
|
||||
- les conditions d'application ('non applicable si')
|
||||
- la formule
|
||||
})(rule)
|
||||
|
||||
TODO: mettre les conditions d'application dans "formule", et traiter la formule comme un mécanisme normal dans treat()
|
||||
|
||||
*/
|
||||
r => {
|
||||
let
|
||||
formuleValue = r.formule.nodeValue,
|
||||
condValue = R.path(['non applicable si', 'nodeValue'])(r),
|
||||
nodeValue = computeRuleValue(formuleValue, condValue)
|
||||
|
||||
return {...r, nodeValue}
|
||||
return {
|
||||
// Pas de propriété explanation et jsx ici car on est parti du (mauvais) principe que 'non applicable si' et 'formule' sont particuliers, alors qu'ils pourraient être rangé avec les autres mécanismes
|
||||
...parsedRoot,
|
||||
evaluate,
|
||||
collectMissing
|
||||
}
|
||||
)(rule)
|
||||
}
|
||||
|
||||
|
||||
/* Analyse the set of selected rules, and add derived information to them :
|
||||
- do they need variables that are not present in the user situation ?
|
||||
- if not, do they have a computed value or are they non applicable ?
|
||||
*/
|
||||
|
||||
export let analyseSituation = (rules, rootVariable) => situationGate =>
|
||||
treatRuleRoot(
|
||||
situationGate,
|
||||
rules,
|
||||
findRuleByName(rules, rootVariable)
|
||||
)
|
||||
export let analyseSituation = (rules, rootVariable) => situationGate => {
|
||||
let {root, parsedRules} = analyseTopDown(rules,rootVariable)(situationGate)
|
||||
return root
|
||||
}
|
||||
|
||||
|
||||
|
||||
export let analyseTopDown = (rules, rootVariable) => situationGate => {
|
||||
clearDict()
|
||||
let
|
||||
/*
|
||||
La fonction treatRuleRoot va descendre l'arbre de la règle `rule` et produire un AST, un objet contenant d'autres objets contenant d'autres objets...
|
||||
Aujourd'hui, une règle peut avoir (comme propriétés à parser) `non applicable si` et `formule`,
|
||||
qui ont elles-mêmes des propriétés de type mécanisme (ex. barème) ou des expressions en ligne (ex. maVariable + 3).
|
||||
Ces mécanismes où variables sont descendues à leur tour grâce à `treat()`.
|
||||
Lors de ce traitement, des fonctions 'evaluate', `collectMissingVariables` et `jsx` sont attachés aux objets de l'AST
|
||||
*/
|
||||
treatOne = rule => treatRuleRoot(rules, rule),
|
||||
|
||||
//On fait ainsi pour chaque règle de la base.
|
||||
parsedRules = R.map(treatOne,rules),
|
||||
rootRule = findRuleByName(parsedRules, rootVariable),
|
||||
|
||||
/*
|
||||
Ce n'est que dans cette nouvelle étape que l'arbre est vraiment évalué.
|
||||
Auparavant, l'évaluation était faite lors de la construction de l'AST.
|
||||
*/
|
||||
root = evaluateNode(situationGate, parsedRules, rootRule)
|
||||
|
||||
|
||||
|
||||
/*--------------------------------------------------------------------------------
|
||||
Ce qui suit est la première tentative d'écriture du principe du moteur et de la syntaxe */
|
||||
|
||||
// let types = {
|
||||
/*
|
||||
(expression):
|
||||
| (variable)
|
||||
| (négation)
|
||||
| (égalité)
|
||||
| (comparaison numérique)
|
||||
| (test d'inclusion court)
|
||||
*/
|
||||
|
||||
// }
|
||||
|
||||
|
||||
/*
|
||||
Variable:
|
||||
- applicable si: (boolean logic)
|
||||
- non applicable si: (boolean logic)
|
||||
- concerne: (expression)
|
||||
- ne concerne pas: (expression)
|
||||
|
||||
(boolean logic):
|
||||
toutes ces conditions: ([expression | boolean logic])
|
||||
une de ces conditions: ([expression | boolean logic])
|
||||
conditions exclusives: ([expression | boolean logic])
|
||||
|
||||
"If you write a regular expression, walk away for a cup of coffee, come back, and can't easily understand what you just wrote, then you should look for a clearer way to express what you're doing."
|
||||
|
||||
Les expressions sont le seul mécanisme relativement embêtant pour le moteur. Dans un premier temps, il les gerera au moyen d'expressions régulières, puis il faudra probablement mieux s'équiper avec un "javascript parser generator" :
|
||||
https://medium.com/@daffl/beyond-regex-writing-a-parser-in-javascript-8c9ed10576a6
|
||||
|
||||
(variable): (string)
|
||||
|
||||
(négation):
|
||||
! (variable)
|
||||
|
||||
(égalité):
|
||||
(variable) = (variable.type)
|
||||
|
||||
(comparaison numérique):
|
||||
| (variable) < (variable.type)
|
||||
| (variable) <= (variable.type)
|
||||
| (variable) > (variable.type)
|
||||
| (variable) <= (variable.type)
|
||||
|
||||
(test d'inclusion court):
|
||||
(variable) ⊂ [variable.type]
|
||||
|
||||
in Variable.formule :
|
||||
- composantes
|
||||
- linéaire
|
||||
- barème en taux marginaux
|
||||
- test d'inclusion: (test d'inclusion)
|
||||
|
||||
(test d'inclusion):
|
||||
variable: (variable)
|
||||
possibilités: [variable.type]
|
||||
|
||||
# pas nécessaire pour le CDD
|
||||
|
||||
in Variable
|
||||
- variations: [si]
|
||||
|
||||
(si):
|
||||
si: (expression)
|
||||
# corps
|
||||
|
||||
*/
|
||||
return {
|
||||
root,
|
||||
parsedRules
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,7 +9,7 @@ import {buildNextSteps, generateGridQuestions, generateSimpleQuestions} from 'En
|
|||
import computeThemeColours from 'Components/themeColours'
|
||||
import { STEP_ACTION, START_CONVERSATION, EXPLAIN_VARIABLE, POINT_OUT_OBJECTIVES, CHANGE_THEME_COLOUR} from './actions'
|
||||
|
||||
import {analyseSituation} from 'Engine/traverse'
|
||||
import {analyseTopDown} from 'Engine/traverse'
|
||||
|
||||
let situationGate = state =>
|
||||
name => formValueSelector('conversation')(state, name)
|
||||
|
@ -17,7 +17,7 @@ let situationGate = state =>
|
|||
let analyse = rootVariable => R.pipe(
|
||||
situationGate,
|
||||
// une liste des objectifs de la simulation (des 'rules' aussi nommées 'variables')
|
||||
analyseSituation(rules, rootVariable)
|
||||
analyseTopDown(rules, rootVariable)
|
||||
)
|
||||
|
||||
export let reduceSteps = (state, action) => {
|
||||
|
@ -25,7 +25,7 @@ export let reduceSteps = (state, action) => {
|
|||
if (![START_CONVERSATION, STEP_ACTION].includes(action.type))
|
||||
return state
|
||||
|
||||
let rootVariable = action.type == START_CONVERSATION ? action.rootVariable : state.analysedSituation.name
|
||||
let rootVariable = action.type == START_CONVERSATION ? action.rootVariable : state.analysedSituation.root.name
|
||||
|
||||
let returnObject = {
|
||||
...state,
|
||||
|
@ -35,15 +35,15 @@ export let reduceSteps = (state, action) => {
|
|||
if (action.type == START_CONVERSATION) {
|
||||
return {
|
||||
...returnObject,
|
||||
foldedSteps: state.foldedSteps || [],
|
||||
unfoldedSteps: buildNextSteps(rules, returnObject.analysedSituation)
|
||||
foldedSteps: [],
|
||||
unfoldedSteps: buildNextSteps(situationGate(state), rules, returnObject.analysedSituation)
|
||||
}
|
||||
}
|
||||
if (action.type == STEP_ACTION && action.name == 'fold') {
|
||||
return {
|
||||
...returnObject,
|
||||
foldedSteps: [...state.foldedSteps, R.head(state.unfoldedSteps)],
|
||||
unfoldedSteps: buildNextSteps(rules, returnObject.analysedSituation)
|
||||
unfoldedSteps: buildNextSteps(situationGate(state), rules, returnObject.analysedSituation)
|
||||
}
|
||||
}
|
||||
if (action.type == STEP_ACTION && action.name == 'unfold') {
|
||||
|
|
|
@ -0,0 +1,38 @@
|
|||
var webpack = require('webpack'),
|
||||
path = require('path')
|
||||
|
||||
module.exports = {
|
||||
devtool: 'cheap-module-source-map',
|
||||
resolve: {
|
||||
alias: {
|
||||
Engine: path.resolve('source/engine/'),
|
||||
Règles: path.resolve('règles/'),
|
||||
Components: path.resolve('source/components/')
|
||||
}
|
||||
},
|
||||
module: {
|
||||
loaders: [ {// slow : ~ 1s
|
||||
test: /\.css$/,
|
||||
loader: 'ignore-loader'
|
||||
}, {
|
||||
test: /\.html$/,
|
||||
loader: 'ignore-loader'
|
||||
},
|
||||
{
|
||||
test: /\.yaml$/,
|
||||
loader: 'json-loader!yaml-loader'
|
||||
},
|
||||
{
|
||||
test: /\.js$/,
|
||||
exclude: /node_modules/,
|
||||
loader: 'babel-loader'
|
||||
},
|
||||
{ //slow : ~ 3 seconds
|
||||
test: /\.(jpe?g|png|gif|svg)$/i,
|
||||
loader: 'ignore-loader'
|
||||
}, {
|
||||
test: /\.ne$/,
|
||||
loader: 'babel-loader!nearley-loader'
|
||||
}]
|
||||
}
|
||||
}
|
|
@ -1,54 +1,145 @@
|
|||
import R from 'ramda'
|
||||
import {expect} from 'chai'
|
||||
import {rules, enrichRule} from '../source/engine/rules'
|
||||
import {analyseSituation} from '../source/engine/traverse'
|
||||
import {rules as realRules, enrichRule} from '../source/engine/rules'
|
||||
import {analyseSituation, analyseTopDown} from '../source/engine/traverse'
|
||||
import {buildNextSteps, collectMissingVariables, getObjectives} from '../source/engine/generateQuestions'
|
||||
|
||||
let stateSelector = (state, name) => null
|
||||
let stateSelector = (name) => null
|
||||
|
||||
describe('collectMissingVariables', function() {
|
||||
describe('getObjectives', function() {
|
||||
|
||||
it('should derive objectives from the root rule', function() {
|
||||
let rawRules = [
|
||||
{nom: "startHere", formule: {somme: [2, "deux"]}, espace: "sum"},
|
||||
{nom: "deux", formule: 2, "non applicable si" : "sum . evt . ko", espace: "sum"},
|
||||
{nom: "startHere", formule: 2, "non applicable si" : "sum . evt . ko", espace: "sum"},
|
||||
{nom: "evt", espace: "sum", formule: {"une possibilité":["ko"]}, titre: "Truc", question:"?"},
|
||||
{nom: "ko", espace: "sum . evt"}],
|
||||
rules = rawRules.map(enrichRule),
|
||||
situation = analyseSituation(rules,"startHere")(stateSelector),
|
||||
result = getObjectives(situation)
|
||||
{root, parsedRules} = analyseTopDown(rules,"startHere")(stateSelector),
|
||||
result = getObjectives(stateSelector, root, parsedRules)
|
||||
|
||||
expect(result).to.have.lengthOf(1)
|
||||
expect(result[0]).to.have.property('name','deux')
|
||||
expect(result[0]).to.have.property('name','startHere')
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('collectMissingVariables', function() {
|
||||
|
||||
it('should identify missing variables', function() {
|
||||
let rawRules = [
|
||||
{nom: "startHere", formule: {somme: [2, "deux"]}, espace: "sum"},
|
||||
{nom: "deux", formule: 2, "non applicable si" : "sum . evt . ko", espace: "sum"},
|
||||
{nom: "startHere", formule: 2, "non applicable si" : "sum . evt . ko", espace: "sum"},
|
||||
{nom: "evt", espace: "sum", formule: {"une possibilité":["ko"]}, titre: "Truc", question:"?"},
|
||||
{nom: "ko", espace: "sum . evt"}],
|
||||
rules = rawRules.map(enrichRule),
|
||||
situation = analyseSituation(rules,"startHere")(stateSelector),
|
||||
result = collectMissingVariables()(situation)
|
||||
|
||||
situation = analyseTopDown(rules,"startHere")(stateSelector),
|
||||
result = collectMissingVariables()(stateSelector,situation)
|
||||
expect(result).to.have.property('sum . evt . ko')
|
||||
});
|
||||
|
||||
it('should identify missing variables mentioned in expressions', function() {
|
||||
let rawRules = [
|
||||
{nom: "startHere", formule: {somme: [2, "deux"]}, espace: "sum"},
|
||||
{nom: "deux", formule: 2, "non applicable si" : "evt . nyet > evt . nope", espace: "sum"},
|
||||
{nom: "startHere", formule: 2, "non applicable si" : "evt . nyet > evt . nope", espace: "sum"},
|
||||
{nom: "nope", espace: "sum . evt"},
|
||||
{nom: "nyet", espace: "sum . evt"}],
|
||||
rules = rawRules.map(enrichRule),
|
||||
situation = analyseSituation(rules,"startHere")(stateSelector),
|
||||
result = collectMissingVariables()(situation)
|
||||
situation = analyseTopDown(rules,"startHere")(stateSelector),
|
||||
result = collectMissingVariables()(stateSelector,situation)
|
||||
|
||||
expect(result).to.have.property('sum . evt . nyet')
|
||||
expect(result).to.have.property('sum . evt . nope')
|
||||
});
|
||||
|
||||
it('should ignore missing variables in the formula if not applicable', function() {
|
||||
let rawRules = [
|
||||
{nom: "startHere", formule: "trois", "non applicable si" : "3 > 2", espace: "sum"},
|
||||
{nom: "trois", espace: "sum"}],
|
||||
rules = rawRules.map(enrichRule),
|
||||
situation = analyseTopDown(rules,"startHere")(stateSelector),
|
||||
result = collectMissingVariables()(stateSelector,situation)
|
||||
|
||||
expect(result).to.deep.equal({})
|
||||
});
|
||||
|
||||
it('should not report missing variables when "one of these" short-circuits', function() {
|
||||
let rawRules = [
|
||||
{nom: "startHere", formule: "trois", "non applicable si" : {"une de ces conditions": ["3 > 2", "trois"]}, espace: "sum"},
|
||||
{nom: "trois", espace: "sum"}],
|
||||
rules = rawRules.map(enrichRule),
|
||||
situation = analyseTopDown(rules,"startHere")(stateSelector),
|
||||
result = collectMissingVariables()(stateSelector,situation)
|
||||
|
||||
expect(result).to.deep.equal({})
|
||||
});
|
||||
|
||||
it('should report missing variables in switch statements', function() {
|
||||
let rawRules = [
|
||||
{ nom: "startHere", formule: {"aiguillage numérique": {
|
||||
"11 > dix":"1000%",
|
||||
"3 > dix":"1100%",
|
||||
"1 > dix":"1200%"
|
||||
}}, espace: "top"},
|
||||
{nom: "dix", espace: "top"}],
|
||||
rules = rawRules.map(enrichRule),
|
||||
situation = analyseTopDown(rules,"startHere")(stateSelector),
|
||||
result = collectMissingVariables()(stateSelector,situation)
|
||||
|
||||
expect(result).to.have.property('top . dix')
|
||||
});
|
||||
|
||||
it('should not report missing variables in switch for consequences of false conditions', function() {
|
||||
let rawRules = [
|
||||
{ nom: "startHere", formule: {"aiguillage numérique": {
|
||||
"8 > 10":"1000%",
|
||||
"1 > 2":"dix"
|
||||
}}, espace: "top"},
|
||||
{nom: "dix", espace: "top"}],
|
||||
rules = rawRules.map(enrichRule),
|
||||
situation = analyseTopDown(rules,"startHere")(stateSelector),
|
||||
result = collectMissingVariables()(stateSelector,situation)
|
||||
|
||||
expect(result).to.deep.equal({})
|
||||
});
|
||||
|
||||
it('should report missing variables in consequence when its condition is unresolved', function() {
|
||||
let rawRules = [
|
||||
{ nom: "startHere",
|
||||
formule: {
|
||||
"aiguillage numérique": {
|
||||
"10 > 11": "1000%",
|
||||
"3 > dix": {
|
||||
"douze": "560%",
|
||||
"1 > 2": "75015%" }
|
||||
}
|
||||
},
|
||||
espace: "top"
|
||||
},
|
||||
{ nom: "douze", espace: "top" },
|
||||
{ nom: "dix", espace: "top" }
|
||||
],
|
||||
rules = rawRules.map(enrichRule),
|
||||
situation = analyseTopDown(rules, "startHere")(stateSelector),
|
||||
result = collectMissingVariables()(stateSelector, situation);
|
||||
|
||||
|
||||
expect(result).to.have.property('top . dix')
|
||||
expect(result).to.have.property('top . douze')
|
||||
});
|
||||
|
||||
it('should not report missing variables when a switch short-circuits', function() {
|
||||
let rawRules = [
|
||||
{ nom: "startHere", formule: {"aiguillage numérique": {
|
||||
"11 > 10":"1000%",
|
||||
"3 > dix":"1100%",
|
||||
"1 > dix":"1200%"
|
||||
}}, espace: "top"},
|
||||
{nom: "dix", espace: "top"}],
|
||||
rules = rawRules.map(enrichRule),
|
||||
situation = analyseTopDown(rules,"startHere")(stateSelector),
|
||||
result = collectMissingVariables()(stateSelector,situation)
|
||||
|
||||
expect(result).to.deep.equal({})
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('buildNextSteps', function() {
|
||||
|
@ -60,11 +151,27 @@ describe('buildNextSteps', function() {
|
|||
{nom: "evt", espace: "top . sum", formule: {"une possibilité":["ko"]}, titre: "Truc", question:"?"},
|
||||
{nom: "ko", espace: "top . sum . evt"}],
|
||||
rules = rawRules.map(enrichRule),
|
||||
situation = analyseSituation(rules,"sum")(stateSelector),
|
||||
result = buildNextSteps(rules, situation)
|
||||
situation = analyseTopDown(rules,"sum")(stateSelector),
|
||||
result = buildNextSteps(stateSelector, rules, situation)
|
||||
|
||||
expect(result).to.have.lengthOf(1)
|
||||
expect(R.path(["question","props","label"])(result[0])).to.equal("?")
|
||||
});
|
||||
|
||||
it('should generate questions from the real rules', function() {
|
||||
let rules = realRules.map(enrichRule),
|
||||
situation = analyseTopDown(rules,"surcoût CDD")(stateSelector),
|
||||
objectives = getObjectives(stateSelector, situation.root, situation.parsedRules),
|
||||
result = buildNextSteps(stateSelector, rules, situation)
|
||||
|
||||
expect(objectives).to.have.lengthOf(4)
|
||||
expect(result).to.have.lengthOf(6)
|
||||
expect(R.path(["question","props","label"])(result[0])).to.equal("Pensez-vous être confronté à l'un de ces événements au cours du contrat ?")
|
||||
expect(R.path(["question","props","label"])(result[1])).to.equal("Quel est le motif de recours au CDD ?")
|
||||
expect(R.path(["question","props","label"])(result[2])).to.equal("Quel est le salaire brut ?")
|
||||
expect(R.path(["question","props","label"])(result[3])).to.equal("Est-ce un contrat jeune vacances ?")
|
||||
expect(R.path(["question","props","label"])(result[4])).to.equal("Quelle est la durée du contrat ?")
|
||||
expect(R.path(["question","props","label"])(result[5])).to.equal("Combien de jours de congés ne seront pas pris ?")
|
||||
});
|
||||
|
||||
});
|
||||
|
|
|
@ -1,5 +1,3 @@
|
|||
require('babel-register')();
|
||||
|
||||
var jsdom = require('jsdom/lib/old-api').jsdom;
|
||||
|
||||
var exposedProperties = ['window', 'navigator', 'document'];
|
||||
|
|
|
@ -1,564 +0,0 @@
|
|||
const noop = () => {}
|
||||
|
||||
const loadYaml = (module, filename) => {
|
||||
const yaml = require('js-yaml');
|
||||
module.exports = yaml.safeLoad(fs.readFileSync(filename, 'utf8'));
|
||||
}
|
||||
|
||||
const loadNearley = (module, filename) => {
|
||||
var nearley = require('nearley/lib/nearley.js');
|
||||
var compile = require('nearley/lib/compile.js');
|
||||
var generate = require('nearley/lib/generate.js');
|
||||
var grammar = require('nearley/lib/nearley-language-bootstrapped.js');
|
||||
|
||||
var parser = new nearley.Parser(grammar.ParserRules, grammar.ParserStart);
|
||||
parser.feed(fs.readFileSync(filename, 'utf8'));
|
||||
var compilation = compile(parser.results[0], {});
|
||||
var content = generate(compilation, 'Grammar');
|
||||
|
||||
module._compile(content,filename)
|
||||
}
|
||||
|
||||
require.extensions['.yaml'] = loadYaml
|
||||
require.extensions['.ne'] = loadNearley
|
||||
require.extensions['.css'] = noop
|
||||
|
||||
/**
|
||||
* Module dependencies.
|
||||
*/
|
||||
|
||||
var program = require('commander');
|
||||
var path = require('path');
|
||||
var fs = require('fs');
|
||||
var resolve = path.resolve;
|
||||
var exists = fs.existsSync || path.existsSync;
|
||||
var Mocha = require('mocha');
|
||||
var utils = Mocha.utils;
|
||||
var interfaceNames = Object.keys(Mocha.interfaces);
|
||||
var join = path.join;
|
||||
var cwd = process.cwd();
|
||||
var getOptions = require('mocha/bin/options');
|
||||
var mocha = new Mocha();
|
||||
|
||||
/**
|
||||
* Save timer references to avoid Sinon interfering (see GH-237).
|
||||
*/
|
||||
|
||||
var Date = global.Date;
|
||||
var setTimeout = global.setTimeout;
|
||||
var setInterval = global.setInterval;
|
||||
var clearTimeout = global.clearTimeout;
|
||||
var clearInterval = global.clearInterval;
|
||||
|
||||
/**
|
||||
* Files.
|
||||
*/
|
||||
|
||||
var files = [];
|
||||
|
||||
/**
|
||||
* Globals.
|
||||
*/
|
||||
|
||||
var globals = [];
|
||||
|
||||
/**
|
||||
* Requires.
|
||||
*/
|
||||
|
||||
var requires = [];
|
||||
|
||||
// options
|
||||
|
||||
program
|
||||
.usage('[debug] [options] [files]')
|
||||
.option('-A, --async-only', 'force all tests to take a callback (async) or return a promise')
|
||||
.option('-c, --colors', 'force enabling of colors')
|
||||
.option('-C, --no-colors', 'force disabling of colors')
|
||||
.option('-G, --growl', 'enable growl notification support')
|
||||
.option('-O, --reporter-options <k=v,k2=v2,...>', 'reporter-specific options')
|
||||
.option('-R, --reporter <name>', 'specify the reporter to use', 'spec')
|
||||
.option('-S, --sort', 'sort test files')
|
||||
.option('-b, --bail', 'bail after first test failure')
|
||||
.option('-d, --debug', "enable node's debugger, synonym for node --debug")
|
||||
.option('-g, --grep <pattern>', 'only run tests matching <pattern>')
|
||||
.option('-f, --fgrep <string>', 'only run tests containing <string>')
|
||||
.option('-gc, --expose-gc', 'expose gc extension')
|
||||
.option('-i, --invert', 'inverts --grep and --fgrep matches')
|
||||
.option('-r, --require <name>', 'require the given module')
|
||||
.option('-s, --slow <ms>', '"slow" test threshold in milliseconds [75]')
|
||||
.option('-t, --timeout <ms>', 'set test-case timeout in milliseconds [2000]')
|
||||
.option('-u, --ui <name>', 'specify user-interface (' + interfaceNames.join('|') + ')', 'bdd')
|
||||
.option('-w, --watch', 'watch files for changes')
|
||||
.option('--check-leaks', 'check for global variable leaks')
|
||||
.option('--full-trace', 'display the full stack trace')
|
||||
.option('--compilers <ext>:<module>,...', 'use the given module(s) to compile files', list, [])
|
||||
.option('--debug-brk', "enable node's debugger breaking on the first line")
|
||||
.option('--globals <names>', 'allow the given comma-delimited global [names]', list, [])
|
||||
.option('--es_staging', 'enable all staged features')
|
||||
.option('--harmony<_classes,_generators,...>', 'all node --harmony* flags are available')
|
||||
.option('--preserve-symlinks', 'Instructs the module loader to preserve symbolic links when resolving and caching modules')
|
||||
.option('--icu-data-dir', 'include ICU data')
|
||||
.option('--inline-diffs', 'display actual/expected differences inline within each string')
|
||||
.option('--inspect', 'activate devtools in chrome')
|
||||
.option('--inspect-brk', 'activate devtools in chrome and break on the first line')
|
||||
.option('--interfaces', 'display available interfaces')
|
||||
.option('--no-deprecation', 'silence deprecation warnings')
|
||||
.option('--no-exit', 'require a clean shutdown of the event loop: mocha will not call process.exit')
|
||||
.option('--no-timeouts', 'disables timeouts, given implicitly with --debug')
|
||||
.option('--no-warnings', 'silence all node process warnings')
|
||||
.option('--opts <path>', 'specify opts path', 'test/mocha.opts')
|
||||
.option('--perf-basic-prof', 'enable perf linux profiler (basic support)')
|
||||
.option('--napi-modules', 'enable experimental NAPI modules')
|
||||
.option('--prof', 'log statistical profiling information')
|
||||
.option('--log-timer-events', 'Time events including external callbacks')
|
||||
.option('--recursive', 'include sub directories')
|
||||
.option('--reporters', 'display available reporters')
|
||||
.option('--retries <times>', 'set numbers of time to retry a failed test case')
|
||||
.option('--throw-deprecation', 'throw an exception anytime a deprecated function is used')
|
||||
.option('--trace', 'trace function calls')
|
||||
.option('--trace-deprecation', 'show stack traces on deprecations')
|
||||
.option('--trace-warnings', 'show stack traces on node process warnings')
|
||||
.option('--use_strict', 'enforce strict mode')
|
||||
.option('--watch-extensions <ext>,...', 'additional extensions to monitor with --watch', list, [])
|
||||
.option('--delay', 'wait for async suite definition')
|
||||
.option('--allow-uncaught', 'enable uncaught errors to propagate')
|
||||
.option('--forbid-only', 'causes test marked with only to fail the suite')
|
||||
.option('--forbid-pending', 'causes pending tests and test marked with skip to fail the suite');
|
||||
|
||||
program._name = 'mocha';
|
||||
|
||||
// --globals
|
||||
|
||||
program.on('globals', function (val) {
|
||||
globals = globals.concat(list(val));
|
||||
});
|
||||
|
||||
// --reporters
|
||||
|
||||
program.on('reporters', function () {
|
||||
console.log();
|
||||
console.log(' dot - dot matrix');
|
||||
console.log(' doc - html documentation');
|
||||
console.log(' spec - hierarchical spec list');
|
||||
console.log(' json - single json object');
|
||||
console.log(' progress - progress bar');
|
||||
console.log(' list - spec-style listing');
|
||||
console.log(' tap - test-anything-protocol');
|
||||
console.log(' landing - unicode landing strip');
|
||||
console.log(' xunit - xunit reporter');
|
||||
console.log(' min - minimal reporter (great with --watch)');
|
||||
console.log(' json-stream - newline delimited json events');
|
||||
console.log(' markdown - markdown documentation (github flavour)');
|
||||
console.log(' nyan - nyan cat!');
|
||||
console.log();
|
||||
process.exit();
|
||||
});
|
||||
|
||||
// --interfaces
|
||||
|
||||
program.on('interfaces', function () {
|
||||
console.log('');
|
||||
interfaceNames.forEach(function (interfaceName) {
|
||||
console.log(' ' + interfaceName);
|
||||
});
|
||||
console.log('');
|
||||
process.exit();
|
||||
});
|
||||
|
||||
// -r, --require
|
||||
|
||||
module.paths.push(cwd, join(cwd, 'node_modules'));
|
||||
|
||||
program.on('require', function (mod) {
|
||||
var abs = exists(mod) || exists(mod + '.js');
|
||||
if (abs) {
|
||||
mod = resolve(mod);
|
||||
}
|
||||
requires.push(mod);
|
||||
});
|
||||
|
||||
// If not already done, load mocha.opts
|
||||
if (!process.env.LOADED_MOCHA_OPTS) {
|
||||
getOptions();
|
||||
}
|
||||
|
||||
// parse args
|
||||
|
||||
program.parse(process.argv);
|
||||
|
||||
// infinite stack traces
|
||||
|
||||
Error.stackTraceLimit = Infinity; // TODO: config
|
||||
|
||||
// reporter options
|
||||
|
||||
var reporterOptions = {};
|
||||
if (program.reporterOptions !== undefined) {
|
||||
program.reporterOptions.split(',').forEach(function (opt) {
|
||||
var L = opt.split('=');
|
||||
if (L.length > 2 || L.length === 0) {
|
||||
throw new Error("invalid reporter option '" + opt + "'");
|
||||
} else if (L.length === 2) {
|
||||
reporterOptions[L[0]] = L[1];
|
||||
} else {
|
||||
reporterOptions[L[0]] = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// reporter
|
||||
|
||||
mocha.reporter(program.reporter, reporterOptions);
|
||||
|
||||
// load reporter
|
||||
|
||||
var Reporter = null;
|
||||
try {
|
||||
Reporter = require('mocha/lib/reporters/' + program.reporter);
|
||||
} catch (err) {
|
||||
try {
|
||||
Reporter = require(program.reporter);
|
||||
} catch (err2) {
|
||||
throw new Error('reporter "' + program.reporter + '" does not exist');
|
||||
}
|
||||
}
|
||||
|
||||
// --no-colors
|
||||
|
||||
if (!program.colors) {
|
||||
mocha.useColors(false);
|
||||
}
|
||||
|
||||
// --colors
|
||||
|
||||
if (~process.argv.indexOf('--colors') || ~process.argv.indexOf('-c')) {
|
||||
mocha.useColors(true);
|
||||
}
|
||||
|
||||
// --inline-diffs
|
||||
|
||||
if (program.inlineDiffs) {
|
||||
mocha.useInlineDiffs(true);
|
||||
}
|
||||
|
||||
// --slow <ms>
|
||||
|
||||
if (program.slow) {
|
||||
mocha.suite.slow(program.slow);
|
||||
}
|
||||
|
||||
// --no-timeouts
|
||||
|
||||
if (!program.timeouts) {
|
||||
mocha.enableTimeouts(false);
|
||||
}
|
||||
|
||||
// --timeout
|
||||
|
||||
if (program.timeout) {
|
||||
mocha.suite.timeout(program.timeout);
|
||||
}
|
||||
|
||||
// --bail
|
||||
|
||||
mocha.suite.bail(program.bail);
|
||||
|
||||
// --grep
|
||||
|
||||
if (program.grep) {
|
||||
mocha.grep(program.grep);
|
||||
}
|
||||
|
||||
// --fgrep
|
||||
|
||||
if (program.fgrep) {
|
||||
mocha.fgrep(program.fgrep);
|
||||
}
|
||||
|
||||
// --invert
|
||||
|
||||
if (program.invert) {
|
||||
mocha.invert();
|
||||
}
|
||||
|
||||
// --check-leaks
|
||||
|
||||
if (program.checkLeaks) {
|
||||
mocha.checkLeaks();
|
||||
}
|
||||
|
||||
// --stack-trace
|
||||
|
||||
if (program.fullTrace) {
|
||||
mocha.fullTrace();
|
||||
}
|
||||
|
||||
// --growl
|
||||
|
||||
if (program.growl) {
|
||||
mocha.growl();
|
||||
}
|
||||
|
||||
// --async-only
|
||||
|
||||
if (program.asyncOnly) {
|
||||
mocha.asyncOnly();
|
||||
}
|
||||
|
||||
// --delay
|
||||
|
||||
if (program.delay) {
|
||||
mocha.delay();
|
||||
}
|
||||
|
||||
// --allow-uncaught
|
||||
|
||||
if (program.allowUncaught) {
|
||||
mocha.allowUncaught();
|
||||
}
|
||||
|
||||
// --globals
|
||||
|
||||
mocha.globals(globals);
|
||||
|
||||
// --retries
|
||||
|
||||
if (program.retries) {
|
||||
mocha.suite.retries(program.retries);
|
||||
}
|
||||
|
||||
// --forbid-only
|
||||
|
||||
if (program.forbidOnly) mocha.forbidOnly();
|
||||
|
||||
// --forbid-pending
|
||||
|
||||
if (program.forbidPending) mocha.forbidPending();
|
||||
|
||||
// custom compiler support
|
||||
|
||||
var extensions = ['js'];
|
||||
program.compilers.forEach(function (c) {
|
||||
var idx = c.indexOf(':');
|
||||
var ext = c.slice(0, idx);
|
||||
var mod = c.slice(idx + 1);
|
||||
|
||||
if (mod[0] === '.') {
|
||||
mod = join(process.cwd(), mod);
|
||||
}
|
||||
require(mod);
|
||||
extensions.push(ext);
|
||||
program.watchExtensions.push(ext);
|
||||
});
|
||||
|
||||
// requires
|
||||
|
||||
requires.forEach(function (mod) {
|
||||
require(mod);
|
||||
});
|
||||
|
||||
// interface
|
||||
|
||||
mocha.ui(program.ui);
|
||||
|
||||
// args
|
||||
|
||||
var args = program.args;
|
||||
|
||||
// default files to test/*.{js,coffee}
|
||||
|
||||
if (!args.length) {
|
||||
args.push('test');
|
||||
}
|
||||
|
||||
args.forEach(function (arg) {
|
||||
var newFiles;
|
||||
try {
|
||||
newFiles = utils.lookupFiles(arg, extensions, program.recursive);
|
||||
} catch (err) {
|
||||
if (err.message.indexOf('cannot resolve path') === 0) {
|
||||
console.error('Warning: Could not find any test files matching pattern: ' + arg);
|
||||
return;
|
||||
}
|
||||
|
||||
throw err;
|
||||
}
|
||||
|
||||
files = files.concat(newFiles);
|
||||
});
|
||||
|
||||
if (!files.length) {
|
||||
console.error('No test files found');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// resolve
|
||||
|
||||
files = files.map(function (path) {
|
||||
return resolve(path);
|
||||
});
|
||||
|
||||
if (program.sort) {
|
||||
files.sort();
|
||||
}
|
||||
|
||||
// --watch
|
||||
|
||||
var runner;
|
||||
var loadAndRun;
|
||||
var purge;
|
||||
var rerun;
|
||||
|
||||
if (program.watch) {
|
||||
console.log();
|
||||
hideCursor();
|
||||
process.on('SIGINT', function () {
|
||||
showCursor();
|
||||
console.log('\n');
|
||||
process.exit(130);
|
||||
});
|
||||
|
||||
var watchFiles = utils.files(cwd, [ 'js' ].concat(program.watchExtensions));
|
||||
var runAgain = false;
|
||||
|
||||
loadAndRun = function loadAndRun () {
|
||||
try {
|
||||
mocha.files = files;
|
||||
runAgain = false;
|
||||
runner = mocha.run(function () {
|
||||
runner = null;
|
||||
if (runAgain) {
|
||||
rerun();
|
||||
}
|
||||
});
|
||||
} catch (e) {
|
||||
console.log(e.stack);
|
||||
}
|
||||
};
|
||||
|
||||
purge = function purge () {
|
||||
watchFiles.forEach(function (file) {
|
||||
delete require.cache[file];
|
||||
});
|
||||
};
|
||||
|
||||
loadAndRun();
|
||||
|
||||
rerun = function rerun () {
|
||||
purge();
|
||||
stop();
|
||||
if (!program.grep) {
|
||||
mocha.grep(null);
|
||||
}
|
||||
mocha.suite = mocha.suite.clone();
|
||||
mocha.suite.ctx = new Mocha.Context();
|
||||
mocha.ui(program.ui);
|
||||
loadAndRun();
|
||||
};
|
||||
|
||||
utils.watch(watchFiles, function () {
|
||||
runAgain = true;
|
||||
if (runner) {
|
||||
runner.abort();
|
||||
} else {
|
||||
rerun();
|
||||
}
|
||||
});
|
||||
} else {
|
||||
// load
|
||||
|
||||
mocha.files = files;
|
||||
runner = mocha.run(program.exit ? exit : exitLater);
|
||||
}
|
||||
|
||||
function exitLater (code) {
|
||||
process.on('exit', function () {
|
||||
process.exit(Math.min(code, 255));
|
||||
});
|
||||
}
|
||||
|
||||
function exit (code) {
|
||||
var clampedCode = Math.min(code, 255);
|
||||
|
||||
// Eagerly set the process's exit code in case stream.write doesn't
|
||||
// execute its callback before the process terminates.
|
||||
process.exitCode = clampedCode;
|
||||
|
||||
// flush output for Node.js Windows pipe bug
|
||||
// https://github.com/joyent/node/issues/6247 is just one bug example
|
||||
// https://github.com/visionmedia/mocha/issues/333 has a good discussion
|
||||
function done () {
|
||||
if (!(draining--)) {
|
||||
process.exit(clampedCode);
|
||||
}
|
||||
}
|
||||
|
||||
var draining = 0;
|
||||
var streams = [process.stdout, process.stderr];
|
||||
|
||||
streams.forEach(function (stream) {
|
||||
// submit empty write request and wait for completion
|
||||
draining += 1;
|
||||
stream.write('', done);
|
||||
});
|
||||
|
||||
done();
|
||||
}
|
||||
|
||||
process.on('SIGINT', function () {
|
||||
runner.abort();
|
||||
|
||||
// This is a hack:
|
||||
// Instead of `process.exit(130)`, set runner.failures to 130 (exit code for SIGINT)
|
||||
// The amount of failures will be emitted as error code later
|
||||
runner.failures = 130;
|
||||
});
|
||||
|
||||
/**
|
||||
* Parse list.
|
||||
*/
|
||||
|
||||
function list (str) {
|
||||
return str.split(/ *, */);
|
||||
}
|
||||
|
||||
/**
|
||||
* Hide the cursor.
|
||||
*/
|
||||
|
||||
function hideCursor () {
|
||||
process.stdout.write('\u001b[?25l');
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the cursor.
|
||||
*/
|
||||
|
||||
function showCursor () {
|
||||
process.stdout.write('\u001b[?25h');
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop play()ing.
|
||||
*/
|
||||
|
||||
function stop () {
|
||||
process.stdout.write('\u001b[2K');
|
||||
clearInterval(play.timer);
|
||||
}
|
||||
|
||||
/**
|
||||
* Play the given array of strings.
|
||||
*/
|
||||
|
||||
function play (arr, interval) {
|
||||
var len = arr.length;
|
||||
interval = interval || 100;
|
||||
var i = 0;
|
||||
|
||||
play.timer = setInterval(function () {
|
||||
var str = arr[i++ % len];
|
||||
process.stdout.write('\u001b[0G' + str);
|
||||
}, interval);
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
import R from 'ramda'
|
||||
|
||||
let directoryLoaderFunction =
|
||||
require.context('./mécanismes', true, /.yaml$/)
|
||||
|
||||
let items = directoryLoaderFunction.keys().map(directoryLoaderFunction)
|
||||
|
||||
export default items
|
|
@ -0,0 +1,44 @@
|
|||
/*
|
||||
Les mécanismes sont testés dans mécanismes/ comme le sont les variables directement dans la base YAML.
|
||||
On y créée dans chaque fichier une base YAML 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 {enrichRule} from '../source/engine/rules'
|
||||
import {analyseTopDown} from '../source/engine/traverse'
|
||||
import {collectMissingVariables} from '../source/engine/generateQuestions'
|
||||
import testSuites from './load-mecanism-tests'
|
||||
import R from 'ramda'
|
||||
|
||||
describe('Mécanismes', () =>
|
||||
testSuites.map( suite =>
|
||||
suite.map(({exemples, nom, test}) =>
|
||||
exemples && describe(test || 'Nom de test (propriété "test") manquant dans la variable contenant ces "exemples"', () =>
|
||||
exemples.map(({nom: testTexte, situation, 'valeur attendue': valeur, 'variables manquantes': expectedMissing}) =>
|
||||
it(testTexte + '', () => {
|
||||
|
||||
let rules = suite.map(enrichRule),
|
||||
state = situation || {},
|
||||
stateSelector = name => state[name],
|
||||
analysis = analyseTopDown(rules, nom)(stateSelector),
|
||||
missing = collectMissingVariables()(stateSelector,analysis)
|
||||
|
||||
// console.log('JSON.stringify(analysis', JSON.stringify(analysis))
|
||||
if (valeur !== undefined) {
|
||||
expect(analysis.root)
|
||||
.to.have.property(
|
||||
'nodeValue',
|
||||
valeur
|
||||
)
|
||||
}
|
||||
|
||||
if (expectedMissing) {
|
||||
expect(R.keys(missing).sort()).to.eql(expectedMissing.sort())
|
||||
}
|
||||
|
||||
})
|
||||
)
|
||||
))
|
||||
)
|
||||
)
|
|
@ -1,2 +0,0 @@
|
|||
test/**/*.test.js
|
||||
|
|
@ -0,0 +1,78 @@
|
|||
# Utiliser http://romainvaleri.online.fr/ pour se donner des idées de noms de variables originales
|
||||
|
||||
- nom: dégradation mineure
|
||||
|
||||
- nom: dégradation majeure
|
||||
|
||||
- nom: retenue sur dépot de garantie
|
||||
test: Aiguillage numérique simple
|
||||
formule:
|
||||
aiguillage numérique:
|
||||
dégradation mineure: 10%
|
||||
dégradation majeure: 30%
|
||||
|
||||
exemples:
|
||||
- nom: le premier aiguillage est activé -> sa valeur est renvoyée
|
||||
situation:
|
||||
dégradation mineure: oui
|
||||
valeur attendue: 0.1
|
||||
- nom: seul le 2ème aiguillage est activé
|
||||
situation:
|
||||
dégradation mineure: non
|
||||
dégradation majeure: oui
|
||||
valeur attendue: 0.3
|
||||
- nom: aucun aiguillage n'est activé
|
||||
situation:
|
||||
dégradation mineure: non
|
||||
dégradation majeure: non
|
||||
valeur attendue: 0
|
||||
- nom: L'ordre des termes est important
|
||||
situation:
|
||||
dégradation mineure: null
|
||||
dégradation majeure: oui
|
||||
valeur attendue: null
|
||||
|
||||
|
||||
- nom: montant caution
|
||||
format: €
|
||||
|
||||
- nom: deuxième retenue sur dépot de garantie
|
||||
test: Imbrication d'aiguillages numériques
|
||||
formule:
|
||||
aiguillage numérique:
|
||||
dégradation mineure: 5%
|
||||
dégradation majeure:
|
||||
montant caution > 2000: 20%
|
||||
montant caution > 1000: 10%
|
||||
|
||||
|
||||
exemples:
|
||||
- nom: imbrication simple
|
||||
situation:
|
||||
dégradation mineure: oui
|
||||
dégradation majeure: non
|
||||
montant caution: 3000
|
||||
valeur attendue: 0.05
|
||||
- nom: imbrication simple 2
|
||||
situation:
|
||||
dégradation mineure: non
|
||||
dégradation majeure: oui
|
||||
montant caution: 1200
|
||||
valeur attendue: 0.10
|
||||
- nom: imbrication nulle
|
||||
valeur attendue: null
|
||||
variables manquantes:
|
||||
- dégradation mineure
|
||||
- dégradation majeure
|
||||
- montant caution
|
||||
- nom: variables manquantes même si innaccessibles
|
||||
situation:
|
||||
dégradation mineure: non
|
||||
valeur attendue: null
|
||||
variables manquantes:
|
||||
- dégradation majeure
|
||||
- montant caution
|
||||
|
||||
|
||||
|
||||
# pouvoir tester les variables inconnues mais requises ?
|
|
@ -0,0 +1,31 @@
|
|||
- nom: farine
|
||||
format: kg
|
||||
|
||||
- nom: sucre
|
||||
format: kg
|
||||
|
||||
- nom: poids total
|
||||
test: Somme
|
||||
formule:
|
||||
somme:
|
||||
- farine
|
||||
- sucre
|
||||
|
||||
exemples:
|
||||
- nom: somme simple
|
||||
situation:
|
||||
farine: 29000
|
||||
sucre: 200
|
||||
valeur attendue: 29200
|
||||
- nom: un nul dans la somme
|
||||
situation:
|
||||
sucre: 200
|
||||
valeur attendue: null
|
||||
- nom: une somme de nuls
|
||||
situation: # pas de situation
|
||||
valeur attendue: null
|
||||
- nom: un entier + un flotant
|
||||
situation:
|
||||
farine: 2.1
|
||||
sucre: 200
|
||||
valeur attendue: 202.1
|
|
@ -0,0 +1,32 @@
|
|||
|
||||
- nom: dégradation mineure
|
||||
|
||||
- nom: dégradation majeure
|
||||
|
||||
- nom: remboursement dépot de garantie
|
||||
test: Une de ces deux conditions
|
||||
non applicable si:
|
||||
une de ces conditions:
|
||||
- dégradation mineure
|
||||
- dégradation majeure
|
||||
formule:
|
||||
3000
|
||||
|
||||
exemples:
|
||||
- nom: Est vraie -> non applicable -> 0
|
||||
situation:
|
||||
dégradation mineure: oui
|
||||
valeur attendue: 0
|
||||
variables manquantes: []
|
||||
- nom: Est fausse -> en attente de l'autre
|
||||
situation:
|
||||
dégradation majeure: non
|
||||
valeur attendue: null
|
||||
variables manquantes:
|
||||
- dégradation mineure
|
||||
- nom: Toutes fausses -> valeur de la formule
|
||||
situation:
|
||||
dégradation mineure: non
|
||||
dégradation majeure: non
|
||||
valeur attendue: 3000
|
||||
variables manquantes: []
|
|
@ -1,7 +1,7 @@
|
|||
import R from 'ramda'
|
||||
import {expect} from 'chai'
|
||||
import {rules, enrichRule, findVariantsAndRecords} from '../source/engine/rules'
|
||||
import {analyseSituation} from '../source/engine/traverse'
|
||||
import {analyseSituation, analyseTopDown} from '../source/engine/traverse'
|
||||
|
||||
let stateSelector = (state, name) => null
|
||||
|
||||
|
@ -32,7 +32,7 @@ describe('findVariantsAndRecords', function() {
|
|||
{nom: "dix", formule: "cinq", espace: "top"},
|
||||
{nom: "cinq", espace: "top", question:"?"}],
|
||||
rules = rawRules.map(enrichRule),
|
||||
situation = analyseSituation(rules,"startHere")(stateSelector),
|
||||
situation = analyseTopDown(rules,"startHere")(stateSelector),
|
||||
result = findVariantsAndRecords(rules, ['top . cinq'])
|
||||
|
||||
expect(result).to.have.deep.property('recordGroups', {top: ['top . cinq']})
|
||||
|
@ -45,7 +45,7 @@ describe('findVariantsAndRecords', function() {
|
|||
{nom: "evt", espace: "top . sum", formule: {"une possibilité":["ko"]}, titre: "Truc", question:"?"},
|
||||
{nom: "ko", espace: "top . sum . evt"}],
|
||||
rules = rawRules.map(enrichRule),
|
||||
situation = analyseSituation(rules,"sum")(stateSelector),
|
||||
situation = analyseTopDown(rules,"sum")(stateSelector),
|
||||
result = findVariantsAndRecords(rules, ['top . sum . evt . ko'])
|
||||
|
||||
expect(result).to.have.deep.property('variantGroups', {"top . sum . evt": ['top . sum . evt . ko']})
|
||||
|
|
|
@ -1,26 +1,9 @@
|
|||
import {expect} from 'chai'
|
||||
import {enrichRule} from '../source/engine/rules'
|
||||
import {treatRuleRoot} from '../source/engine/traverse'
|
||||
import {analyseSituation} from '../source/engine/traverse'
|
||||
|
||||
let stateSelector = (state, name) => null
|
||||
|
||||
describe('treatRuleRoot', function() {
|
||||
|
||||
it('should directly return simple numerical values', function() {
|
||||
let rule = {formule: 3269}
|
||||
expect(treatRuleRoot(stateSelector,[rule],rule)).to.have.property('nodeValue',3269)
|
||||
});
|
||||
|
||||
/* TODO: make this pass
|
||||
it('should directly return simple numerical values', function() {
|
||||
let rule = {formule: "3269"}
|
||||
expect(treatRuleRoot(stateSelector,[rule],rule)).to.have.property('nodeValue',3269)
|
||||
});
|
||||
*/
|
||||
|
||||
});
|
||||
|
||||
describe('analyseSituation', function() {
|
||||
|
||||
it('should directly return simple numerical values', function() {
|
||||
|
@ -39,6 +22,14 @@ describe('analyseSituation', function() {
|
|||
|
||||
describe('analyseSituation on raw rules', function() {
|
||||
|
||||
it('should handle direct referencing of a variable', function() {
|
||||
let rawRules = [
|
||||
{nom: "startHere", formule: "dix", espace: "top"},
|
||||
{nom: "dix", formule: 10, espace: "top"}],
|
||||
rules = rawRules.map(enrichRule)
|
||||
expect(analyseSituation(rules,"startHere")(stateSelector)).to.have.property('nodeValue',10)
|
||||
});
|
||||
|
||||
it('should handle expressions referencing other rules', function() {
|
||||
let rawRules = [
|
||||
{nom: "startHere", formule: "3259 + dix", espace: "top"},
|
||||
|
@ -56,22 +47,12 @@ describe('analyseSituation on raw rules', function() {
|
|||
expect(analyseSituation(rules,"startHere")(stateSelector)).to.have.property('nodeValue',3259)
|
||||
});
|
||||
|
||||
it('should handle complements', function() {
|
||||
it('should handle comparisons', function() {
|
||||
let rawRules = [
|
||||
{nom: "startHere", formule: {complément: {cible: "dix", montant: 93}}, espace: "top"},
|
||||
{nom: "dix", formule: 17, espace: "top"}],
|
||||
{nom: "startHere", formule: "3259 > dix", espace: "top"},
|
||||
{nom: "dix", formule: 10, espace: "top"}],
|
||||
rules = rawRules.map(enrichRule)
|
||||
expect(analyseSituation(rules,"startHere")(stateSelector)).to.have.property('nodeValue',93-17)
|
||||
});
|
||||
|
||||
it('should handle components in complements', function() {
|
||||
let rawRules = [
|
||||
{nom: "startHere", formule: {complément: {cible: "dix",
|
||||
composantes: [{montant: 93},{montant: 93}]
|
||||
}}, espace: "top"},
|
||||
{nom: "dix", formule: 17, espace: "top"}],
|
||||
rules = rawRules.map(enrichRule)
|
||||
expect(analyseSituation(rules,"startHere")(stateSelector)).to.have.property('nodeValue',2*(93-17))
|
||||
expect(analyseSituation(rules,"startHere")(stateSelector)).to.have.property('nodeValue',true)
|
||||
});
|
||||
|
||||
/* TODO: make this pass
|
||||
|
@ -105,10 +86,10 @@ describe('analyseSituation with mecanisms', function() {
|
|||
|
||||
it('should handle switch statements', function() {
|
||||
let rawRules = [
|
||||
{nom: "startHere", formule: {"logique numérique": {
|
||||
"1 > dix":"10",
|
||||
"3 < dix":"11",
|
||||
"3 > dix":"12"
|
||||
{nom: "startHere", formule: {"aiguillage numérique": {
|
||||
"1 > dix":"1000%",
|
||||
"3 < dix":"1100%",
|
||||
"3 > dix":"1200%"
|
||||
}}, espace: "top"},
|
||||
{nom: "dix", formule: 10, espace: "top"}],
|
||||
rules = rawRules.map(enrichRule)
|
||||
|
@ -124,9 +105,10 @@ describe('analyseSituation with mecanisms', function() {
|
|||
|
||||
it('should handle sums', function() {
|
||||
let rawRules = [
|
||||
{nom: "startHere", formule: {"somme": [3200, 60, 9]}}],
|
||||
{nom: "startHere", formule: {"somme": [3200, "dix", 9]}},
|
||||
{nom: "dix", formule: 10}],
|
||||
rules = rawRules.map(enrichRule)
|
||||
expect(analyseSituation(rules,"startHere")(stateSelector)).to.have.property('nodeValue',3269)
|
||||
expect(analyseSituation(rules,"startHere")(stateSelector)).to.have.property('nodeValue',3219)
|
||||
});
|
||||
|
||||
it('should handle multiplications', function() {
|
||||
|
@ -186,6 +168,24 @@ describe('analyseSituation with mecanisms', function() {
|
|||
expect(analyseSituation(rules,"startHere")(stateSelector)).to.have.property('nodeValue',3200)
|
||||
});
|
||||
|
||||
it('should handle complements', function() {
|
||||
let rawRules = [
|
||||
{nom: "startHere", formule: {complément: {cible: "dix", montant: 93}}, espace: "top"},
|
||||
{nom: "dix", formule: 17, espace: "top"}],
|
||||
rules = rawRules.map(enrichRule)
|
||||
expect(analyseSituation(rules,"startHere")(stateSelector)).to.have.property('nodeValue',93-17)
|
||||
});
|
||||
|
||||
it('should handle components in complements', function() {
|
||||
let rawRules = [
|
||||
{nom: "startHere", formule: {complément: {cible: "dix",
|
||||
composantes: [{montant: 93},{montant: 93}]
|
||||
}}, espace: "top"},
|
||||
{nom: "dix", formule: 17, espace: "top"}],
|
||||
rules = rawRules.map(enrichRule)
|
||||
expect(analyseSituation(rules,"startHere")(stateSelector)).to.have.property('nodeValue',2*(93-17))
|
||||
});
|
||||
|
||||
it('should handle filtering on components', function() {
|
||||
let rawRules = [
|
||||
{nom: "startHere", espace: "top", formule: "composed (salarié)"},
|
||||
|
|
|
@ -51,4 +51,12 @@ describe('evaluateVariable', function() {
|
|||
expect(evaluateVariable(situationGate, "contrat salarié . CDD . motif . classique . accroissement activité", rule)).to.be.null
|
||||
});
|
||||
|
||||
it ("should set the value of variants to false if one of them is true", function() {
|
||||
let rule = {nom: "ici", espace: "univers", formule: {"une possibilité": ["noir","blanc"]}},
|
||||
state = {"univers . ici": "blanc"},
|
||||
situationGate = (name) => state[name]
|
||||
|
||||
expect(evaluateVariable(situationGate, "univers . ici . noir", rule)).to.be.false
|
||||
});
|
||||
|
||||
});
|
||||
|
|
Loading…
Reference in New Issue