From 1e3398f0506bdc16ee53d1d0409c7662a2c4aa2d Mon Sep 17 00:00:00 2001 From: Laurent Bossavit Date: Tue, 11 Jul 2017 16:00:10 +0200 Subject: [PATCH] :gear: Analyse top-down, correction de collect/buildNextSteps --- source/engine/generateQuestions.js | 46 ++++++---------------- source/engine/mecanisms.js | 11 ++++-- source/engine/rules.js | 1 + source/engine/traverse.js | 63 ++++++++++++++++++++++++------ source/reducers.js | 4 +- test/generateQuestions.test.js | 12 +++--- test/rules.test.js | 6 +-- 7 files changed, 83 insertions(+), 60 deletions(-) diff --git a/source/engine/generateQuestions.js b/source/engine/generateQuestions.js index 5b00acc1b..8b263df62 100644 --- a/source/engine/generateQuestions.js +++ b/source/engine/generateQuestions.js @@ -6,7 +6,7 @@ import Question from 'Components/conversation/Question' import Input from 'Components/conversation/Input' import formValueTypes from 'Components/conversation/formValueTypes' -import {analyseSituation} from './traverse' +import {analyseSituation, evaluateNode} from './traverse' import {formValueSelector} from 'redux-form' import {rules, findRuleByDottedName, findVariantsAndRecords} from './rules' @@ -42,7 +42,7 @@ export let analyse = rootVariable => R.pipe( // On peut travailler sur une somme, les objectifs sont alors les variables de cette somme. // Ou sur une variable unique ayant une formule, elle est elle-même le seul objectif -export let getObjectives = (root, parsedRules) => { +export let getObjectives = (situationGate, root, parsedRules) => { let formuleType = R.path(["formule", "explanation", "name"])( root ) @@ -55,37 +55,17 @@ export let getObjectives = (root, parsedRules) => { : formuleType ? [root] : null, names = targets ? R.reject(R.isNil)(targets) : [] - return R.map(R.curry(findRuleByDottedName)(parsedRules),names) + let findAndEvaluate = name => evaluateNode(situationGate,parsedRules,findRuleByDottedName(parsedRules,name)) + return R.map(findAndEvaluate,names) } -// FIXME - this relies on side-effects and the recursion is grossly indiscriminate -let collectNodeMissingVariables = (root, source=root, results=[]) => { - if (root == source) console.log("cNMV:",root) - if ( - source.nodeValue != null || - source.shortCircuit && source.shortCircuit(root) - ) { - // console.log('nodev or shortcircuit root, source', root, source) - return [] - } - - if (source['missingVariables']) { - // console.log('root, source', root, source) - results.push(source['missingVariables']) - } - - for (var prop in source) { - if (R.is(Object)(source[prop])) { - collectNodeMissingVariables(root, source[prop], results) - } - } - - return results +let collectNodeMissingVariables = (root) => { + return root.collectMissing ? root.collectMissing(root) : [] } -export let collectMissingVariables = (groupMethod='groupByMissingVariable') => ({root, parsedRules}) => { +export let collectMissingVariables = (groupMethod='groupByMissingVariable') => (situationGate, {root, parsedRules}) => { return R.pipe( - getObjectives, + R.curry(getObjectives)(situationGate), R.chain( v => R.pipe( collectNodeMissingVariables, @@ -101,9 +81,9 @@ export let collectMissingVariables = (groupMethod='groupByMissingVariable') => ( )(root, parsedRules) } -export let buildNextSteps = (allRules, analysedSituation) => { +export let buildNextSteps = (situationGate, flatRules, analysedSituation) => { let missingVariables = collectMissingVariables('groupByMissingVariable')( - analysedSituation + situationGate, analysedSituation ) /* @@ -142,11 +122,11 @@ export let buildNextSteps = (allRules, analysedSituation) => { return R.pipe( R.keys, - R.curry(findVariantsAndRecords)(allRules), + R.curry(findVariantsAndRecords)(flatRules), // on va maintenant construire la liste des composants React qui afficheront les questions à l'utilisateur pour que l'on obtienne les variables manquantes R.evolve({ - variantGroups: generateGridQuestions(allRules, missingVariables), - recordGroups: generateSimpleQuestions(allRules, missingVariables), + variantGroups: generateGridQuestions(analysedSituation.parsedRules, missingVariables), + recordGroups: generateSimpleQuestions(analysedSituation.parsedRules, missingVariables), }), R.values, R.unnest, diff --git a/source/engine/mecanisms.js b/source/engine/mecanisms.js index a4f333c81..2462add4c 100644 --- a/source/engine/mecanisms.js +++ b/source/engine/mecanisms.js @@ -2,7 +2,7 @@ import R from 'ramda' import React from 'react' import {anyNull, val} from './traverse-common-functions' import {Node, Leaf} from './traverse-common-jsx' -import {evaluateNode} from './traverse' +import {evaluateNode, collectNodeMissing} from './traverse' let transformPercentage = s => R.contains('%')(s) ? @@ -266,13 +266,18 @@ export let mecanismPercentage = (recurse,k,v) => { export let mecanismSum = (recurse,k,v) => { let evaluate = (situationGate, parsedRules, node) => { - let evaluateOne = child => evaluateNode(situationGate, parsedRules, child).nodeValue, - values = R.map(evaluateOne, node.explanation), + let evaluateOne = child => evaluateNode(situationGate, parsedRules, child), + explanation = R.map(evaluateOne, node.explanation), + values = R.pluck("nodeValue",explanation), nodeValue = R.any(R.equals(null),values) ? null : R.reduce(R.add,0,values) + let collectMissing = node => R.chain(collectNodeMissing,node.explanation) + return { ...node, nodeValue, + collectMissing, + explanation, jsx: { ...node.jsx, value: nodeValue diff --git a/source/engine/rules.js b/source/engine/rules.js index ddc212f71..836c0e84a 100644 --- a/source/engine/rules.js +++ b/source/engine/rules.js @@ -112,6 +112,7 @@ export let findVariantsAndRecords = (allRules, names) => { groupByType = R.groupBy(R.prop("type")), stripTypes = R.map(R.map(R.omit("type"))), mergeLists = R.map(R.reduce(R.mergeWith(R.concat),{})) + console.log("classify",classify(names)) return R.pipe(classify,groupByType,stripTypes,mergeLists)(names) } diff --git a/source/engine/traverse.js b/source/engine/traverse.js index 453bdcc91..996010b09 100644 --- a/source/engine/traverse.js +++ b/source/engine/traverse.js @@ -80,9 +80,12 @@ let createVariableNode = (rules, rule, situationGate) => (parseResult) => { explanation = parsedRule, missingVariables = variableIsCalculable ? [] : (nodeValue == null ? [dottedName] : []) + let collectMissing = node => node.missingVariables + return { ...node, nodeValue, + collectMissing, missingVariables, explanation, jsx: { @@ -171,16 +174,21 @@ let treat = (situationGate, rules, rule) => rawNode => { '+': 'add', '-': 'subtract' }[node.operator], - value1 = evaluateNode(situation,parsedRules,node.explanation[0]).nodeValue, - value2 = evaluateNode(situation,parsedRules,node.explanation[1]).nodeValue, + explanation = R.map(R.curry(evaluateNode)(situation,parsedRules),node.explanation), + value1 = explanation[0].nodeValue, + value2 = explanation[1].nodeValue, operatorFunction = R[operatorFunctionName], nodeValue = value1 == null || value2 == null ? null : operatorFunction(value1, value2) + let collectMissing = node => R.chain(collectNodeMissing,node.explanation) + return { ...node, nodeValue, + collectMissing, + explanation, jsx: { ...node.jsx, value: nodeValue @@ -237,15 +245,20 @@ let treat = (situationGate, rules, rule) => rawNode => { //TODO '=' }[node.operator], comparatorFunction = R[comparatorFunctionName], - value1 = evaluateNode(situation,parsedRules,node.explanation[0]).nodeValue, - value2 = evaluateNode(situation,parsedRules,node.explanation[1]).nodeValue, + explanation = R.map(R.curry(evaluateNode)(situation,parsedRules),node.explanation), + value1 = explanation[0].nodeValue, + value2 = explanation[1].nodeValue, nodeValue = value1 == null || value2 == null ? null : comparatorFunction(value1, value2) + let collectMissing = node => R.chain(collectNodeMissing,node.explanation) + return { ...node, nodeValue, + collectMissing, + explanation, jsx: { ...node.jsx, value: nodeValue @@ -365,12 +378,23 @@ export let computeRuleValue = (formuleValue, condValue) => export let treatRuleRoot = (situationGate, rules, rule) => { let evaluate = (situationGate, parsedRules, r) => { let - formuleValue = r.formule && evaluateNode(situationGate, parsedRules, r.formule).nodeValue, - condition = R.prop('non applicable si',r), - condValue = condition && evaluateNode(situationGate, parsedRules, condition).nodeValue, + evaluated = R.evolve({ + formule:R.curry(evaluateNode)(situationGate, parsedRules), + "non applicable si":R.curry(evaluateNode)(situationGate, parsedRules) + },r), + formuleValue = evaluated.formule && evaluated.formule.nodeValue, + condition = R.prop('non applicable si',evaluated), + condValue = condition && condition.nodeValue, nodeValue = computeRuleValue(formuleValue, condValue) - return {...r, nodeValue} + return {...evaluated, nodeValue} + } + let collectMissing = node => { + let cond = R.prop('non applicable si',node), + condMissing = cond ? collectNodeMissing(cond) : [], + formule = node.formule, + formMissing = formule ? collectNodeMissing(formule) : [] + return R.concat(condMissing,formMissing) } let parsedRoot = R.evolve({ // -> Voilà les attributs que peut comporter, pour l'instant, une Variable. @@ -380,10 +404,12 @@ export let treatRuleRoot = (situationGate, rules, rule) => { // 'cond' : Conditions d'applicabilité de la règle 'non applicable si': value => { let evaluate = (situationGate, parsedRules, node) => { - let nodeValue = evaluateNode(situationGate, parsedRules, node.explanation).nodeValue + let explanation = evaluateNode(situationGate, parsedRules, node.explanation), + nodeValue = explanation.nodeValue return { ...node, nodeValue, + explanation, jsx: { ...node.jsx, value: nodeValue @@ -393,8 +419,11 @@ export let treatRuleRoot = (situationGate, rules, rule) => { let child = treat(situationGate, rules, rule)(value) + let collectMissing = node => collectNodeMissing(node.explanation) + return { evaluate, + collectMissing, category: 'ruleProp', rulePropType: 'cond', name: 'non applicable si', @@ -418,10 +447,12 @@ export let treatRuleRoot = (situationGate, rules, rule) => { // [n'importe quel mécanisme numérique] : multiplication || barème en taux marginaux || le maximum de || le minimum de || ... 'formule': value => { let evaluate = (situationGate, parsedRules, node) => { - let nodeValue = evaluateNode(situationGate, parsedRules, node.explanation).nodeValue + let explanation = evaluateNode(situationGate, parsedRules, node.explanation), + nodeValue = explanation.nodeValue return { ...node, nodeValue, + explanation, jsx: { ...node.jsx, value: nodeValue @@ -430,14 +461,16 @@ export let treatRuleRoot = (situationGate, rules, rule) => { } let child = treat(situationGate, rules, rule)(value) + let collectMissing = node => collectNodeMissing(node.explanation) + return { evaluate, + collectMissing, category: 'ruleProp', rulePropType: 'formula', name: 'formule', type: 'numeric', explanation: child, - shortCircuit: R.pathEq(['non applicable si', 'nodeValue'], true), jsx: { return { ...parsedRoot, - evaluate + evaluate, + collectMissing } } -export let evaluateNode = (situationGate, parsedRules, node) => node.evaluate(situationGate, parsedRules, node) +export let evaluateNode = (situationGate, parsedRules, node) => + node.evaluate(situationGate, parsedRules, node) +export let collectNodeMissing = (node) => + node.collectMissing ? node.collectMissing(node) : [] /* Analyse the set of selected rules, and add derived information to them : - do they need variables that are not present in the user situation ? diff --git a/source/reducers.js b/source/reducers.js index 6aaf8cec6..a6eeee41a 100644 --- a/source/reducers.js +++ b/source/reducers.js @@ -36,14 +36,14 @@ export let reduceSteps = (state, action) => { return { ...returnObject, foldedSteps: state.foldedSteps || [], - unfoldedSteps: buildNextSteps(rules, returnObject.analysedSituation.root) + unfoldedSteps: buildNextSteps(situationGate(state), rules, returnObject.analysedSituation) } } if (action.type == STEP_ACTION && action.name == 'fold') { return { ...returnObject, foldedSteps: [...state.foldedSteps, R.head(state.unfoldedSteps)], - unfoldedSteps: buildNextSteps(rules, returnObject.analysedSituation.root) + unfoldedSteps: buildNextSteps(situationGate(state), rules, returnObject.analysedSituation) } } if (action.type == STEP_ACTION && action.name == 'unfold') { diff --git a/test/generateQuestions.test.js b/test/generateQuestions.test.js index 4ae919b5c..3adbcd191 100644 --- a/test/generateQuestions.test.js +++ b/test/generateQuestions.test.js @@ -4,7 +4,7 @@ import {rules, enrichRule} from '../source/engine/rules' import {analyseSituation, analyseTopDown} from '../source/engine/traverse' import {buildNextSteps, collectMissingVariables, getObjectives} from '../source/engine/generateQuestions' -let stateSelector = (state, name) => null +let stateSelector = (name) => null describe('getObjectives', function() { @@ -16,7 +16,7 @@ describe('getObjectives', function() { {nom: "ko", espace: "sum . evt"}], rules = rawRules.map(enrichRule), {root, parsedRules} = analyseTopDown(rules,"startHere")(stateSelector), - result = getObjectives(root, parsedRules) + result = getObjectives(stateSelector, root, parsedRules) expect(result).to.have.lengthOf(1) expect(result[0]).to.have.property('name','deux') @@ -34,7 +34,7 @@ describe('collectMissingVariables', function() { {nom: "ko", espace: "sum . evt"}], rules = rawRules.map(enrichRule), situation = analyseTopDown(rules,"startHere")(stateSelector), - result = collectMissingVariables()(situation) + result = collectMissingVariables()(stateSelector,situation) expect(result).to.have.property('sum . evt . ko') }); @@ -47,7 +47,7 @@ describe('collectMissingVariables', function() { {nom: "nyet", espace: "sum . evt"}], rules = rawRules.map(enrichRule), situation = analyseTopDown(rules,"startHere")(stateSelector), - result = collectMissingVariables()(situation) + result = collectMissingVariables()(stateSelector,situation) expect(result).to.have.property('sum . evt . nyet') expect(result).to.have.property('sum . evt . nope') @@ -64,8 +64,8 @@ describe('buildNextSteps', function() { {nom: "evt", espace: "top . sum", formule: {"une possibilité":["ko"]}, titre: "Truc", question:"?"}, {nom: "ko", espace: "top . sum . evt"}], rules = rawRules.map(enrichRule), - situation = analyseSituation(rules,"sum")(stateSelector), - result = buildNextSteps(rules, situation) + situation = analyseTopDown(rules,"sum")(stateSelector), + result = buildNextSteps(stateSelector, rules, situation) expect(result).to.have.lengthOf(1) expect(R.path(["question","props","label"])(result[0])).to.equal("?") diff --git a/test/rules.test.js b/test/rules.test.js index deb6a9187..72e1869e7 100644 --- a/test/rules.test.js +++ b/test/rules.test.js @@ -1,7 +1,7 @@ import R from 'ramda' import {expect} from 'chai' import {rules, enrichRule, findVariantsAndRecords} from '../source/engine/rules' -import {analyseSituation} from '../source/engine/traverse' +import {analyseSituation, analyseTopDown} from '../source/engine/traverse' let stateSelector = (state, name) => null @@ -32,7 +32,7 @@ describe('findVariantsAndRecords', function() { {nom: "dix", formule: "cinq", espace: "top"}, {nom: "cinq", espace: "top", question:"?"}], rules = rawRules.map(enrichRule), - situation = analyseSituation(rules,"startHere")(stateSelector), + situation = analyseTopDown(rules,"startHere")(stateSelector), result = findVariantsAndRecords(rules, ['top . cinq']) expect(result).to.have.deep.property('recordGroups', {top: ['top . cinq']}) @@ -45,7 +45,7 @@ describe('findVariantsAndRecords', function() { {nom: "evt", espace: "top . sum", formule: {"une possibilité":["ko"]}, titre: "Truc", question:"?"}, {nom: "ko", espace: "top . sum . evt"}], rules = rawRules.map(enrichRule), - situation = analyseSituation(rules,"sum")(stateSelector), + situation = analyseTopDown(rules,"sum")(stateSelector), result = findVariantsAndRecords(rules, ['top . sum . evt . ko']) expect(result).to.have.deep.property('variantGroups', {"top . sum . evt": ['top . sum . evt . ko']})