From d3a38af3cf24fa520b7ceda0183d6cbbc751418a Mon Sep 17 00:00:00 2001 From: Laurent Bossavit Date: Mon, 26 Jun 2017 14:11:25 +0200 Subject: [PATCH 01/20] :white_check_mark: Ajouter des tests unitaires --- __tests__/utils.test.js | 9 +++++++++ package.json | 7 +++++-- 2 files changed, 14 insertions(+), 2 deletions(-) create mode 100644 __tests__/utils.test.js 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..7a7967543 100644 --- a/package.json +++ b/package.json @@ -54,6 +54,8 @@ "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", @@ -62,13 +64,14 @@ "redux-devtools-log-monitor": "^1.3.0", "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 webpack.config-test.js \"__tests__/**/*.test.js\"" } } From 1c330558df97ad354a6d3745c2b3fe67e0372105 Mon Sep 17 00:00:00 2001 From: Laurent Bossavit Date: Tue, 27 Jun 2017 15:14:30 +0200 Subject: [PATCH 02/20] :white_check_mark: Ajoute chai et quelques tests moins triviaux --- .gitignore | 1 + __tests__/rules.test.js | 20 ++++++++++++++++++++ package.json | 3 ++- 3 files changed, 23 insertions(+), 1 deletion(-) create mode 100644 __tests__/rules.test.js 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..caf56bd20 --- /dev/null +++ b/__tests__/rules.test.js @@ -0,0 +1,20 @@ +import {expect} from 'chai' +import {enrichRule} from '../source/engine/rules' + +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('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') + }); +}); diff --git a/package.json b/package.json index 7a7967543..0cda889e7 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", @@ -72,6 +73,6 @@ "start": "node source/server.js", "compile": "NODE_ENV='production' webpack --config source/webpack.config.js", "surge": "npm run compile && surge --domain scientific-wish.surge.sh -p ./ && rm -rf dist/", - "test": "mocha-webpack --webpack-config webpack.config-test.js \"__tests__/**/*.test.js\"" + "test": "mocha-webpack --webpack-config source/webpack.config.js \"__tests__/**/*.test.js\"" } } From c6899c239c8740f884547bd4c100db0f1c09d4bf Mon Sep 17 00:00:00 2001 From: Laurent Bossavit Date: Tue, 27 Jun 2017 16:50:16 +0200 Subject: [PATCH 03/20] =?UTF-8?q?:white=5Fcheck=5Fmark:=20Compl=C3=A8te=20?= =?UTF-8?q?les=20tests=20enrichRule?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- __tests__/rules.test.js | 1 + 1 file changed, 1 insertion(+) diff --git a/__tests__/rules.test.js b/__tests__/rules.test.js index caf56bd20..786e75918 100644 --- a/__tests__/rules.test.js +++ b/__tests__/rules.test.js @@ -10,6 +10,7 @@ describe('enrichRule', function() { 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') }); From c87ab9a887372ffa35529c83c32a07e28e6c820e Mon Sep 17 00:00:00 2001 From: Laurent Bossavit Date: Tue, 27 Jun 2017 18:30:29 +0200 Subject: [PATCH 04/20] :white_check_mark: Tests unitaires pour treatRuleRoot --- __tests__/traverse.test.js | 13 +++++++++++++ source/engine/traverse.js | 2 +- 2 files changed, 14 insertions(+), 1 deletion(-) create mode 100644 __tests__/traverse.test.js diff --git a/__tests__/traverse.test.js b/__tests__/traverse.test.js new file mode 100644 index 000000000..d03e8ba9b --- /dev/null +++ b/__tests__/traverse.test.js @@ -0,0 +1,13 @@ +import {expect} from 'chai' +import {treatRuleRoot} from '../source/engine/traverse' + +describe('treatRuleRoot', function() { + + let stateSelector = (state, name) => null + + it('should directly return simple numerical values', function() { + let rule = {formule: 3269} + expect(treatRuleRoot(stateSelector,rule)).to.have.property('nodeValue',3269) + }); + +}); diff --git a/source/engine/traverse.js b/source/engine/traverse.js index b99694e44..375253169 100644 --- a/source/engine/traverse.js +++ b/source/engine/traverse.js @@ -725,7 +725,7 @@ export let computeRuleValue = (formuleValue, condValue) => ? 0 : formuleValue -let treatRuleRoot = (situationGate, rule) => R.pipe( +export let treatRuleRoot = (situationGate, rule) => R.pipe( R.evolve({ // -> Voilà les attributs que peut comporter, pour l'instant, une Variable. // 'meta': pas de traitement pour l'instant From a608f0f5ae65a7a3054ff38405e04ffea464c1c0 Mon Sep 17 00:00:00 2001 From: Laurent Bossavit Date: Tue, 27 Jun 2017 19:53:37 +0200 Subject: [PATCH 05/20] =?UTF-8?q?:gear:=20Pour=20la=20testabilit=C3=A9,=20?= =?UTF-8?q?r=C3=A9duit=20l'acc=C3=A8s=20global=20aux=20r=C3=A8gles?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- __tests__/traverse.test.js | 15 ++++++- source/components/Aide.js | 4 +- source/components/Simulateur.js | 4 +- source/components/conversation/Explicable.js | 4 +- source/components/rule/Examples.js | 7 +-- source/components/rule/Rule.js | 4 +- source/engine/generateQuestions.js | 12 +++--- source/engine/rules.js | 37 ++++++---------- source/engine/traverse.js | 45 ++++++++------------ source/engine/variables.js | 2 +- source/reducers.js | 2 - 11 files changed, 63 insertions(+), 73 deletions(-) diff --git a/__tests__/traverse.test.js b/__tests__/traverse.test.js index d03e8ba9b..6a8b62afa 100644 --- a/__tests__/traverse.test.js +++ b/__tests__/traverse.test.js @@ -1,5 +1,6 @@ import {expect} from 'chai' import {treatRuleRoot} from '../source/engine/traverse' +import {analyseSituation} from '../source/engine/traverse' describe('treatRuleRoot', function() { @@ -7,7 +8,19 @@ describe('treatRuleRoot', function() { it('should directly return simple numerical values', function() { let rule = {formule: 3269} - expect(treatRuleRoot(stateSelector,rule)).to.have.property('nodeValue',3269) + expect(treatRuleRoot(stateSelector,[rule],rule)).to.have.property('nodeValue',3269) + }); + +}); + +describe('analyseSituation', function() { + + let stateSelector = (state, name) => null + + 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) }); }); 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/rules.js b/source/engine/rules.js index b359392dc..a3e999caf 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 */ @@ -159,16 +146,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 375253169..aebbfff42 100644 --- a/source/engine/traverse.js +++ b/source/engine/traverse.js @@ -1,5 +1,5 @@ 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' @@ -54,18 +54,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,8 +117,8 @@ let buildNegatedVariable = variable => { } } -let treat = (situationGate, rule) => rawNode => { - let reTreat = treat(situationGate, rule) +let treat = (situationGate, rules, rule) => rawNode => { + let reTreat = treat(situationGate, rules, rule) if (R.is(String)(rawNode)) { /* On a à faire à un string, donc à une expression infixe. @@ -134,17 +135,17 @@ let treat = (situationGate, rule) => rawNode => { throw "Attention ! Erreur de traitement de l'expression : " + rawNode if (parseResult.category == 'variable') - return fillVariableNode(rule, situationGate)(parseResult) + return fillVariableNode(rules, rule, situationGate)(parseResult) if (parseResult.category == 'negatedVariable') return buildNegatedVariable( - fillVariableNode(rule, situationGate)(parseResult.variable) + fillVariableNode(rules, 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', 'variable'), fillVariableNode(rules, rule, situationGate)], [R.propEq('category', 'value'), node => R.assoc('jsx', {node.nodeValue} @@ -188,7 +189,7 @@ let treat = (situationGate, rule) => rawNode => { let filledExplanation = parseResult.explanation.map( R.cond([ - [R.propEq('category', 'variable'), fillVariableNode(rule, situationGate)], + [R.propEq('category', 'variable'), fillVariableNode(rules, rule, situationGate)], [R.propEq('category', 'value'), node => R.assoc('jsx', {node.nodeValue} @@ -679,7 +680,7 @@ let treat = (situationGate, rule) => rawNode => { } if (k === 'le maximum de') { - let contenders = v.map(treat(situationGate, rule)), + let contenders = v.map(treat(situationGate, rules, rule)), contenderValues = R.pluck('nodeValue')(contenders), stopEverything = R.contains(null, contenderValues), maxValue = R.max(...contenderValues), @@ -725,7 +726,7 @@ export let computeRuleValue = (formuleValue, condValue) => ? 0 : formuleValue -export 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 +734,7 @@ export 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 +763,7 @@ export 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 +813,13 @@ export 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' From 44ffa7695590510b0680ccdaaf72cbaaeb86d90f Mon Sep 17 00:00:00 2001 From: Laurent Bossavit Date: Tue, 27 Jun 2017 22:31:03 +0200 Subject: [PATCH 06/20] =?UTF-8?q?:white=5Fcheck=5Fmark:=20Ajout=20des=20so?= =?UTF-8?q?urcemap=20pour=20aider=20=C3=A0=20la=20mise=20au=20point?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index 0cda889e7..cf3e278a9 100644 --- a/package.json +++ b/package.json @@ -63,6 +63,7 @@ "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.6.1", @@ -73,6 +74,6 @@ "start": "node source/server.js", "compile": "NODE_ENV='production' webpack --config source/webpack.config.js", "surge": "npm run compile && surge --domain scientific-wish.surge.sh -p ./ && rm -rf dist/", - "test": "mocha-webpack --webpack-config source/webpack.config.js \"__tests__/**/*.test.js\"" + "test": "mocha-webpack --webpack-config source/webpack.config.js --require source-map-support/register \"__tests__/**/*.test.js\"" } } From c5f63467ccc805a7509dcbb478adb2aedc818c7c Mon Sep 17 00:00:00 2001 From: Laurent Bossavit Date: Tue, 27 Jun 2017 22:31:34 +0200 Subject: [PATCH 07/20] =?UTF-8?q?:white=5Fcheck=5Fmark:=20Tester=20les=20e?= =?UTF-8?q?xpressions=20r=C3=A9f=C3=A9ren=C3=A7ant=20des=20r=C3=A8gles?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- __tests__/traverse.test.js | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/__tests__/traverse.test.js b/__tests__/traverse.test.js index 6a8b62afa..29c68b3e4 100644 --- a/__tests__/traverse.test.js +++ b/__tests__/traverse.test.js @@ -1,4 +1,5 @@ import {expect} from 'chai' +import {enrichRule} from '../source/engine/rules' import {treatRuleRoot} from '../source/engine/traverse' import {analyseSituation} from '../source/engine/traverse' @@ -23,4 +24,18 @@ describe('analyseSituation', function() { 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) + }); + + 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) + }); + }); From 9a4afd4f6807fbd9409a124cadf1b69ddf8062e0 Mon Sep 17 00:00:00 2001 From: Laurent Bossavit Date: Tue, 27 Jun 2017 23:09:03 +0200 Subject: [PATCH 08/20] :white_check_mark: Restructurer treat() vers des conditionnelles --- source/engine/traverse.js | 1141 +++++++++++++++++++------------------ 1 file changed, 573 insertions(+), 568 deletions(-) diff --git a/source/engine/traverse.js b/source/engine/traverse.js index aebbfff42..4af2ed410 100644 --- a/source/engine/traverse.js +++ b/source/engine/traverse.js @@ -118,600 +118,605 @@ let buildNegatedVariable = variable => { } let treat = (situationGate, rules, rule) => rawNode => { - let reTreat = treat(situationGate, rules, rule) + // 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(rules, rule, situationGate)(parseResult) - if (parseResult.category == 'negatedVariable') - return buildNegatedVariable( - fillVariableNode(rules, rule, situationGate)(parseResult.variable) - ) - - 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) - ] - ]) - ), - [{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(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) - - 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), + 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(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) + + return { + text: rawNode, + nodeValue: nodeValue, + category: 'comparison', + type: 'boolean', + explanation: filledExplanation, + jsx: + {filledExplanation[0].jsx} + {parseResult.operator} + {filledExplanation[1].jsx} + + } + /> + } + } + }, + treatNumber = rawNode => { + return { + category: 'number', + nodeValue: rawNode, type: 'numeric', - category: 'percentage', - percentage: rate, - explanation: null, jsx: - - {rate} + + {rawNode} - }), - 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 + } + }, + 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)) - 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: {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 + ), + 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.explanation.map(item =>
  • {item.jsx}
  • )} + {result.explanation.map(item =>
  • {item.jsx}
  • )} } /> - }) - )) - - if (k === 'logique numérique') { - return treatNumericalLogicRec(v) - } - - if (k === 'taux') { - 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 = 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) - - return { - nodeValue, - category: 'mecanism', - name: 'somme', - type: 'numeric', - explanation: summedVariables, - jsx: - {summedVariables.map(v =>
  • {v.jsx}
  • )} - } - /> - } - } - - 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) &&
  • - ] - ) - } - - } - /> } - } + 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) + } - 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( + //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.values, R.map(val), R.any(R.equals(null)) - ) - )(tranches), - */ - // nulled = anyNull([assiette, multiplicateur]), - nulled = val(assiette) == null || val(multiplicateur) == null, + 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 - 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 {...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}
  • )} + + } + /> + }) + )) - 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} -
  • - - - - - - - {v['tranches'].map(({'en-dessous de': maxOnly, 'au-dessus de': minOnly, de: min, 'à': max, taux}) => - - - - - )} - -
    Tranches de l'assietteTaux
    - { maxOnly ? 'En dessous de ' + maxOnly - : minOnly ? 'Au dessus de ' + minOnly - : `De ${min} à ${max}` } - {taux}
    - + if (k === 'logique numérique') { + return treatNumericalLogicRec(v) + } + + if (k === 'taux') { + 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} + + } } - /> - } - } - - if (k === 'le maximum de') { - let contenders = v.map(treat(situationGate, rules, 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} -
  • - )} - + 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) + + return { + nodeValue, + category: 'mecanism', + name: 'somme', + type: 'numeric', + explanation: summedVariables, + jsx: + {summedVariables.map(v =>
  • {v.jsx}
  • )} + + } + /> + } + } + + 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) &&
  • + ] + ) + } + + } + /> + } + } + + 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} +
  • + + + + + + + {v['tranches'].map(({'en-dessous de': maxOnly, 'au-dessus de': minOnly, de: min, 'à': max, taux}) => + + + + + )} + +
    Tranches de l'assietteTaux
    + { 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, rules, 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 !" } - } - - throw "Le mécanisme est inconnu !" + let onNodeType = R.cond([ + [R.is(String), treatString], + [R.is(Number), treatNumber], + [!R.is(Object), treatOther], + [R.T, treatObject] + ]) + return onNodeType(rawNode) } //TODO c'est moche : From a7d5f3dc552c7b8859a65f054928785a59e819a5 Mon Sep 17 00:00:00 2001 From: Laurent Bossavit Date: Tue, 27 Jun 2017 23:44:20 +0200 Subject: [PATCH 09/20] =?UTF-8?q?:white=5Fcheck=5Fmark:=20Tester=20la=20lo?= =?UTF-8?q?gique=20d'applicabilit=C3=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- __tests__/traverse.test.js | 30 ++++++++++++++++++++++++++---- 1 file changed, 26 insertions(+), 4 deletions(-) diff --git a/__tests__/traverse.test.js b/__tests__/traverse.test.js index 29c68b3e4..b137b3c83 100644 --- a/__tests__/traverse.test.js +++ b/__tests__/traverse.test.js @@ -3,9 +3,9 @@ import {enrichRule} from '../source/engine/rules' import {treatRuleRoot} from '../source/engine/traverse' import {analyseSituation} from '../source/engine/traverse' -describe('treatRuleRoot', function() { +let stateSelector = (state, name) => null - let stateSelector = (state, name) => null +describe('treatRuleRoot', function() { it('should directly return simple numerical values', function() { let rule = {formule: 3269} @@ -16,8 +16,6 @@ describe('treatRuleRoot', function() { describe('analyseSituation', function() { - let stateSelector = (state, name) => null - it('should directly return simple numerical values', function() { let rule = {name: "startHere", formule: 3269} let rules = [rule] @@ -30,6 +28,10 @@ describe('analyseSituation', function() { 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"}, @@ -38,4 +40,24 @@ describe('analyseSituation', function() { 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) + }); + */ + }); From 9b427690ecce7fc0dd69d3be5b6921f995462401 Mon Sep 17 00:00:00 2001 From: Laurent Bossavit Date: Wed, 28 Jun 2017 00:38:55 +0200 Subject: [PATCH 10/20] :white_check_mark: Tester la logique n-conditions, corriger l'aiguillage, supprimer le warning https://fb.me/react-warning-keys --- __tests__/traverse.test.js | 11 +++++++++++ source/engine/traverse.js | 6 +++--- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/__tests__/traverse.test.js b/__tests__/traverse.test.js index b137b3c83..c12370477 100644 --- a/__tests__/traverse.test.js +++ b/__tests__/traverse.test.js @@ -61,3 +61,14 @@ describe('analyseSituation on raw rules', function() { */ }); + +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) + }); + +}); diff --git a/source/engine/traverse.js b/source/engine/traverse.js index 4af2ed410..e6a0ff37d 100644 --- a/source/engine/traverse.js +++ b/source/engine/traverse.js @@ -286,7 +286,7 @@ let treat = (situationGate, rules, rule) => rawNode => { value={result.nodeValue} child={
      - {result.explanation.map(item =>
    • {item.jsx}
    • )} + {result.explanation.map(item =>
    • {item.jsx}
    • )}
    } /> @@ -713,8 +713,8 @@ let treat = (situationGate, rules, rule) => rawNode => { let onNodeType = R.cond([ [R.is(String), treatString], [R.is(Number), treatNumber], - [!R.is(Object), treatOther], - [R.T, treatObject] + [R.is(Object), treatObject], + [R.T, treatOther] ]) return onNodeType(rawNode) } From 8ca86d0029cbe591504e060a67dbef83267df8ce Mon Sep 17 00:00:00 2001 From: Laurent Bossavit Date: Wed, 28 Jun 2017 01:02:36 +0200 Subject: [PATCH 11/20] =?UTF-8?q?:white=5Fcheck=5Fmark:=20Tester=20les=20m?= =?UTF-8?q?=C3=A9canismes=20du=20et=20logique,=20de=20l'aiguillage?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- __tests__/traverse.test.js | 19 +++ source/engine/traverse.js | 275 ++++++++++++++++++------------------- 2 files changed, 155 insertions(+), 139 deletions(-) diff --git a/__tests__/traverse.test.js b/__tests__/traverse.test.js index c12370477..2b84500a7 100644 --- a/__tests__/traverse.test.js +++ b/__tests__/traverse.test.js @@ -71,4 +71,23 @@ describe('analyseSituation with mecanisms', function() { 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) + }); + }); diff --git a/source/engine/traverse.js b/source/engine/traverse.js index e6a0ff37d..320ef6158 100644 --- a/source/engine/traverse.js +++ b/source/engine/traverse.js @@ -246,6 +246,139 @@ let treat = (situationGate, rules, rule) => rawNode => { console.log() // eslint-disable-line no-console throw 'Cette donnée : ' + rawNode + ' doit être un Number, String ou Object' }, + mecanismOneOf = (k, v) => { + 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 + ), + 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}
  • )} + + } + /> + } + }, + mecanismAllOf = (k,v) => { + 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) + }, + mecanismNumericalLogic = (k,v) => + 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 = mecanismNumericalLogic(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), treatObject = rawNode => { let mecanisms = R.intersection(R.keys(rawNode), R.keys(knownMecanisms)) @@ -257,145 +390,9 @@ let treat = (situationGate, rules, rule) => rawNode => { 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 - ), - 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) - } - - //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}
  • )} - - } - /> - }) - )) - - if (k === 'logique numérique') { - return treatNumericalLogicRec(v) - } + if (k === 'une de ces conditions') return mecanismOneOf(k,v) + if (k === 'toutes ces conditions') return mecanismAllOf(k,v) + if (k === 'logique numérique') return mecanismNumericalLogic(k,v) if (k === 'taux') { let reg = /^(\d+(\.\d+)?)\%$/ From a19c5739a656ecdeb3593011ba01626277e1a5b0 Mon Sep 17 00:00:00 2001 From: Laurent Bossavit Date: Wed, 28 Jun 2017 01:06:37 +0200 Subject: [PATCH 12/20] =?UTF-8?q?:white=5Fcheck=5Fmark:=20Tester=20le=20m?= =?UTF-8?q?=C3=A9canisme=20de=20taux?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- __tests__/traverse.test.js | 7 +++ source/engine/traverse.js | 88 +++++++++++++++++++------------------- 2 files changed, 51 insertions(+), 44 deletions(-) diff --git a/__tests__/traverse.test.js b/__tests__/traverse.test.js index 2b84500a7..cf02c39c1 100644 --- a/__tests__/traverse.test.js +++ b/__tests__/traverse.test.js @@ -90,4 +90,11 @@ describe('analyseSituation with mecanisms', function() { 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) + }); + }); diff --git a/source/engine/traverse.js b/source/engine/traverse.js index 320ef6158..7d973485d 100644 --- a/source/engine/traverse.js +++ b/source/engine/traverse.js @@ -379,6 +379,49 @@ let treat = (situationGate, rules, rule) => rawNode => { /> }) ))(v), + mecanismTaux = (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 = reTreat(v) + return { + type: 'numeric', + category: 'percentage', + percentage: node.nodeValue, + nodeValue: node.nodeValue, + explanation: node, + jsx: node.jsx + } + } + }, treatObject = rawNode => { let mecanisms = R.intersection(R.keys(rawNode), R.keys(knownMecanisms)) @@ -393,50 +436,7 @@ let treat = (situationGate, rules, rule) => rawNode => { if (k === 'une de ces conditions') return mecanismOneOf(k,v) if (k === 'toutes ces conditions') return mecanismAllOf(k,v) if (k === 'logique numérique') return mecanismNumericalLogic(k,v) - - if (k === 'taux') { - 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 = reTreat(v) - return { - type: 'numeric', - category: 'percentage', - percentage: node.nodeValue, - nodeValue: node.nodeValue, - explanation: node, - jsx: node.jsx - } - } - } + if (k === 'taux') return mecanismTaux(k,v) // Une simple somme de variables if (k === 'somme') { From 6725673abc77b4f3e80256a3ee16defeb641d790 Mon Sep 17 00:00:00 2001 From: Laurent Bossavit Date: Wed, 28 Jun 2017 01:19:38 +0200 Subject: [PATCH 13/20] =?UTF-8?q?:white=5Fcheck=5Fmark:=20Tester=20le=20m?= =?UTF-8?q?=C3=A9canisme=20de=20somme?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- __tests__/traverse.test.js | 14 +++++++++ source/engine/traverse.js | 58 +++++++++++++++++++------------------- 2 files changed, 43 insertions(+), 29 deletions(-) diff --git a/__tests__/traverse.test.js b/__tests__/traverse.test.js index cf02c39c1..527323d8c 100644 --- a/__tests__/traverse.test.js +++ b/__tests__/traverse.test.js @@ -12,6 +12,13 @@ describe('treatRuleRoot', function() { 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() { @@ -97,4 +104,11 @@ describe('analyseSituation with mecanisms', function() { 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) + }); + }); diff --git a/source/engine/traverse.js b/source/engine/traverse.js index 7d973485d..90f6449ae 100644 --- a/source/engine/traverse.js +++ b/source/engine/traverse.js @@ -233,6 +233,7 @@ let treat = (situationGate, rules, rule) => rawNode => { }, treatNumber = rawNode => { return { + text: ""+rawNode, category: 'number', nodeValue: rawNode, type: 'numeric', @@ -379,7 +380,7 @@ let treat = (situationGate, rules, rule) => rawNode => { /> }) ))(v), - mecanismTaux = (k,v) => { + mecanismPercentage = (k,v) => { let reg = /^(\d+(\.\d+)?)\%$/ if (R.test(reg)(v)) return { @@ -422,6 +423,31 @@ let treat = (situationGate, rules, rule) => rawNode => { } } }, + mecanismSum = (k,v) => { + let + summedVariables = v.map(reTreat), + 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}
  • )} + + } + /> + } + }, treatObject = rawNode => { let mecanisms = R.intersection(R.keys(rawNode), R.keys(knownMecanisms)) @@ -436,34 +462,8 @@ let treat = (situationGate, rules, rule) => rawNode => { if (k === 'une de ces conditions') return mecanismOneOf(k,v) if (k === 'toutes ces conditions') return mecanismAllOf(k,v) if (k === 'logique numérique') return mecanismNumericalLogic(k,v) - if (k === 'taux') return mecanismTaux(k,v) - - // 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) - - return { - nodeValue, - category: 'mecanism', - name: 'somme', - type: 'numeric', - explanation: summedVariables, - jsx: - {summedVariables.map(v =>
  • {v.jsx}
  • )} - - } - /> - } - } + if (k === 'taux') return mecanismPercentage(k,v) + if (k === 'somme') return mecanismSum(k,v) if (k === 'multiplication') { let From ab197a4a39afdebcea545348257f5b0e7811efa1 Mon Sep 17 00:00:00 2001 From: Laurent Bossavit Date: Wed, 28 Jun 2017 07:57:47 +0200 Subject: [PATCH 14/20] =?UTF-8?q?:white=5Fcheck=5Fmark:=20Tester=20le=20m?= =?UTF-8?q?=C3=A9canisme=20de=20multiplication?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- __tests__/traverse.test.js | 7 +++ source/engine/traverse.js | 118 ++++++++++++++++++------------------- 2 files changed, 66 insertions(+), 59 deletions(-) diff --git a/__tests__/traverse.test.js b/__tests__/traverse.test.js index 527323d8c..bcb8ce57b 100644 --- a/__tests__/traverse.test.js +++ b/__tests__/traverse.test.js @@ -111,4 +111,11 @@ describe('analyseSituation with mecanisms', function() { 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) + }); + }); diff --git a/source/engine/traverse.js b/source/engine/traverse.js index 90f6449ae..eb7c1d1b0 100644 --- a/source/engine/traverse.js +++ b/source/engine/traverse.js @@ -448,6 +448,64 @@ let treat = (situationGate, rules, rule) => rawNode => { /> } }, + mecanismProduct = (k,v) => { + 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} +
  • } + + } + /> + } + }, treatObject = rawNode => { let mecanisms = R.intersection(R.keys(rawNode), R.keys(knownMecanisms)) @@ -464,65 +522,7 @@ let treat = (situationGate, rules, rule) => rawNode => { if (k === 'logique numérique') return mecanismNumericalLogic(k,v) if (k === 'taux') return mecanismPercentage(k,v) if (k === 'somme') return mecanismSum(k,v) - - 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 === 'multiplication') return mecanismProduct(k,v) if (k === 'barème') { // Sous entendu : barème en taux marginaux. From ab7073464f79f04db0d1dd0c0493084a1a0253b8 Mon Sep 17 00:00:00 2001 From: Laurent Bossavit Date: Wed, 28 Jun 2017 08:39:11 +0200 Subject: [PATCH 15/20] =?UTF-8?q?:white=5Fcheck=5Fmark:=20Tester=20le=20m?= =?UTF-8?q?=C3=A9canisme=20de=20bar=C3=A8me?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- __tests__/traverse.test.js | 11 ++ source/engine/traverse.js | 298 ++++++++++++++++++------------------- 2 files changed, 160 insertions(+), 149 deletions(-) diff --git a/__tests__/traverse.test.js b/__tests__/traverse.test.js index bcb8ce57b..540a2a3c7 100644 --- a/__tests__/traverse.test.js +++ b/__tests__/traverse.test.js @@ -118,4 +118,15 @@ describe('analyseSituation with mecanisms', function() { 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) + }); + }); diff --git a/source/engine/traverse.js b/source/engine/traverse.js index eb7c1d1b0..d1b82d318 100644 --- a/source/engine/traverse.js +++ b/source/engine/traverse.js @@ -506,6 +506,154 @@ let treat = (situationGate, rules, rule) => rawNode => { /> } }, + mecanismScale = (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 => + ({ + ... 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) &&
  • + ] + ) + } + + } + /> + } + } + + 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} +
  • + + + + + + + {v['tranches'].map(({'en-dessous de': maxOnly, 'au-dessus de': minOnly, de: min, 'à': max, taux}) => + + + + + )} + +
    Tranches de l'assietteTaux
    + { maxOnly ? 'En dessous de ' + maxOnly + : minOnly ? 'Au dessus de ' + minOnly + : `De ${min} à ${max}` } + {taux}
    + + } + /> + } + }, treatObject = rawNode => { let mecanisms = R.intersection(R.keys(rawNode), R.keys(knownMecanisms)) @@ -523,155 +671,7 @@ let treat = (situationGate, rules, rule) => rawNode => { if (k === 'taux') return mecanismPercentage(k,v) if (k === 'somme') return mecanismSum(k,v) if (k === 'multiplication') return mecanismProduct(k,v) - - 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) &&
  • - ] - ) - } - - } - /> - } - } - - 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} -
  • - - - - - - - {v['tranches'].map(({'en-dessous de': maxOnly, 'au-dessus de': minOnly, de: min, 'à': max, taux}) => - - - - - )} - -
    Tranches de l'assietteTaux
    - { maxOnly ? 'En dessous de ' + maxOnly - : minOnly ? 'Au dessus de ' + minOnly - : `De ${min} à ${max}` } - {taux}
    - - } - /> - } - } + if (k === 'barème') return mecanismScale(k,v) if (k === 'le maximum de') { let contenders = v.map(treat(situationGate, rules, rule)), From bfd1f2a3af6180a28f80ba5c339b7a61bf16f169 Mon Sep 17 00:00:00 2001 From: Laurent Bossavit Date: Wed, 28 Jun 2017 09:06:52 +0200 Subject: [PATCH 16/20] =?UTF-8?q?:white=5Fcheck=5Fmark:=20Tester=20le=20m?= =?UTF-8?q?=C3=A9canisme=20de=20maximum,=20restructurer=20l'aiguillage?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- __tests__/traverse.test.js | 7 ++++ source/engine/traverse.js | 84 ++++++++++++++++++++------------------ 2 files changed, 52 insertions(+), 39 deletions(-) diff --git a/__tests__/traverse.test.js b/__tests__/traverse.test.js index 540a2a3c7..b718fd712 100644 --- a/__tests__/traverse.test.js +++ b/__tests__/traverse.test.js @@ -129,4 +129,11 @@ describe('analyseSituation with mecanisms', function() { 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/source/engine/traverse.js b/source/engine/traverse.js index d1b82d318..bdb64dd0a 100644 --- a/source/engine/traverse.js +++ b/source/engine/traverse.js @@ -654,6 +654,39 @@ let treat = (situationGate, rules, rule) => rawNode => { /> } }, + mecanismMax = (k,v) => { + let contenders = v.map(reTreat), + 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} +
  • + )} + + } + /> + } + }, + mecanismError = (k,v) => { + throw "Le mécanisme est inconnu !" + }, treatObject = rawNode => { let mecanisms = R.intersection(R.keys(rawNode), R.keys(knownMecanisms)) @@ -665,46 +698,19 @@ let treat = (situationGate, rules, rule) => rawNode => { let k = R.head(mecanisms), v = rawNode[k] - if (k === 'une de ces conditions') return mecanismOneOf(k,v) - if (k === 'toutes ces conditions') return mecanismAllOf(k,v) - if (k === 'logique numérique') return mecanismNumericalLogic(k,v) - if (k === 'taux') return mecanismPercentage(k,v) - if (k === 'somme') return mecanismSum(k,v) - if (k === 'multiplication') return mecanismProduct(k,v) - if (k === 'barème') return mecanismScale(k,v) + 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 === 'le maximum de') { - let contenders = v.map(treat(situationGate, rules, 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 !" + return action(k,v) } let onNodeType = R.cond([ From 552af3f98ddaa03f840f9d8250cc3b3e589d48e9 Mon Sep 17 00:00:00 2001 From: Laurent Bossavit Date: Wed, 28 Jun 2017 09:31:37 +0200 Subject: [PATCH 17/20] =?UTF-8?q?:gear:=20Extraire=20les=20m=C3=A9canismes?= =?UTF-8?q?=20dans=20un=20fichier=20d=C3=A9di=C3=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- source/engine/mecanisms.js | 451 +++++++++++++++++++++++++++++++++++++ source/engine/traverse.js | 451 +------------------------------------ 2 files changed, 453 insertions(+), 449 deletions(-) create mode 100644 source/engine/mecanisms.js diff --git a/source/engine/mecanisms.js b/source/engine/mecanisms.js new file mode 100644 index 000000000..98ec0802b --- /dev/null +++ b/source/engine/mecanisms.js @@ -0,0 +1,451 @@ +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}
  • )} + + } + /> + } + }, + 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) + }, + mecanismNumericalLogic = (recurse, k,v) => + 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(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), + 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 + } + } + }, + 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}
  • )} + + } + /> + } + }, + 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} +
  • } + + } + /> + } + }, + 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} +
  • + + + + + + + {v['tranches'].map(({'en-dessous de': maxOnly, 'au-dessus de': minOnly, de: min, 'à': max, taux}) => + + + + + )} + +
    Tranches de l'assietteTaux
    + { maxOnly ? 'En dessous de ' + maxOnly + : minOnly ? 'Au dessus de ' + minOnly + : `De ${min} à ${max}` } + {taux}
    + + } + /> + } + }, + 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} +
  • + )} + + } + /> + } + }, + mecanismError = (recurse,k,v) => { + throw "Le mécanisme est inconnu !" + } diff --git a/source/engine/traverse.js b/source/engine/traverse.js index bdb64dd0a..12c2e60a6 100644 --- a/source/engine/traverse.js +++ b/source/engine/traverse.js @@ -6,8 +6,7 @@ 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, @@ -247,446 +240,6 @@ let treat = (situationGate, rules, rule) => rawNode => { console.log() // eslint-disable-line no-console throw 'Cette donnée : ' + rawNode + ' doit être un Number, String ou Object' }, - mecanismOneOf = (k, v) => { - 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 - ), - 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}
  • )} - - } - /> - } - }, - mecanismAllOf = (k,v) => { - 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) - }, - mecanismNumericalLogic = (k,v) => - 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 = mecanismNumericalLogic(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), - mecanismPercentage = (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 = reTreat(v) - return { - type: 'numeric', - category: 'percentage', - percentage: node.nodeValue, - nodeValue: node.nodeValue, - explanation: node, - jsx: node.jsx - } - } - }, - mecanismSum = (k,v) => { - let - summedVariables = v.map(reTreat), - 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}
  • )} - - } - /> - } - }, - mecanismProduct = (k,v) => { - 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} -
  • } - - } - /> - } - }, - mecanismScale = (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 => - ({ - ... 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) &&
  • - ] - ) - } - - } - /> - } - } - - 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} -
  • - - - - - - - {v['tranches'].map(({'en-dessous de': maxOnly, 'au-dessus de': minOnly, de: min, 'à': max, taux}) => - - - - - )} - -
    Tranches de l'assietteTaux
    - { maxOnly ? 'En dessous de ' + maxOnly - : minOnly ? 'Au dessus de ' + minOnly - : `De ${min} à ${max}` } - {taux}
    - - } - /> - } - }, - mecanismMax = (k,v) => { - let contenders = v.map(reTreat), - 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} -
  • - )} - - } - /> - } - }, - mecanismError = (k,v) => { - throw "Le mécanisme est inconnu !" - }, treatObject = rawNode => { let mecanisms = R.intersection(R.keys(rawNode), R.keys(knownMecanisms)) @@ -710,7 +263,7 @@ let treat = (situationGate, rules, rule) => rawNode => { }, action = R.pathOr(mecanismError,[k],dispatch) - return action(k,v) + return action(reTreat,k,v) } let onNodeType = R.cond([ From 92903233f97b69254406edc8225aa148e373f077 Mon Sep 17 00:00:00 2001 From: Laurent Bossavit Date: Wed, 28 Jun 2017 11:28:06 +0200 Subject: [PATCH 18/20] =?UTF-8?q?:bug:=20Corrige=20l'appel=20r=C3=A9cursif?= =?UTF-8?q?=20au=20m=C3=A9canisme=20d'aiguillage?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- source/engine/mecanisms.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/source/engine/mecanisms.js b/source/engine/mecanisms.js index 98ec0802b..0577ee8d2 100644 --- a/source/engine/mecanisms.js +++ b/source/engine/mecanisms.js @@ -89,7 +89,7 @@ export let let {nodeValue, explanation} = memo, conditionNode = recurse(condition), // can be a 'comparison', a 'variable', TODO a 'negation' - childNumericalLogic = mecanismNumericalLogic(condition, consequence), + childNumericalLogic = mecanismNumericalLogic(recurse, condition, consequence), nextNodeValue = conditionNode.nodeValue == null ? // Si la proposition n'est pas encore résolvable null From c1624d3a84473aa5f1c8dbd99a22a36f61251a03 Mon Sep 17 00:00:00 2001 From: Laurent Bossavit Date: Wed, 28 Jun 2017 12:08:32 +0200 Subject: [PATCH 19/20] =?UTF-8?q?:gear:=20S=C3=A9pare=20les=20fonctions=20?= =?UTF-8?q?des=20m=C3=A9canismes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- source/engine/mecanisms.js | 868 +++++++++++++++++++------------------ 1 file changed, 438 insertions(+), 430 deletions(-) diff --git a/source/engine/mecanisms.js b/source/engine/mecanisms.js index 0577ee8d2..926e39b31 100644 --- a/source/engine/mecanisms.js +++ b/source/engine/mecanisms.js @@ -8,444 +8,452 @@ let transformPercentage = 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}
  • )} - - } - /> - } - }, - 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) - }, - mecanismNumericalLogic = (recurse, k,v) => - 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), - 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 - } - } - }, - 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}
  • )} - - } - /> - } - }, - 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} -
  • } - - } - /> - } - }, - 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 +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 ), - //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} -
  • - - - - - - - {v['tranches'].map(({'en-dessous de': maxOnly, 'au-dessus de': minOnly, de: min, 'à': max, taux}) => - - - - - )} - -
    Tranches de l'assietteTaux
    - { maxOnly ? 'En dessous de ' + maxOnly - : minOnly ? 'Au dessus de ' + minOnly - : `De ${min} à ${max}` } - {taux}
    - - } - /> + explanation: [...explanation, child] } - }, - 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 + }, { + 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}
  • )} + + } + /> + } +} - return { - type: 'numeric', +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: 'le maximum de', - nodeValue, - explanation: contenders, + name: "logique numérique", + type: 'boolean || numeric', // lol ! + explanation: [] + }), + node => ({...node, jsx: - {contenders.map((item, i) => -
  • -
    {v[i].description}
    - {item.jsx} -
  • - )} + {node.explanation.map(item =>
  • {item.jsx}
  • )} } /> - } - }, - mecanismError = (recurse,k,v) => { - throw "Le mécanisme est inconnu !" + }) + ))(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} +
  • + + + + + + + {v['tranches'].map(({'en-dessous de': maxOnly, 'au-dessus de': minOnly, de: min, 'à': max, taux}) => + + + + + )} + +
    Tranches de l'assietteTaux
    + { 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 !" +} From 777ddbc7b79e83905abda7aefa352401d85921df Mon Sep 17 00:00:00 2001 From: Laurent Bossavit Date: Wed, 28 Jun 2017 16:12:10 +0200 Subject: [PATCH 20/20] :white_check_mark: Tester getObjectives --- __tests__/rules.test.js | 22 +++++++++++++++++++++- source/engine/rules.js | 4 +++- 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/__tests__/rules.test.js b/__tests__/rules.test.js index 786e75918..796ea56ec 100644 --- a/__tests__/rules.test.js +++ b/__tests__/rules.test.js @@ -1,5 +1,8 @@ import {expect} from 'chai' -import {enrichRule} from '../source/engine/rules' +import {enrichRule, collectMissingVariables, getObjectives} from '../source/engine/rules' +import {analyseSituation} from '../source/engine/traverse' + +let stateSelector = (state, name) => null describe('enrichRule', function() { @@ -19,3 +22,20 @@ describe('enrichRule', function() { 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/source/engine/rules.js b/source/engine/rules.js index a3e999caf..190b933ce 100644 --- a/source/engine/rules.js +++ b/source/engine/rules.js @@ -117,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) }