Merge branch 'new-top-down'

pull/44/head
mama 2017-08-22 14:28:33 +02:00
commit 576dc16a23
37 changed files with 13493 additions and 1575 deletions

12378
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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 ?")
});
});

View File

@ -1,5 +1,3 @@
require('babel-register')();
var jsdom = require('jsdom/lib/old-api').jsdom;
var exposedProperties = ['window', 'navigator', 'document'];

View File

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

View File

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

44
test/mecanisms.test.js Normal file
View File

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

View File

@ -1,2 +0,0 @@
test/**/*.test.js

View File

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

View File

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

View File

@ -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: []

View File

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

View File

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

View File

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