Merge pull request #14 from sgmap/add-tests

Ajouter des tests unitaires
pull/44/head
Laurent Bossavit 2017-06-28 17:35:26 +02:00 committed by GitHub
commit 38f1750e96
16 changed files with 843 additions and 649 deletions

1
.gitignore vendored
View File

@ -1,3 +1,4 @@
.tags*
.tmp
node_modules/
dist/

41
__tests__/rules.test.js Normal file
View File

@ -0,0 +1,41 @@
import {expect} from 'chai'
import {enrichRule, collectMissingVariables, getObjectives} from '../source/engine/rules'
import {analyseSituation} from '../source/engine/traverse'
let stateSelector = (state, name) => null
describe('enrichRule', function() {
it('should extract the type of the rule', function() {
let rule = {cotisation:{}}
expect(enrichRule(rule)).to.have.property('type','cotisation')
});
it('should extract the dotted name of the rule', function() {
let rule = {espace:"contrat salarié", nom: "CDD"}
expect(enrichRule(rule)).to.have.property('name','CDD')
expect(enrichRule(rule)).to.have.property('dottedName','contrat salarié . CDD')
});
it('should render Markdown in sub-questions', function() {
let rule = {"sous-question":"**wut**"}
expect(enrichRule(rule)).to.have.property('subquestion','<p><strong>wut</strong></p>\n')
});
});
describe('collectMissingVariables', function() {
it('should derive objectives from the root rule', function() {
let rawRules = [
{nom: "startHere", formule: {somme: [3259, "dix"]}, espace: "top"},
{nom: "dix", formule: "cinq", espace: "top"},
{nom: "cinq", espace: "top"}],
rules = rawRules.map(enrichRule),
situation = analyseSituation(rules,"startHere")(stateSelector),
result = getObjectives(situation)
expect(result).to.have.lengthOf(1)
expect(result[0]).to.have.property('name','dix')
});
});

139
__tests__/traverse.test.js Normal file
View File

@ -0,0 +1,139 @@
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() {
let rule = {name: "startHere", formule: 3269}
let rules = [rule]
expect(analyseSituation(rules,"startHere")(stateSelector)).to.have.property('nodeValue',3269)
});
it('should compute expressions combining constants', function() {
let rule = {name: "startHere", formule: "32 + 69"}
let rules = [rule]
expect(analyseSituation(rules,"startHere")(stateSelector)).to.have.property('nodeValue',101)
});
});
describe('analyseSituation on raw rules', function() {
it('should handle expressions referencing other rules', function() {
let rawRules = [
{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',3269)
});
it('should handle applicability conditions', function() {
let rawRules = [
{nom: "startHere", formule: "3259 + dix", espace: "top"},
{nom: "dix", formule: 10, espace: "top", "non applicable si" : "vrai"},
{nom: "vrai", formule: "2 > 1", espace: "top"}],
rules = rawRules.map(enrichRule)
expect(analyseSituation(rules,"startHere")(stateSelector)).to.have.property('nodeValue',3259)
});
/* TODO: make this pass
it('should handle applicability conditions', function() {
let rawRules = [
{nom: "startHere", formule: "3259 + dix", espace: "top"},
{nom: "dix", formule: 10, espace: "top", "non applicable si" : "vrai"},
{nom: "vrai", formule: "1", espace: "top"}],
rules = rawRules.map(enrichRule)
expect(analyseSituation(rules,"startHere")(stateSelector)).to.have.property('nodeValue',3259)
});
*/
});
describe('analyseSituation with mecanisms', function() {
it('should handle n-way "or"', function() {
let rawRules = [
{nom: "startHere", formule: {"une de ces conditions": ["1 > 2", "1 > 0", "0 > 2"]}}],
rules = rawRules.map(enrichRule)
expect(analyseSituation(rules,"startHere")(stateSelector)).to.have.property('nodeValue',true)
});
it('should handle n-way "and"', function() {
let rawRules = [
{nom: "startHere", formule: {"toutes ces conditions": ["1 > 2", "1 > 0", "0 > 2"]}}],
rules = rawRules.map(enrichRule)
expect(analyseSituation(rules,"startHere")(stateSelector)).to.have.property('nodeValue',false)
});
it('should handle switch statements', function() {
let rawRules = [
{nom: "startHere", formule: {"logique numérique": {
"1 > dix":"10",
"3 < dix":"11",
"3 > dix":"12"
}}, espace: "top"},
{nom: "dix", formule: 10, espace: "top"}],
rules = rawRules.map(enrichRule)
expect(analyseSituation(rules,"startHere")(stateSelector)).to.have.property('nodeValue',11)
});
it('should handle percentages', function() {
let rawRules = [
{nom: "startHere", formule: {taux: "35%"}, espace: "top"}],
rules = rawRules.map(enrichRule)
expect(analyseSituation(rules,"startHere")(stateSelector)).to.have.property('nodeValue',0.35)
});
it('should handle sums', function() {
let rawRules = [
{nom: "startHere", formule: {"somme": [3200, 60, 9]}}],
rules = rawRules.map(enrichRule)
expect(analyseSituation(rules,"startHere")(stateSelector)).to.have.property('nodeValue',3269)
});
it('should handle multiplications', function() {
let rawRules = [
{nom: "startHere", formule: {"multiplication": {assiette:3259, plafond:3200, facteur:1, taux:1.5}}}],
rules = rawRules.map(enrichRule)
expect(analyseSituation(rules,"startHere")(stateSelector)).to.have.property('nodeValue',4800)
});
it('should handle progressive scales', function() {
let rawRules = [
{nom: "startHere", formule: {"barème": {
assiette:2008,
"multiplicateur des tranches":1000,
"tranches":[{"en-dessous de":1, taux: 0.1},{de:1, "à": 2, taux: 1.2}, ,{"au-dessus de":2, taux: 10}]
}}}],
rules = rawRules.map(enrichRule)
expect(analyseSituation(rules,"startHere")(stateSelector)).to.have.property('nodeValue',100+1200+80)
});
it('should handle max', function() {
let rawRules = [
{nom: "startHere", formule: {"le maximum de": [3200, 60, 9]}}],
rules = rawRules.map(enrichRule)
expect(analyseSituation(rules,"startHere")(stateSelector)).to.have.property('nodeValue',3200)
});
});

9
__tests__/utils.test.js Normal file
View File

@ -0,0 +1,9 @@
var assert = require('assert');
const utils = require("../source/utils.js")
describe('capitalise0', function() {
it('should turn the first character into its capital', function() {
assert.equal("Salaire", utils.capitalise0("salaire"));
});
});

View File

@ -45,6 +45,7 @@
"babel-polyfill": "^6.23.0",
"babel-preset-env": "^1.4.0",
"babel-preset-react": "^6.24.1",
"chai": "^4.0.2",
"core-js": "^2.4.1",
"css-loader": "^0.28.1",
"eslint": "^3.19.0",
@ -54,21 +55,25 @@
"html-loader": "^0.4.5",
"img-loader": "^2.0.0",
"json-loader": "^0.5.4",
"mocha": "^3.4.2",
"mocha-webpack": "^0.7.0",
"nearley-loader": "0.0.2",
"postcss-loader": "^2.0.5",
"react-hot-loader": "^3.0.0-beta.6",
"redux-devtools": "^3.4.0",
"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",
"url-loader": "^0.5.8",
"webpack": "^2.5.1",
"webpack": "^2.6.1",
"webpack-dev-server": "^2.4.5",
"yaml-loader": "^0.4.0"
},
"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/"
"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 \"__tests__/**/*.test.js\""
}
}

