diff --git a/.gitignore b/.gitignore
index 3d5d935fe..4b111963a 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,3 +1,4 @@
.tags*
+.tmp
node_modules/
dist/
diff --git a/__tests__/rules.test.js b/__tests__/rules.test.js
new file mode 100644
index 000000000..796ea56ec
--- /dev/null
+++ b/__tests__/rules.test.js
@@ -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','
wut
\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')
+ });
+
+});
diff --git a/__tests__/traverse.test.js b/__tests__/traverse.test.js
new file mode 100644
index 000000000..b718fd712
--- /dev/null
+++ b/__tests__/traverse.test.js
@@ -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)
+ });
+
+});
diff --git a/__tests__/utils.test.js b/__tests__/utils.test.js
new file mode 100644
index 000000000..35437f93b
--- /dev/null
+++ b/__tests__/utils.test.js
@@ -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"));
+ });
+});
diff --git a/package.json b/package.json
index 6ef27934f..cf3e278a9 100644
--- a/package.json
+++ b/package.json
@@ -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\""
}
}
diff --git a/source/components/Aide.js b/source/components/Aide.js
index 2b82427b1..7cd0530ff 100644
--- a/source/components/Aide.js
+++ b/source/components/Aide.js
@@ -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
- let rule = findRuleByDottedName(explained),
+ let rule = findRuleByDottedName(rule, explained),
text = rule.description,
refs = rule.références
diff --git a/source/components/Simulateur.js b/source/components/Simulateur.js
index b9d57e7c4..286e6bc9e 100644
--- a/source/components/Simulateur.js
+++ b/source/components/Simulateur.js
@@ -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)
diff --git a/source/components/conversation/Explicable.js b/source/components/conversation/Explicable.js
index dc6ba00b3..07d9180c8 100644
--- a/source/components/conversation/Explicable.js
+++ b/source/components/conversation/Explicable.js
@@ -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 {label}
diff --git a/source/components/rule/Examples.js b/source/components/rule/Examples.js
index 2232da990..02fe288bc 100644
--- a/source/components/rule/Examples.js
+++ b/source/components/rule/Examples.js
@@ -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
diff --git a/source/components/rule/Rule.js b/source/components/rule/Rule.js
index 370c325f9..652e8ef5c 100644
--- a/source/components/rule/Rule.js
+++ b/source/components/rule/Rule.js
@@ -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 {
diff --git a/source/engine/generateQuestions.js b/source/engine/generateQuestions.js
index 8aeb64301..0b6eac9ca 100644
--- a/source/engine/generateQuestions.js
+++ b/source/engine/generateQuestions.js
@@ -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),
diff --git a/source/engine/mecanisms.js b/source/engine/mecanisms.js
new file mode 100644
index 000000000..926e39b31
--- /dev/null
+++ b/source/engine/mecanisms.js
@@ -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:
+ {result.explanation.map(item => {item.jsx})}
+
+ }
+ />
+ }
+}
+
+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:
+
+ {rate}
+
+ }),
+ 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:
+ {conditionNode.jsx}
+
+ {childNumericalLogic.jsx}
+
+
+ }],
+ }
+ }, {
+ nodeValue: false,
+ category: 'mecanism',
+ name: "logique numérique",
+ type: 'boolean || numeric', // lol !
+ explanation: []
+ }),
+ node => ({...node,
+ jsx:
+ {node.explanation.map(item => {item.jsx})}
+
+ }
+ />
+ })
+ ))(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:
+
+ {v}
+
+ }
+ // 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:
+
+ {lazySelection}
+
+ }
+ }
+ 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:
+ {summedVariables.map(v => {v.jsx})}
+
+ }
+ />
+ }
+}
+
+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:
+
+ assiette:
+ {assiette.jsx}
+
+ {taux.nodeValue != 1 &&
+
+ taux:
+ {taux.jsx}
+ }
+ {facteur.nodeValue != 1 &&
+
+ facteur:
+ {facteur.jsx}
+ }
+ {plafond.nodeValue != Infinity &&
+
+ plafond:
+ {plafond.jsx}
+ }
+
+ }
+ />
+ }
+}
+
+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:
+ { composantes.map((c, i) =>
+ [
+
+ {R.toPairs(c.composante).map(([k,v]) =>
+ -
+ {k}:
+ {v}
+
+ )}
+
+
+ {c.jsx}
+
+ ,
+ i < (composantes.length - 1) &&
+ ]
+ )
+ }
+
+ }
+ />
+ }
+ }
+
+ 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:
+
+ assiette:
+ {assiette.jsx}
+
+
+ multiplicateur des tranches:
+ {multiplicateur.jsx}
+
+
+
+
+ Tranches de l'assiette |
+ Taux |
+
+ {v['tranches'].map(({'en-dessous de': maxOnly, 'au-dessus de': minOnly, de: min, 'à': max, taux}) =>
+
+
+ { maxOnly ? 'En dessous de ' + maxOnly
+ : minOnly ? 'Au dessus de ' + minOnly
+ : `De ${min} à ${max}` }
+ |
+ {taux} |
+
+ )}
+
+
+
+ }
+ />
+ }
+}
+
+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:
+ {contenders.map((item, i) =>
+
+ {v[i].description}
+ {item.jsx}
+
+ )}
+
+ }
+ />
+ }
+}
+
+export let mecanismError = (recurse,k,v) => {
+ throw "Le mécanisme est inconnu !"
+}
diff --git a/source/engine/rules.js b/source/engine/rules.js
index b359392dc..190b933ce 100644
--- a/source/engine/rules.js
+++ b/source/engine/rules.js
@@ -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]}),
diff --git a/source/engine/traverse.js b/source/engine/traverse.js
index b99694e44..12c2e60a6 100644
--- a/source/engine/traverse.js
+++ b/source/engine/traverse.js
@@ -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',
- {node.nodeValue}
- )(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:
- {filledExplanation[0].jsx}
- {parseResult.operator}
- {filledExplanation[1].jsx}
-
- }
- />
- }
- }
- 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',
- {node.nodeValue}
- )(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:
- {filledExplanation[0].jsx}
- {parseResult.operator}
- {filledExplanation[1].jsx}
-
- }
- />
- }
- }
- }
-
- //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:
-
- {rawNode}
-
- }
- }
-
-
- 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',
+ {node.nodeValue}
+ )(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:
- {result.explanation.map(item => {item.jsx})}
-
- }
- />
- }
- }
- 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:
-
- {rate}
-
- }),
- 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:
- {conditionNode.jsx}
-
- {childNumericalLogic.jsx}
-
-
- }],
- }
- }, {
- nodeValue: false,
- category: 'mecanism',
- name: "logique numérique",
- type: 'boolean || numeric', // lol !
- explanation: []
- }),
- node => ({...node,
- jsx:
- {node.explanation.map(item => {item.jsx})}
-
+
+ {filledExplanation[0].jsx}
+ {parseResult.operator}
+ {filledExplanation[1].jsx}
+
}
/>
- })
- ))
+ }
+ }
+ 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',
+ {node.nodeValue}
+ )(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:
+ {filledExplanation[0].jsx}
+ {parseResult.operator}
+ {filledExplanation[1].jsx}
+
+ }
+ />
+ }
+ }
+ },
+ 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:
-
- {v}
+
+ {rawNode}
}
- // 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:
-
- {lazySelection}
-
+ },
+ 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:
- {summedVariables.map(v => {v.jsx})}
-
- }
- />
- }
- }
+ 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:
-
- assiette:
- {assiette.jsx}
-
- {taux.nodeValue != 1 &&
-
- taux:
- {taux.jsx}
- }
- {facteur.nodeValue != 1 &&
-
- facteur:
- {facteur.jsx}
- }
- {plafond.nodeValue != Infinity &&
-
- plafond:
- {plafond.jsx}
- }
-
- }
- />
- }
- }
-
- 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:
- { composantes.map((c, i) =>
- [
-
- {R.toPairs(c.composante).map(([k,v]) =>
- -
- {k}:
- {v}
-
- )}
-
-
- {c.jsx}
-
- ,
- i < (composantes.length - 1) &&
- ]
- )
- }
-
- }
- />
- }
+ 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:
-
- assiette:
- {assiette.jsx}
-
-
- multiplicateur des tranches:
- {multiplicateur.jsx}
-
-
-
-
- Tranches de l'assiette |
- Taux |
-
- {v['tranches'].map(({'en-dessous de': maxOnly, 'au-dessus de': minOnly, de: min, 'à': max, taux}) =>
-
-
- { maxOnly ? 'En dessous de ' + maxOnly
- : minOnly ? 'Au dessus de ' + minOnly
- : `De ${min} à ${max}` }
- |
- {taux} |
-
- )}
-
-
-
- }
- />
- }
- }
-
- 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:
- {contenders.map((item, i) =>
-
- {v[i].description}
- {item.jsx}
-
- )}
-
- }
- />
- }
- }
-
- 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'
-}
diff --git a/source/engine/variables.js b/source/engine/variables.js
index 9e0bd0a45..11aff531b 100644
--- a/source/engine/variables.js
+++ b/source/engine/variables.js
@@ -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 => {
diff --git a/source/reducers.js b/source/reducers.js
index 7a62410dc..902bf4144 100644
--- a/source/reducers.js
+++ b/source/reducers.js
@@ -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'