View File

@ -1,6 +1,6 @@
import React, {Component} from 'react'
import {connect} from 'react-redux'
import {findRuleByDottedName} from '../engine/rules'
import {rules, findRuleByDottedName} from '../engine/rules'
import './Aide.css'
import {EXPLAIN_VARIABLE} from '../actions'
import References from './rule/References'
@ -23,7 +23,7 @@ export default class Aide extends Component {
if (!explained) return <section id="helpWrapper" />
let rule = findRuleByDottedName(explained),
let rule = findRuleByDottedName(rule, explained),
text = rule.description,
refs = rule.références

View File

@ -6,7 +6,7 @@ import R from 'ramda'
import {Redirect, Link, withRouter} from 'react-router-dom'
import Aide from './Aide'
import {createMarkdownDiv} from 'Engine/marked'
import {findRuleByName, decodeRuleName} from 'Engine/rules'
import {rules, findRuleByName, decodeRuleName} from 'Engine/rules'
import 'Components/conversation/conversation.css'
import 'Components/Simulateur.css'
import classNames from 'classnames'
@ -44,7 +44,7 @@ export default class extends React.Component {
this.encodedName = encodedName
this.name = name
this.rule = findRuleByName(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)

View File

@ -4,7 +4,7 @@ import './Explicable.css'
import HoverDecorator from '../HoverDecorator'
import {connect} from 'react-redux'
import {EXPLAIN_VARIABLE} from '../../actions'
import {findRuleByDottedName} from '../../engine/rules'
import {rules, findRuleByDottedName} from '../../engine/rules'
@connect(state => ({explained: state.explainedVariable}), dispatch => ({
@ -18,7 +18,7 @@ export default class Explicable extends React.Component {
explain, explained,
lightBackground
} = this.props,
rule = findRuleByDottedName(dottedName)
rule = findRuleByDottedName(rules, dottedName)
// Rien à expliquer ici, ce n'est pas une règle
if (!rule) return <span>{label}</span>

View File

@ -2,8 +2,8 @@ import React, { Component } from "react"
import R from "ramda"
import classNames from "classnames"
import {
rules,
decodeRuleName,
findRuleByName,
disambiguateRuleReference
} from "Engine/rules.js"
import { analyseSituation } from "Engine/traverse"
@ -18,13 +18,14 @@ export default class Examples extends Component {
return exemples.map(ex => {
// les variables dans les tests peuvent être exprimées relativement à l'espace de nom de la règle,
// comme dans sa formule
// TODO - absolutely don't do this here but as a transformation step in rule parsing
let exempleSituation = R.pipe(
R.toPairs,
R.map(([k, v]) => [disambiguateRuleReference(rule, k), v]),
R.map(([k, v]) => [disambiguateRuleReference(rules, rule, k), v]),
R.fromPairs
)(ex.situation)
let runExemple = analyseSituation(rule.name)(v => exempleSituation[v]),
let runExemple = analyseSituation(rules, rule.name)(v => exempleSituation[v]),
exempleCalculatedValue = runExemple["non applicable si"] &&
runExemple["non applicable si"].nodeValue
? null

View File

@ -4,7 +4,7 @@ import {connect} from 'react-redux'
import {formValueSelector} from 'redux-form'
import R from 'ramda'
import './Rule.css'
import {decodeRuleName, findRuleByName, disambiguateRuleReference} from 'Engine/rules.js'
import {rules, decodeRuleName} from 'Engine/rules.js'
import mockSituation from 'Engine/mockSituation.yaml'
import {analyseSituation} from 'Engine/traverse'
import {START_CONVERSATION} from '../../actions'
@ -41,7 +41,7 @@ export default class Rule extends Component {
}
}
setRule(name){
this.rule = analyseSituation(decodeRuleName(name))(this.props.situationGate)
this.rule = analyseSituation(rules, decodeRuleName(name))(this.props.situationGate)
}
componentWillMount(){
let {

View File

@ -7,7 +7,7 @@ import formValueTypes from 'Components/conversation/formValueTypes'
import {analyseSituation} from './traverse'
import {formValueSelector} from 'redux-form'
import { STEP_ACTION, START_CONVERSATION} from '../actions'
import {findGroup, findRuleByDottedName, parentName, collectMissingVariables, findVariantsAndRecords} from './rules'
import {rules, findRuleByDottedName, collectMissingVariables, deprecated_findVariantsAndRecords} from './rules'
export let reduceSteps = (state, action) => {
@ -58,7 +58,7 @@ let situationGate = state =>
let analyse = rootVariable => R.pipe(
situationGate,
// une liste des objectifs de la simulation (des 'rules' aussi nommées 'variables')
analyseSituation(rootVariable)
analyseSituation(rules, rootVariable)
)
@ -111,7 +111,7 @@ let buildNextSteps = analysedSituation => {
return R.pipe(
R.keys,
R.reduce(
findVariantsAndRecords
deprecated_findVariantsAndRecords
, {variantGroups: {}, recordGroups: {}}
),
// on va maintenant construire la liste des composants React qui afficheront les questions à l'utilisateur pour que l'on obtienne les variables manquantes
@ -153,7 +153,7 @@ let isVariant = R.path(['formule', 'une possibilité'])
let buildVariantTree = relevantPaths => path => {
let rec = path => {
let node = findRuleByDottedName(path),
let node = findRuleByDottedName(rules, path),
variant = isVariant(node),
variants = variant && R.unless(R.is(Array), R.prop('possibilités'))(variant),
shouldBeExpanded = variant && variants.find( v => relevantPaths.find(rp => R.contains(path + ' . ' + v)(rp) )),
@ -175,7 +175,7 @@ export let generateGridQuestions = missingVariables => R.pipe(
R.toPairs,
R.map( ([variantRoot, relevantVariants]) =>
({
...constructStepMeta(findRuleByDottedName(variantRoot)),
...constructStepMeta(findRuleByDottedName(rules, variantRoot)),
component: Question,
choices: buildVariantTree(relevantVariants)(variantRoot),
objectives: R.pipe(
@ -192,7 +192,7 @@ export let generateSimpleQuestions = missingVariables => R.pipe(
R.values, //TODO exploiter ici les groupes de questions de type 'record' (R.keys): elles pourraient potentiellement êtres regroupées visuellement dans le formulaire
R.unnest,
R.map(dottedName => {
let rule = findRuleByDottedName(dottedName)
let rule = findRuleByDottedName(rules, dottedName)
if (rule == null) console.log(dottedName)
return Object.assign(
constructStepMeta(rule),

459
source/engine/mecanisms.js Normal file
View File

@ -0,0 +1,459 @@
import R from 'ramda'
import React from 'react'
import {anyNull, val} from './traverse-common-functions'
import {Node, Leaf} from './traverse-common-jsx'
let transformPercentage = s =>
R.contains('%')(s) ?
+s.replace('%', '') / 100
: +s
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
classes="mecanism conditions list"
name={result.name}
value={result.nodeValue}
child={
<ul>
{result.explanation.map(item => <li key={item.name || item.text}>{item.jsx}</li>)}
</ul>
}
/>
}
}
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]
}
}, {
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)
}
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
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)
}
export let mecanismPercentage = (recurse,k,v) => {
let reg = /^(\d+(\.\d+)?)\%$/
if (R.test(reg)(v))
return {
category: 'percentage',
type: 'numeric',
percentage: v,
nodeValue: R.match(reg)(v)[1]/100,
explanation: null,
jsx:
<span className="percentage" >
<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)
return {
type: 'numeric',
category: 'percentage',
percentage: node.nodeValue,
nodeValue: node.nodeValue,
explanation: node,
jsx: node.jsx
}
}
}
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)
return {
nodeValue,
category: 'mecanism',
name: 'somme',
type: 'numeric',
explanation: summedVariables,
jsx: <Node
classes="mecanism somme"
name="somme"
value={nodeValue}
child={
<ul>
{summedVariables.map(v => <li key={v.name || v.text}>{v.jsx}</li>)}
</ul>
}
/>
}
}
export let mecanismProduct = (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) ?
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
classes="mecanism multiplication"
name="multiplication"
value={nodeValue}
child={
<ul className="properties">
<li key="assiette">
<span className="key">assiette: </span>
<span className="value">{assiette.jsx}</span>
</li>
{taux.nodeValue != 1 &&
<li key="taux">
<span className="key">taux: </span>
<span className="value">{taux.jsx}</span>
</li>}
{facteur.nodeValue != 1 &&
<li key="facteur">
<span className="key">facteur: </span>
<span className="value">{facteur.jsx}</span>
</li>}
{plafond.nodeValue != Infinity &&
<li key="plafond">
<span className="key">plafond: </span>
<span className="value">{plafond.jsx}</span>
</li>}
</ul>
}
/>
}
}
export let mecanismScale = (recurse,k,v) => {
// Sous entendu : barème en taux marginaux.
// A étendre (avec une propriété type ?) quand les règles en contiendront d'autres.
if (v.composantes) { //mécanisme de composantes. Voir known-mecanisms.md/composantes
let
baremeProps = R.dissoc('composantes')(v),
composantes = v.composantes.map(c =>
({
... recurse(
{
barème: {
... baremeProps,
... R.dissoc('attributs')(c)
}
}
),
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
classes="mecanism composantes"
name="composantes"
value={nodeValue}
child={
<ul>
{ composantes.map((c, i) =>
[<li className="composante" key={JSON.stringify(c.composante)}>
<ul className="composanteAttributes">
{R.toPairs(c.composante).map(([k,v]) =>
<li>
<span>{k}: </span>
<span>{v}</span>
</li>
)}
</ul>
<div className="content">
{c.jsx}
</div>
</li>,
i < (composantes.length - 1) && <li className="composantesSymbol"><i className="fa fa-plus-circle" aria-hidden="true"></i></li>
]
)
}
</ul>
}
/>
}
}
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 =>
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
),
//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(
R.pipe(
R.values, R.map(val), R.any(R.equals(null))
)
)(tranches),
*/
// nulled = anyNull([assiette, multiplicateur]),
nulled = val(assiette) == null || val(multiplicateur) == null,
nodeValue =
nulled ?
null
: tranches.reduce((memo, {de: min, 'à': max, taux}) =>
( val(assiette) < ( min * val(multiplicateur) ) )
? memo + 0
: memo
+ ( 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
classes="mecanism barème"
name="barème"
value={nodeValue}
child={
<ul className="properties">
<li key="assiette">
<span className="key">assiette: </span>
<span className="value">{assiette.jsx}</span>
</li>
<li key="multiplicateur">
<span className="key">multiplicateur des tranches: </span>
<span className="value">{multiplicateur.jsx}</span>
</li>
<table className="tranches">
<thead>
<tr>
<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}) =>
<tr key={min || minOnly || 0}>
<td>
{ maxOnly ? 'En dessous de ' + maxOnly
: minOnly ? 'Au dessus de ' + minOnly
: `De ${min} à ${max}` }
</td>
<td> {taux} </td>
</tr>
)}
</thead>
</table>
</ul>
}
/>
}
}
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
return {
type: 'numeric',
category: 'mecanism',
name: 'le maximum de',
nodeValue,
explanation: contenders,
jsx: <Node
classes="mecanism list maximum"
name="le maximum de"
value={nodeValue}
child={
<ul>
{contenders.map((item, i) =>
<li key={i}>
<div className="description">{v[i].description}</div>
{item.jsx}
</li>
)}
</ul>
}
/>
}
}
export let mecanismError = (recurse,k,v) => {
throw "Le mécanisme est inconnu !"
}

View File

@ -46,7 +46,8 @@ export let decodeRuleName = name => name.replace(/\-/g, ' ')
/* Les variables peuvent être exprimées dans la formule d'une règle relativement à son propre espace de nom, pour une plus grande lisibilité. Cette fonction résoud cette ambiguité.
*/
export let disambiguateRuleReference = ({ns, name}, partialName) => {
export let disambiguateRuleReference = (allRules, {ns, name}, partialName) => {
let
fragments = ns.split(' . '), // ex. [CDD . événements . rupture]
pathPossibilities = // -> [ [CDD . événements . rupture], [CDD . événements], [CDD] ]
@ -56,7 +57,7 @@ export let disambiguateRuleReference = ({ns, name}, partialName) => {
found = R.reduce((res, path) =>
R.when(
R.is(Object), R.reduced
)(findRuleByDottedName([...path, partialName].join(' . ')))
)(findRuleByDottedName(allRules, [...path, partialName].join(' . ')))
, null, pathPossibilities)
return found && found.dottedName || do {throw `OUUUUPS la référence '${partialName}' dans la règle '${name}' est introuvable dans la base`}
@ -69,9 +70,8 @@ export let rules = rawRules.map(enrichRule)
/****************************************
Méthodes de recherche d'une règle */
export let findRuleByName = search =>
rules
.map(enrichRule)
export let findRuleByName = (allRules, search) =>
allRules
.find( ({name}) =>
name.toLowerCase() === search.toLowerCase()
)
@ -83,21 +83,8 @@ export let searchRules = searchInput =>
JSON.stringify(rule).toLowerCase().indexOf(searchInput) > -1)
.map(enrichRule)
export let findRuleByDottedName = dottedName => dottedName &&
rules.find(rule => rule.dottedName.toLowerCase() == dottedName.toLowerCase())
export let findGroup = R.pipe(
findRuleByDottedName,
found => found && found['une possibilité'] && found,
// Is there a way to express this more litterally in ramda ?
// R.unless(
// R.isNil,
// R.when(
// R.has('une possibilité'),
// R.identity
// )
// )
)
export let findRuleByDottedName = (allRules, dottedName) => dottedName &&
allRules.find(rule => rule.dottedName.toLowerCase() == dottedName.toLowerCase())
/*********************************
Autres */
@ -130,12 +117,14 @@ export let getObjectives = analysedSituation => {
let formuleType = R.path(["formule", "explanation", "name"])(
analysedSituation
)
return formuleType == "somme"
let result = formuleType == "somme"
? R.pluck(
"explanation",
R.path(["formule", "explanation", "explanation"])(analysedSituation)
)
: formuleType ? [analysedSituation] : null
return R.reject(R.isNil)(result)
}
@ -159,16 +148,16 @@ export let collectMissingVariables = (groupMethod='groupByMissingVariable') => a
let isVariant = R.path(['formule', 'une possibilité'])
export let findVariantsAndRecords =
export let deprecated_findVariantsAndRecords =
({variantGroups, recordGroups}, dottedName, childDottedName) => {
let child = findRuleByDottedName(dottedName),
let child = findRuleByDottedName(rules, dottedName),
parentDottedName = parentName(dottedName),
parent = findRuleByDottedName(parentDottedName)
parent = findRuleByDottedName(rules, parentDottedName)
if (isVariant(parent)) {
let grandParentDottedName = parentName(parentDottedName),
grandParent = findRuleByDottedName(grandParentDottedName)
grandParent = findRuleByDottedName(rules, grandParentDottedName)
if (isVariant(grandParent))
return findVariantsAndRecords({variantGroups, recordGroups}, parentDottedName, childDottedName || dottedName)
return deprecated_findVariantsAndRecords({variantGroups, recordGroups}, parentDottedName, childDottedName || dottedName)
else
return {
variantGroups: R.mergeWith(R.concat, variantGroups, {[parentDottedName]: [childDottedName || dottedName]}),

View File

@ -1,13 +1,12 @@
import React from 'react'
import {findRuleByDottedName, disambiguateRuleReference, findRuleByName} from './rules'
import {rules, findRuleByDottedName, disambiguateRuleReference, findRuleByName} from './rules'
import {evaluateVariable} from './variables'
import R from 'ramda'
import knownMecanisms from './known-mecanisms.yaml'
import { Parser } from 'nearley'
import Grammar from './grammar.ne'
import {Node, Leaf} from './traverse-common-jsx'
import {anyNull, val} from './traverse-common-functions'
import {mecanismOneOf,mecanismAllOf,mecanismNumericalLogic,mecanismSum,mecanismProduct,mecanismPercentage,mecanismScale,mecanismMax,mecanismError} from "./mecanisms"
let nearley = () => new Parser(Grammar.ParserRules, Grammar.ParserStart)
@ -20,12 +19,6 @@ let nearley = () => new Parser(Grammar.ParserRules, Grammar.ParserStart)
*/
let transformPercentage = s =>
R.contains('%')(s) ?
+s.replace('%', '') / 100
: +s
/*
-> Notre règle est naturellement un AST (car notation préfixe dans le YAML)
-> préliminaire : les expression infixes devront être parsées,
@ -54,18 +47,19 @@ par exemple ainsi : https://github.com/Engelberg/instaparse#transforming-the-tre
*/
let fillVariableNode = (rule, situationGate) => (parseResult) => {
let fillVariableNode = (rules, rule, situationGate) => (parseResult) => {
let
{fragments} = parseResult,
variablePartialName = fragments.join(' . '),
dottedName = disambiguateRuleReference(rule, variablePartialName),
variable = findRuleByDottedName(dottedName),
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
),
@ -116,601 +110,169 @@ let buildNegatedVariable = variable => {
}
}
let treat = (situationGate, rule) => rawNode => {
let reTreat = treat(situationGate, rule)
let treat = (situationGate, rules, rule) => rawNode => {
// inner functions
let reTreat = treat(situationGate, 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 obtient un objet de type Variable (avec potentiellement un 'modifier', par exemple temporel (TODO)), CalcExpression ou Comparison.
Cet objet est alors rebalancé à 'treat'.
*/
if (R.is(String)(rawNode)) {
/* On a à faire à un string, donc à une expression infixe.
Elle sera traité avec le parser obtenu grâce à NearleyJs et notre grammaire.
On obtient un objet de type Variable (avec potentiellement un 'modifier', par exemple temporel (TODO)), CalcExpression ou Comparison.
Cet objet est alors rebalancé à 'treat'.
*/
let [parseResult, ...additionnalResults] = nearley().feed(rawNode).results
let [parseResult, ...additionnalResults] = nearley().feed(rawNode).results
if (additionnalResults && additionnalResults.length > 0)
throw "Attention ! L'expression <" + rawNode + '> ne peut être traitée de façon univoque'
if (additionnalResults && additionnalResults.length > 0) throw "Attention ! L'expression <" + rawNode + '> ne peut être traitée de façon univoque'
if (!R.contains(parseResult.category)(['variable', 'calcExpression', 'modifiedVariable', 'comparison', 'negatedVariable']))
throw "Attention ! Erreur de traitement de l'expression : " + rawNode
if (!R.contains(parseResult.category)(['variable', 'calcExpression', 'modifiedVariable', 'comparison', 'negatedVariable']))
throw "Attention ! Erreur de traitement de l'expression : " + rawNode
if (parseResult.category == 'variable')
return fillVariableNode(rules, rule, situationGate)(parseResult)
if (parseResult.category == 'negatedVariable')
return buildNegatedVariable(
fillVariableNode(rules, rule, situationGate)(parseResult.variable)
)
if (parseResult.category == 'variable')
return fillVariableNode(rule, situationGate)(parseResult)
if (parseResult.category == 'negatedVariable')
return buildNegatedVariable(
fillVariableNode(rule, situationGate)(parseResult.variable)
)
if (parseResult.category == 'calcExpression') {
let
filledExplanation = parseResult.explanation.map(
R.cond([
[R.propEq('category', 'variable'), fillVariableNode(rule, situationGate)],
[R.propEq('category', 'value'), node =>
R.assoc('jsx', <span className="value">
{node.nodeValue}
</span>)(node)
]
])
),
[{nodeValue: value1}, {nodeValue: value2}] = filledExplanation,
operatorFunctionName = {
'*': 'multiply',
'/': 'divide',
'+': 'add',
'-': 'subtract'
}[parseResult.operator],
operatorFunction = R[operatorFunctionName],
nodeValue = value1 == null || value2 == null ?
null
: operatorFunction(value1, value2)
return {
text: rawNode,
nodeValue,
category: 'calcExpression',
type: 'numeric',
explanation: filledExplanation,
jsx: <Node
classes="inlineExpression calcExpression"
value={nodeValue}
child={
<span className="nodeContent">
{filledExplanation[0].jsx}
<span className="operator">{parseResult.operator}</span>
{filledExplanation[1].jsx}
</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(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 {
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>
}
/>
}
}
}
//TODO C'est pas bien ça. Devrait être traité par le parser plus haut !
if (R.is(Number)(rawNode)) {
return {
category: 'number',
nodeValue: rawNode,
type: 'numeric',
jsx:
<span className="number">
{rawNode}
</span>
}
}
if (!R.is(Object)(rawNode)) {
console.log() // eslint-disable-line no-console
throw 'Cette donnée : ' + rawNode + ' doit être un Number, String ou Object'
}
let mecanisms = R.intersection(R.keys(rawNode), R.keys(knownMecanisms))
if (mecanisms.length != 1) {
console.log('Erreur : On ne devrait reconnaître que un et un seul mécanisme dans cet objet', rawNode)
throw 'OUPS !'
}
let k = R.head(mecanisms),
v = rawNode[k]
if (k === 'une de ces conditions') {
let result = R.pipe(
R.unless(R.is(Array), () => {throw 'should be array'}),
R.reduce( (memo, next) => {
let {nodeValue, explanation} = memo,
child = reTreat(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
if (parseResult.category == 'calcExpression') {
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)
]
])
),
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
classes="mecanism conditions list"
name={result.name}
value={result.nodeValue}
child={
<ul>
{result.explanation.map(item => <li key={item.name}>{item.jsx}</li>)}
</ul>
}
/>
}
}
if (k === 'toutes ces conditions') {
return R.pipe(
R.unless(R.is(Array), () => {throw 'should be array'}),
R.reduce( (memo, next) => {
let {nodeValue, explanation} = memo,
child = reTreat(next),
{nodeValue: nextValue} = child
return {...memo,
// c'est un ET logique avec une possibilité de null
nodeValue: ! nodeValue ? nodeValue : nextValue,
explanation: [...explanation, child]
}
}, {
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)
}
[{nodeValue: value1}, {nodeValue: value2}] = filledExplanation,
operatorFunctionName = {
'*': 'multiply',
'/': 'divide',
'+': 'add',
'-': 'subtract'
}[parseResult.operator],
operatorFunction = R[operatorFunctionName],
nodeValue = value1 == null || value2 == null ?
null
: operatorFunction(value1, value2)
//TODO perf: declare this closure somewhere else ?
let treatNumericalLogicRec =
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 = reTreat(condition), // can be a 'comparison', a 'variable', TODO a 'negation'
childNumericalLogic = treatNumericalLogicRec(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
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}
return {
text: rawNode,
nodeValue,
category: 'calcExpression',
type: 'numeric',
explanation: filledExplanation,
jsx: <Node
classes="inlineExpression calcExpression"
value={nodeValue}
child={
<ul>
{node.explanation.map(item => <li key={item.name}>{item.jsx}</li>)}
</ul>
<span className="nodeContent">
{filledExplanation[0].jsx}
<span className="operator">{parseResult.operator}</span>
{filledExplanation[1].jsx}
</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)
if (k === 'logique numérique') {
return treatNumericalLogicRec(v)
}
if (k === 'taux') {
let reg = /^(\d+(\.\d+)?)\%$/
if (R.test(reg)(v))
return {
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>
}
/>
}
}
},
treatNumber = rawNode => {
return {
category: 'percentage',
text: ""+rawNode,
category: 'number',
nodeValue: rawNode,
type: 'numeric',
percentage: v,
nodeValue: R.match(reg)(v)[1]/100,
explanation: null,
jsx:
<span className="percentage" >
<span className="name">{v}</span>
<span className="number">
{rawNode}
</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>
},
treatOther = rawNode => {
console.log() // eslint-disable-line no-console
throw 'Cette donnée : ' + rawNode + ' doit être un Number, String ou Object'
},
treatObject = rawNode => {
let mecanisms = R.intersection(R.keys(rawNode), R.keys(knownMecanisms))
if (mecanisms.length != 1) {
console.log('Erreur : On ne devrait reconnaître que un et un seul mécanisme dans cet objet', rawNode)
throw 'OUPS !'
}
}
else {
let node = reTreat(v)
return {
type: 'numeric',
category: 'percentage',
percentage: node.nodeValue,
nodeValue: node.nodeValue,
explanation: node,
jsx: node.jsx
}
}
}
// Une simple somme de variables
if (k === 'somme') {
let
summedVariables = v.map(reTreat),
nodeValue = summedVariables.reduce(
(memo, {nodeValue: nextNodeValue}) => memo == null ? null : nextNodeValue == null ? null : memo + +nextNodeValue,
0)
let k = R.head(mecanisms),
v = rawNode[k]
return {
nodeValue,
category: 'mecanism',
name: 'somme',
type: 'numeric',
explanation: summedVariables,
jsx: <Node
classes="mecanism somme"
name="somme"
value={nodeValue}
child={
<ul>
{summedVariables.map(v => <li key={v.name}>{v.jsx}</li>)}
</ul>
}
/>
}
}
let dispatch = {
'une de ces conditions': mecanismOneOf,
'toutes ces conditions': mecanismAllOf,
'logique numérique': mecanismNumericalLogic,
'taux': mecanismPercentage,
'somme': mecanismSum,
'multiplication': mecanismProduct,
'barème': mecanismScale,
'le maximum de': mecanismMax,
},
action = R.pathOr(mecanismError,[k],dispatch)
if (k === 'multiplication') {
let
mult = (base, rate, facteur, plafond) =>
Math.min(base, plafond) * rate * facteur,
constantNode = constant => ({nodeValue: constant}),
assiette = reTreat(v['assiette']),
//TODO parser le taux dans le parser ?
taux = v['taux'] ? reTreat({taux: v['taux']}) : constantNode(1),
facteur = v['facteur'] ? reTreat(v['facteur']) : constantNode(1),
plafond = v['plafond'] ? reTreat(v['plafond']) : constantNode(Infinity),
//TODO rate == false should be more explicit
nodeValue = (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
classes="mecanism multiplication"
name="multiplication"
value={nodeValue}
child={
<ul className="properties">
<li key="assiette">
<span className="key">assiette: </span>
<span className="value">{assiette.jsx}</span>
</li>
{taux.nodeValue != 1 &&
<li key="taux">
<span className="key">taux: </span>
<span className="value">{taux.jsx}</span>
</li>}
{facteur.nodeValue != 1 &&
<li key="facteur">
<span className="key">facteur: </span>
<span className="value">{facteur.jsx}</span>
</li>}
{plafond.nodeValue != Infinity &&
<li key="plafond">
<span className="key">plafond: </span>
<span className="value">{plafond.jsx}</span>
</li>}
</ul>
}
/>
}
}
if (k === 'barème') {
// Sous entendu : barème en taux marginaux.
// A étendre (avec une propriété type ?) quand les règles en contiendront d'autres.
if (v.composantes) { //mécanisme de composantes. Voir known-mecanisms.md/composantes
let
baremeProps = R.dissoc('composantes')(v),
composantes = v.composantes.map(c =>
({
... reTreat(
{
barème: {
... baremeProps,
... R.dissoc('attributs')(c)
}
}
),
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
classes="mecanism composantes"
name="composantes"
value={nodeValue}
child={
<ul>
{ composantes.map((c, i) =>
[<li className="composante" key={JSON.stringify(c.composante)}>
<ul className="composanteAttributes">
{R.toPairs(c.composante).map(([k,v]) =>
<li>
<span>{k}: </span>
<span>{v}</span>
</li>
)}
</ul>
<div className="content">
{c.jsx}
</div>
</li>,
i < (composantes.length - 1) && <li className="composantesSymbol"><i className="fa fa-plus-circle" aria-hidden="true"></i></li>
]
)
}
</ul>
}
/>
}
return action(reTreat,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 = reTreat(v['assiette']),
multiplicateur = reTreat(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 =>
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
),
//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(
R.pipe(
R.values, R.map(val), R.any(R.equals(null))
)
)(tranches),
*/
// nulled = anyNull([assiette, multiplicateur]),
nulled = val(assiette) == null || val(multiplicateur) == null,
nodeValue =
nulled ?
null
: tranches.reduce((memo, {de: min, 'à': max, taux}) =>
( val(assiette) < ( min * val(multiplicateur) ) )
? memo + 0
: memo
+ ( 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
classes="mecanism barème"
name="barème"
value={nodeValue}
child={
<ul className="properties">
<li key="assiette">
<span className="key">assiette: </span>
<span className="value">{assiette.jsx}</span>
</li>
<li key="multiplicateur">
<span className="key">multiplicateur des tranches: </span>
<span className="value">{multiplicateur.jsx}</span>
</li>
<table className="tranches">
<thead>
<tr>
<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}) =>
<tr key={min || minOnly}>
<td>
{ maxOnly ? 'En dessous de ' + maxOnly
: minOnly ? 'Au dessus de ' + minOnly
: `De ${min} à ${max}` }
</td>
<td> {taux} </td>
</tr>
)}
</thead>
</table>
</ul>
}
/>
}
}
if (k === 'le maximum de') {
let contenders = v.map(treat(situationGate, rule)),
contenderValues = R.pluck('nodeValue')(contenders),
stopEverything = R.contains(null, contenderValues),
maxValue = R.max(...contenderValues),
nodeValue = stopEverything ? null : maxValue
return {
type: 'numeric',
category: 'mecanism',
name: 'le maximum de',
nodeValue,
explanation: contenders,
jsx: <Node
classes="mecanism list maximum"
name="le maximum de"
value={nodeValue}
child={
<ul>
{contenders.map((item, i) =>
<li key={i}>
<div className="description">{v[i].description}</div>
{item.jsx}
</li>
)}
</ul>
}
/>
}
}
throw "Le mécanisme est inconnu !"
let onNodeType = R.cond([
[R.is(String), treatString],
[R.is(Number), treatNumber],
[R.is(Object), treatObject],
[R.T, treatOther]
])
return onNodeType(rawNode)
}
//TODO c'est moche :
@ -725,7 +287,7 @@ export let computeRuleValue = (formuleValue, condValue) =>
? 0
: formuleValue
let treatRuleRoot = (situationGate, rule) => R.pipe(
export let treatRuleRoot = (situationGate, rules, rule) => R.pipe(
R.evolve({ // -> Voilà les attributs que peut comporter, pour l'instant, une Variable.
// 'meta': pas de traitement pour l'instant
@ -733,7 +295,7 @@ let treatRuleRoot = (situationGate, rule) => R.pipe(
// 'cond' : Conditions d'applicabilité de la règle
'non applicable si': value => {
let
child = treat(situationGate, rule)(value),
child = treat(situationGate, rules, rule)(value),
nodeValue = child.nodeValue
return {
@ -762,7 +324,7 @@ let treatRuleRoot = (situationGate, rule) => R.pipe(
// [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, rule)(value),
child = treat(situationGate, rules, rule)(value),
nodeValue = child.nodeValue
return {
category: 'ruleProp',
@ -812,23 +374,13 @@ let treatRuleRoot = (situationGate, rule) => R.pipe(
- if not, do they have a computed value or are they non applicable ?
*/
export let analyseSituation = rootVariable => situationGate =>
export let analyseSituation = (rules, rootVariable) => situationGate =>
treatRuleRoot(
situationGate,
findRuleByName(rootVariable)
rules,
findRuleByName(rules, rootVariable)
)
export let variableType = name => {
if (name == null) return null
let found = findRuleByName(name)
// tellement peu de variables pour l'instant
// que c'est très simpliste
if (!found) return 'boolean'
let {rule} = found
if (typeof rule.formule['somme'] !== 'undefined') return 'numeric'
}

View File

@ -1,5 +1,5 @@
import R from 'ramda'
import {parentName, nameLeaf, findRuleByDottedName, splitName, joinName} from './rules'
import {splitName, joinName} from './rules'
let evaluateBottomUp = situationGate => startingFragments => {

View File

@ -7,8 +7,6 @@ import { euro, months } from './components/conversation/formValueTypes.js'
import { EXPLAIN_VARIABLE, POINT_OUT_OBJECTIVES} from './actions'
import R from 'ramda'
import {findGroup, findRuleByDottedName, parentName, findVariantsAndRecords} from './engine/rules'
import {reduceSteps, generateGridQuestions, generateSimpleQuestions} from './engine/generateQuestions'
import computeThemeColours from './components/themeColours'