diff --git a/.vscode/settings.json b/.vscode/settings.json index 71e6bdb8c..5850abf04 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -4,5 +4,8 @@ "spellright.documentTypes": ["yaml", "git-commit", "markdown"], "typescript.tsdk": "node_modules/typescript/lib", "editor.tabSize": 2, - "eslint.enable": true + "eslint.enable": true, + "editor.codeActionsOnSave": { + "source.organizeImports": true + } } diff --git a/publicodes/source/evaluateReference.ts b/publicodes/source/evaluateReference.ts new file mode 100644 index 000000000..a769cb821 --- /dev/null +++ b/publicodes/source/evaluateReference.ts @@ -0,0 +1,269 @@ +import { EvaluatedNode, ParsedRule } from '.' +import { typeWarning } from './error' +import { evaluateApplicability } from './evaluateRule' +import { mergeMissing, evaluateNode } from './evaluation' +import { convertNodeToUnit } from './nodeUnits' +import { serializeUnit, areUnitConvertible } from './units' + +export const evaluateReference = (cache, situation, rules, node) => { + const rule = rules[node.dottedName] + // When a rule exists in different version (created using the `replace` mecanism), we add + // a redirection in the evaluation of references to use a potential active replacement + const [ + applicableReplacements, + replacementMissingVariableList + ] = getApplicableReplacements( + node.explanation?.contextRuleName ?? '', + cache, + situation, + rules, + rule + ) + + if (applicableReplacements.length) { + if (applicableReplacements.length > 1) { + // eslint-disable-next-line no-console + console.warn(` +Règle ${rule.dottedName}: plusieurs remplacements valides ont été trouvés : +\n\t${applicableReplacements.map(node => node.rawNode).join('\n\t')} + +Par défaut, seul le premier s'applique. Si vous voulez un autre comportement, vous pouvez : + - Restreindre son applicabilité via "applicable si" sur la règle de définition + - Restreindre sa portée en ajoutant une liste blanche (via le mot clé "dans") ou une liste noire (via le mot clé "sauf dans") +`) + } + return applicableReplacements[0] + } + const addReplacementMissingVariable = node => ({ + ...node, + missingVariables: replacementMissingVariableList.reduce( + mergeMissing, + node.missingVariables + ) + }) + const dottedName = node.dottedName + // On va vérifier dans le cache courant, dict, si la variable n'a pas été déjà évaluée + // En effet, l'évaluation dans le cas d'une variable qui a une formule, est coûteuse ! + const cacheName = + dottedName + (node.explanation.filter ? ' .' + node.explanation.filter : '') + const cached = cache[cacheName] + + if (cached) return addReplacementMissingVariable(cached) + + const cacheNode = ( + nodeValue: EvaluatedNode['nodeValue'], + missingVariables: EvaluatedNode['missingVariables'], + explanation?: Record + ) => { + cache[cacheName] = { + ...node, + nodeValue, + ...(explanation && { + explanation + }), + ...(explanation?.temporalValue && { + temporalValue: explanation.temporalValue + }), + ...(explanation?.unit && { unit: explanation.unit }), + missingVariables + } + return addReplacementMissingVariable(cache[cacheName]) + } + const applicabilityEvaluation = evaluateApplicability( + cache, + situation, + rules, + rule + ) + if (!applicabilityEvaluation.nodeValue) { + return cacheNode( + applicabilityEvaluation.nodeValue, + applicabilityEvaluation.missingVariables, + applicabilityEvaluation + ) + } + if (situation[dottedName]) { + // Conditional evaluation is required because some mecanisms like + // "synchronisation" store raw JS objects in the situation. + const situationValue = situation[dottedName]?.evaluate + ? evaluateNode(cache, situation, rules, situation[dottedName]) + : situation[dottedName] + const unit = + !situationValue.unit || serializeUnit(situationValue.unit) === '' + ? rule.unit + : situationValue.unit + return cacheNode( + situationValue?.nodeValue !== undefined + ? situationValue.nodeValue + : situationValue, + applicabilityEvaluation.missingVariables, + { + ...rule, + ...(situationValue?.nodeValue !== undefined && situationValue), + unit + } + ) + } + + if (rule.defaultValue != null) { + const evaluation = evaluateNode(cache, situation, rules, rule.defaultValue) + return cacheNode(evaluation.nodeValue ?? evaluation, { + ...evaluation.missingVariables, + [dottedName]: 1 + }) + } + + if (rule.formule != null) { + const evaluation = evaluateNode(cache, situation, rules, rule) + return cacheNode( + evaluation.nodeValue, + evaluation.missingVariables, + evaluation + ) + } + + return cacheNode(null, { [dottedName]: 2 }) +} + +// This function is a wrapper that can apply : +// - unit transformations to the value of the variable. +// See the unité-temporelle.yaml test suite for details +// - filters on the variable to select one part of the variable's 'composantes' + +export const evaluateReferenceTransforms = ( + cache, + situation, + parsedRules, + node +) => { + // Filter transformation + const filteringSituation = { + ...situation, + '_meta.filter': node.explanation.filter + } + const filteredNode = evaluateNode( + cache, + node.explanation.filter ? filteringSituation : situation, + parsedRules, + node.explanation.originalNode + ) + const { explanation, nodeValue } = filteredNode + if (!explanation || nodeValue === null) { + return filteredNode + } + const unit = node.explanation.unit + if (unit) { + try { + return convertNodeToUnit(unit, filteredNode) + } catch (e) { + typeWarning( + cache._meta.contextRule, + `Impossible de convertir la reference '${filteredNode.name}'`, + e + ) + } + } + + return filteredNode +} + +/** + * Statically filter out replacements from `replaceBy`. + * Note: whitelist and blacklist filtering are applicable to the replacement + * itself or any parent namespace. + */ +export const getApplicableReplacedBy = (contextRuleName, replacedBy) => + replacedBy + .sort( + (replacement1, replacement2) => + +!!replacement2.whiteListedNames - +!!replacement1.whiteListedNames + ) + .filter( + ({ whiteListedNames }) => + !whiteListedNames || + whiteListedNames.some(name => contextRuleName.startsWith(name)) + ) + .filter( + ({ blackListedNames }) => + !blackListedNames || + blackListedNames.every(name => !contextRuleName.startsWith(name)) + ) + .filter(({ referenceNode }) => contextRuleName !== referenceNode.dottedName) + +/** + * Filter-out and apply all possible replacements at runtime. + */ +const getApplicableReplacements = ( + contextRuleName, + cache, + situation, + rules, + rule: ParsedRule +) => { + let missingVariableList: Array = [] + if (contextRuleName.startsWith('[evaluation]')) { + return [[], []] + } + const applicableReplacements = getApplicableReplacedBy( + contextRuleName, + rule.replacedBy + ) + // Remove remplacement defined in a not applicable node + .filter(({ referenceNode }) => { + const referenceRule = rules[referenceNode.dottedName] + const { + nodeValue: isApplicable, + missingVariables + } = evaluateApplicability(cache, situation, rules, referenceRule) + missingVariableList.push(missingVariables) + return isApplicable + }) + // Remove remplacement defined in a node whose situation value is false + .filter(({ referenceNode }) => { + const referenceRule = rules[referenceNode.dottedName] + const situationValue = situation[referenceRule.dottedName] + if (referenceNode.question && situationValue == null) { + missingVariableList.push({ [referenceNode.dottedName]: 1 }) + } + return situationValue?.nodeValue !== false + }) + // Remove remplacement defined in a boolean node whose evaluated value is false + .filter(({ referenceNode }) => { + const referenceRule = rules[referenceNode.dottedName] + if (referenceRule.formule?.explanation?.operationType !== 'comparison') { + return true + } + const { nodeValue: isApplicable, missingVariables } = evaluateNode( + cache, + situation, + rules, + referenceRule + ) + missingVariableList.push(missingVariables) + return isApplicable + }) + .map(({ referenceNode, replacementNode }) => + replacementNode != null + ? evaluateNode(cache, situation, rules, replacementNode) + : evaluateReference(cache, situation, rules, referenceNode) + ) + .map(replacementNode => { + const replacedRuleUnit = rule.unit + if (!areUnitConvertible(replacementNode.unit, replacedRuleUnit)) { + typeWarning( + contextRuleName, + `L'unité de la règle de remplacement n'est pas compatible avec celle de la règle remplacée ${rule.dottedName}` + ) + } + return { + ...replacementNode, + unit: replacementNode.unit || replacedRuleUnit + } + }) + + missingVariableList = missingVariableList.filter( + missingVariables => !!Object.keys(missingVariables).length + ) + + return [applicableReplacements, missingVariableList] +} diff --git a/publicodes/source/evaluateRule.ts b/publicodes/source/evaluateRule.ts index 888d2f2ec..c680c1348 100644 --- a/publicodes/source/evaluateRule.ts +++ b/publicodes/source/evaluateRule.ts @@ -69,7 +69,26 @@ export const evaluateApplicability = ( } } -export default (cache, situation, parsedRules, node) => { +export const evaluateFormula = (cache, situation, parsedRules, node) => { + const explanation = evaluateNode( + cache, + situation, + parsedRules, + node.explanation + ), + { nodeValue, unit, missingVariables, temporalValue } = explanation + + return { + ...node, + nodeValue, + unit, + missingVariables, + explanation, + temporalValue + } +} + +export const evaluateRule = (cache, situation, parsedRules, node) => { cache._meta.contextRule.push(node.dottedName) const applicabilityEvaluation = evaluateApplicability( cache, @@ -103,7 +122,6 @@ export default (cache, situation, parsedRules, node) => { bonus(condMissing, !!Object.keys(condMissing).length), evaluatedFormula.missingVariables ) - // console.log(node.dottedName, evaluatedFormula.unit) const temporalValue = evaluatedFormula.temporalValue cache._meta.contextRule.pop() @@ -118,3 +136,32 @@ export default (cache, situation, parsedRules, node) => { missingVariables } } + +export const evaluateDisabledBy = (cache, situation, parsedRules, node) => { + const isDisabledBy = node.explanation.isDisabledBy.map(disablerNode => + evaluateNode(cache, situation, parsedRules, disablerNode) + ) + const nodeValue = isDisabledBy.some( + x => x.nodeValue !== false && x.nodeValue !== null + ) + const explanation = { ...node.explanation, isDisabledBy } + return { + ...node, + explanation, + nodeValue, + missingVariables: mergeAllMissing(isDisabledBy) + } +} + +export const evaluateCondition = (cache, situation, parsedRules, node) => { + const explanation = evaluateNode( + cache, + situation, + parsedRules, + node.explanation + ) + const nodeValue = explanation.nodeValue + const missingVariables = explanation.missingVariables + + return { ...node, nodeValue, explanation, missingVariables } +} diff --git a/publicodes/source/evaluation.tsx b/publicodes/source/evaluation.tsx index b4cb8d5d2..17d426a98 100644 --- a/publicodes/source/evaluation.tsx +++ b/publicodes/source/evaluation.tsx @@ -1,16 +1,16 @@ -import { - add, - evolve, - filter, - fromPairs, - keys, - map, - mergeWith, - reduce, - dissoc -} from 'ramda' +import { add, evolve, fromPairs, keys, map, mergeWith, reduce } from 'ramda' import React from 'react' import { typeWarning } from './error' +import { + evaluateReference, + evaluateReferenceTransforms +} from './evaluateReference' +import { + evaluateCondition, + evaluateDisabledBy, + evaluateFormula, + evaluateRule +} from './evaluateRule' import { convertNodeToUnit, simplifyNodeUnit } from './nodeUnits' import { concatTemporals, @@ -21,7 +21,7 @@ import { temporalAverage, zipTemporals } from './temporal' -import { EvaluatedNode, ParsedRule, ParsedRules } from './types' +import { EvaluatedNode } from './types' export const makeJsx = (node: EvaluatedNode): JSX.Element => { const Component = node.jsx @@ -38,7 +38,12 @@ export const mergeMissing = (left, right) => mergeWith(add, left || {}, right || {}) export const evaluateNode = (cache, situation, parsedRules, node) => { - return node.evaluate(cache, situation, parsedRules, node) + if (!node.nodeKind) { + throw Error('A node to evaluate must have a "nodeKind" attribute') + } else if (!evaluationFunctions[node.nodeKind]) { + throw Error(`Unknown "nodeKind": ${node.nodeKind}`) + } + return evaluationFunctions[node.nodeKind](cache, situation, parsedRules, node) } function convertNodesToSameUnit(nodes, contextRule, mecanismName) { @@ -106,34 +111,19 @@ export const evaluateArray = (reducer, start) => ( } } -export const evaluateArrayWithFilter = (evaluationFilter, reducer, start) => ( - cache, - situation, - parsedRules, - node -) => { - return evaluateArray(reducer, start)( - cache, - dissoc('_meta.filter', situation), - parsedRules, - { - ...node, - explanation: filter(evaluationFilter(situation), node.explanation) - } - ) -} +export const defaultNode = (nodeValue: EvaluatedNode['nodeValue']) => ({ + nodeValue, + // eslint-disable-next-line + jsx: ({ nodeValue }: EvaluatedNode) => ( + {nodeValue} + ), + isDefault: true, + nodeKind: 'defaultNode' +}) -export const defaultNode = (nodeValue: EvaluatedNode['nodeValue']) => { - const defaultNode = { - nodeValue, - // eslint-disable-next-line - jsx: ({ nodeValue }: EvaluatedNode) => ( - {nodeValue} - ), - isDefault: true - } - return { ...defaultNode, evaluate: () => defaultNode } -} +const evaluateDefaultNode = (cache, situation, parsedRules, node) => node +const evaluateExplanationNode = (cache, situation, parsedRules, node) => + evaluateNode(cache, situation, parsedRules, node.explanation) export const parseObject = (recurse, objectShape, value) => { const recurseOne = key => defaultValue => { @@ -212,3 +202,24 @@ export const evaluateObject = (objectShape, effect) => ( temporalExplanation } } + +const evaluationFunctions = { + rule: evaluateRule, + formula: evaluateFormula, + disabledBy: evaluateDisabledBy, + condition: evaluateCondition, + reference: evaluateReference, + referenceWithTransforms: evaluateReferenceTransforms, + parentDependencies: evaluateExplanationNode, + constant: evaluateDefaultNode, + defaultNode: evaluateDefaultNode +} + +export function registerEvaluationFunction(nodeKind, evaluationFunction) { + if (evaluationFunctions[nodeKind]) { + throw Error( + `Multiple evaluation functions registered for the nodeKind \x1b[4m${nodeKind}` + ) + } + evaluationFunctions[nodeKind] = evaluationFunction +} diff --git a/publicodes/source/mecanisms/applicable.tsx b/publicodes/source/mecanisms/applicable.tsx index fd43597e9..d93715620 100644 --- a/publicodes/source/mecanisms/applicable.tsx +++ b/publicodes/source/mecanisms/applicable.tsx @@ -1,6 +1,12 @@ import React from 'react' import { InfixMecanism } from '../components/mecanisms/common' -import { bonus, evaluateNode, makeJsx, mergeMissing } from '../evaluation' +import { + bonus, + evaluateNode, + makeJsx, + mergeMissing, + registerEvaluationFunction +} from '../evaluation' function MecanismApplicable({ explanation }) { return ( @@ -46,13 +52,16 @@ export default function Applicable(recurse, v) { condition: recurse(v['applicable si']) } return { - evaluate, + // evaluate, jsx: MecanismApplicable, explanation, category: 'mecanism', - name: Applicable.name, + name: Applicable.nom, + nodeKind: Applicable.nom, unit: explanation.valeur.unit } } Applicable.nom = 'applicable si' + +registerEvaluationFunction(Applicable.nom, evaluate) diff --git a/publicodes/source/mecanisms/arrondi.tsx b/publicodes/source/mecanisms/arrondi.tsx index d57d7aaca..5aa816b59 100644 --- a/publicodes/source/mecanisms/arrondi.tsx +++ b/publicodes/source/mecanisms/arrondi.tsx @@ -1,6 +1,11 @@ import React from 'react' import { InfixMecanism } from '../components/mecanisms/common' -import { evaluateNode, makeJsx, mergeAllMissing } from '../evaluation' +import { + evaluateNode, + makeJsx, + mergeAllMissing, + registerEvaluationFunction +} from '../evaluation' import { EvaluatedNode } from '../types' export type ArrondiExplanation = { @@ -61,14 +66,16 @@ export default function Arrondi(recurse, v) { arrondi: recurse(v.arrondi) } return { - evaluate, jsx: MecanismArrondi, explanation, category: 'mecanism', name: 'arrondi', + nodeKind: Arrondi.nom, type: 'numeric', unit: explanation.valeur.unit } } Arrondi.nom = 'arrondi' + +registerEvaluationFunction(Arrondi.nom, evaluate) diff --git a/publicodes/source/mecanisms/barème.ts b/publicodes/source/mecanisms/barème.ts index e3d76a59d..516f2f396 100644 --- a/publicodes/source/mecanisms/barème.ts +++ b/publicodes/source/mecanisms/barème.ts @@ -1,6 +1,11 @@ import Barème from '../components/mecanisms/Barème' import { evaluationError } from '../error' -import { defaultNode, evaluateNode, mergeAllMissing } from '../evaluation' +import { + defaultNode, + evaluateNode, + mergeAllMissing, + registerEvaluationFunction +} from '../evaluation' import { liftTemporal2, liftTemporalNode, @@ -22,10 +27,10 @@ export default function parse(parse, v) { } return { explanation, - evaluate, jsx: Barème, category: 'mecanism', name: 'barème', + nodeKind: 'barème', type: 'numeric', unit: explanation.assiette.unit } @@ -126,3 +131,5 @@ const evaluate = ( unit: assiette.unit } } + +registerEvaluationFunction('barème', evaluate) diff --git a/publicodes/source/mecanisms/composantes.ts b/publicodes/source/mecanisms/composantes.ts new file mode 100644 index 000000000..499fe1f75 --- /dev/null +++ b/publicodes/source/mecanisms/composantes.ts @@ -0,0 +1,54 @@ +import { add, dissoc, filter, objOf } from 'ramda' +import { evaluateArray, registerEvaluationFunction } from '../evaluation' +import { inferUnit } from '../units' +import Composantes from '../components/mecanisms/Composantes' + +export const evaluateComposantes = (cache, situation, parsedRules, node) => { + const evaluationFilter = c => + !situation['_meta.filter'] || + !c.composante || + ((!c.composante['dû par'] || + !['employeur', 'salarié'].includes(situation['_meta.filter']) || + c.composante['dû par'] == situation['_meta.filter']) && + (!c.composante['impôt sur le revenu'] || + !['déductible', 'non déductible'].includes(situation['_meta.filter']) || + c.composante['impôt sur le revenu'] == situation['_meta.filter'])) + + return evaluateArray(add, 0)( + cache, + dissoc('_meta.filter', situation), + parsedRules, + { + ...node, + explanation: filter(evaluationFilter, node.explanation) + } + ) +} + +export const decompose = (recurse, k, v) => { + const subProps = dissoc>('composantes', v) + const explanation = v.composantes.map(c => ({ + ...recurse( + objOf(k, { + ...subProps, + ...dissoc>('attributs', c) + }) + ), + composante: c.nom ? { nom: c.nom } : c.attributs + })) + + return { + explanation, + jsx: Composantes, + nodeKind: 'composantes', + category: 'mecanism', + name: 'composantes', + type: 'numeric', + unit: inferUnit( + '+', + explanation.map(e => e.unit) + ) + } +} + +registerEvaluationFunction('composantes', evaluateComposantes) diff --git a/publicodes/source/mecanisms/condition-allof.tsx b/publicodes/source/mecanisms/condition-allof.tsx index 15c37ad22..f5330fd0c 100644 --- a/publicodes/source/mecanisms/condition-allof.tsx +++ b/publicodes/source/mecanisms/condition-allof.tsx @@ -1,7 +1,12 @@ -import { any, equals, is, map, pluck } from 'ramda' +import { is, map } from 'ramda' import React from 'react' import { Mecanism } from '../components/mecanisms/common' -import { evaluateNode, makeJsx, mergeAllMissing } from '../evaluation' +import { + evaluateNode, + makeJsx, + mergeAllMissing, + registerEvaluationFunction +} from '../evaluation' const evaluate = (cache, situation, parsedRules, node) => { const [nodeValue, explanation] = node.explanation.reduce( @@ -42,11 +47,13 @@ export const mecanismAllOf = (recurse, v) => { ) return { - evaluate: evaluate, jsx, explanation, category: 'mecanism', name: 'toutes ces conditions', + nodeKind: 'toutes ces conditions', type: 'boolean' } } + +registerEvaluationFunction('toutes ces conditions', evaluate) diff --git a/publicodes/source/mecanisms/condition-oneof.tsx b/publicodes/source/mecanisms/condition-oneof.tsx index 6b70d99ec..e35dc744a 100644 --- a/publicodes/source/mecanisms/condition-oneof.tsx +++ b/publicodes/source/mecanisms/condition-oneof.tsx @@ -1,7 +1,12 @@ -import { any, equals, is, map, max, mergeWith, pluck, reduce } from 'ramda' +import { is, map, max, mergeWith, reduce } from 'ramda' import React from 'react' import { Mecanism } from '../components/mecanisms/common' -import { collectNodeMissing, evaluateNode, makeJsx } from '../evaluation' +import { + collectNodeMissing, + evaluateNode, + makeJsx, + registerEvaluationFunction +} from '../evaluation' const evaluate = (cache, situation, parsedRules, node) => { const evaluateOne = child => @@ -40,11 +45,13 @@ export const mecanismOneOf = (recurse, v) => { ) return { - evaluate, jsx, explanation, category: 'mecanism', name: 'une de ces conditions', + nodeKind: 'une de ces conditions', type: 'boolean' } } + +registerEvaluationFunction('une de ces conditions', evaluate) diff --git a/publicodes/source/mecanisms/durée.tsx b/publicodes/source/mecanisms/durée.tsx index 40074523f..10c2df9e9 100644 --- a/publicodes/source/mecanisms/durée.tsx +++ b/publicodes/source/mecanisms/durée.tsx @@ -1,14 +1,15 @@ +import React from 'react' +import { Mecanism } from '../components/mecanisms/common' import { convertToDate, convertToString } from '../date' import { defaultNode, evaluateNode, makeJsx, mergeAllMissing, - parseObject + parseObject, + registerEvaluationFunction } from '../evaluation' -import { Mecanism } from '../components/mecanisms/common' import { parseUnit } from '../units' -import React from 'react' function MecanismDurée({ nodeValue, explanation, unit }) { return ( @@ -70,12 +71,14 @@ export default (recurse, v) => { const explanation = parseObject(recurse, objectShape, v) return { - evaluate, jsx: MecanismDurée, explanation, category: 'mecanism', name: 'Durée', + nodeKind: 'durée', type: 'numeric', unit: parseUnit('jours') } } + +registerEvaluationFunction('durée', evaluate) diff --git a/publicodes/source/mecanisms/grille.ts b/publicodes/source/mecanisms/grille.ts index 3faa115cd..e17f012de 100644 --- a/publicodes/source/mecanisms/grille.ts +++ b/publicodes/source/mecanisms/grille.ts @@ -1,6 +1,11 @@ import { lensPath, over } from 'ramda' import grille from '../components/mecanisms/Grille' -import { defaultNode, evaluateNode, mergeAllMissing } from '../evaluation' +import { + defaultNode, + evaluateNode, + mergeAllMissing, + registerEvaluationFunction +} from '../evaluation' import { liftTemporal2, liftTemporalNode, @@ -24,10 +29,10 @@ export default function parse(parse, v) { } return { explanation, - evaluate, jsx: grille, category: 'mecanism', name: 'grille', + nodeKind: 'grille', type: 'numeric', unit: explanation.tranches[0].montant.unit } @@ -109,3 +114,5 @@ const evaluate = ( unit: activeTranches[0].value[0]?.unit ?? node.unit } } + +registerEvaluationFunction('grille', evaluate) diff --git a/publicodes/source/mecanisms/inversion.ts b/publicodes/source/mecanisms/inversion.ts index 5998ad0da..199be857e 100644 --- a/publicodes/source/mecanisms/inversion.ts +++ b/publicodes/source/mecanisms/inversion.ts @@ -1,7 +1,7 @@ -import { evaluateNode } from '../evaluation' +import InversionNumérique from '../components/mecanisms/InversionNumérique' +import { evaluateNode, registerEvaluationFunction } from '../evaluation' import { convertNodeToUnit } from '../nodeUnits' import uniroot from '../uniroot' -import InversionNumérique from '../components/mecanisms/InversionNumérique' import { parseUnit } from '../units' export const evaluateInversion = (oldCache, situation, parsedRules, node) => { @@ -94,7 +94,6 @@ export const mecanismInversion = dottedName => (recurse, v) => { ) } return { - evaluate: evaluateInversion, unit: v.unité && parseUnit(v.unité), explanation: { ruleToInverse: dottedName, @@ -104,6 +103,9 @@ export const mecanismInversion = dottedName => (recurse, v) => { jsx: InversionNumérique, category: 'mecanism', name: 'inversion numérique', + nodeKind: 'inversion numérique', type: 'numeric' } } + +registerEvaluationFunction('inversion numérique', evaluateInversion) diff --git a/publicodes/source/mecanisms/max.tsx b/publicodes/source/mecanisms/max.tsx index 851a5125b..1e8a18f06 100644 --- a/publicodes/source/mecanisms/max.tsx +++ b/publicodes/source/mecanisms/max.tsx @@ -1,24 +1,14 @@ import React from 'react' import { Mecanism } from '../components/mecanisms/common' -import { evaluateArray, makeJsx } from '../evaluation' +import { + evaluateArray, + makeJsx, + registerEvaluationFunction +} from '../evaluation' export const mecanismMax = (recurse, v) => { const explanation = v.map(recurse) - const max = (a, b) => { - if (a === false) { - return b - } - if (b === false) { - return a - } - if (a === null || b === null) { - return null - } - return Math.max(a, b) - } - const evaluate = evaluateArray(max, false) - const jsx = ({ nodeValue, explanation, unit }) => (
    @@ -33,12 +23,28 @@ export const mecanismMax = (recurse, v) => { ) return { - evaluate, jsx, explanation, type: 'numeric', category: 'mecanism', name: 'le maximum de', + nodeKind: 'maximum', unit: explanation[0].unit } } + +const max = (a, b) => { + if (a === false) { + return b + } + if (b === false) { + return a + } + if (a === null || b === null) { + return null + } + return Math.max(a, b) +} +const evaluate = evaluateArray(max, false) + +registerEvaluationFunction('maximum', evaluate) diff --git a/publicodes/source/mecanisms/min.tsx b/publicodes/source/mecanisms/min.tsx index fdd985ec4..98fbc5b16 100644 --- a/publicodes/source/mecanisms/min.tsx +++ b/publicodes/source/mecanisms/min.tsx @@ -1,11 +1,14 @@ import { min } from 'ramda' import React from 'react' import { Mecanism } from '../components/mecanisms/common' -import { evaluateArray, makeJsx } from '../evaluation' +import { + evaluateArray, + makeJsx, + registerEvaluationFunction +} from '../evaluation' export const mecanismMin = (recurse, v) => { const explanation = v.map(recurse) - const evaluate = evaluateArray(min, Infinity) const jsx = ({ nodeValue, explanation, unit }) => (
      @@ -19,12 +22,16 @@ export const mecanismMin = (recurse, v) => { ) return { - evaluate, jsx, explanation, type: 'numeric', category: 'mecanism', name: 'le minimum de', + nodeKind: 'minimum', unit: explanation[0].unit } } + +const evaluate = evaluateArray(min, Infinity) + +registerEvaluationFunction('minimum', evaluate) diff --git a/publicodes/source/mecanisms/nonApplicable.tsx b/publicodes/source/mecanisms/nonApplicable.tsx index a5128e1c6..aae5472c4 100644 --- a/publicodes/source/mecanisms/nonApplicable.tsx +++ b/publicodes/source/mecanisms/nonApplicable.tsx @@ -4,8 +4,8 @@ import { bonus, evaluateNode, makeJsx, - mergeAllMissing, - mergeMissing + mergeMissing, + registerEvaluationFunction } from '../evaluation' function MecanismNonApplicable({ explanation }) { @@ -54,13 +54,15 @@ export default function NonApplicable(recurse, v) { condition: recurse(v['non applicable si']) } return { - evaluate, jsx: MecanismNonApplicable, explanation, category: 'mecanism', name: 'non applicable', + nodeKind: 'non applicable', unit: explanation.valeur.unit } } NonApplicable.nom = 'non applicable si' + +registerEvaluationFunction('non applicable', evaluate) diff --git a/publicodes/source/mecanisms/one-possibility.tsx b/publicodes/source/mecanisms/one-possibility.tsx index 2e00a1816..65e4e04b7 100644 --- a/publicodes/source/mecanisms/one-possibility.tsx +++ b/publicodes/source/mecanisms/one-possibility.tsx @@ -1,9 +1,17 @@ +import { registerEvaluationFunction } from '../evaluation' + // TODO : This isn't a real mecanism, cf. #963 export const mecanismOnePossibility = dottedName => (recurse, v) => ({ ...v, 'une possibilité': 'oui', - evaluate: (cache, situation, parsedRules, node) => ({ - ...node, - missingVariables: { [dottedName]: 1 } - }) + context: dottedName, + nodeKind: 'une possibilité' }) + +registerEvaluationFunction( + 'une possibilité', + (cache, situation, parsedRules, node) => ({ + ...node, + missingVariables: { [node.context]: 1 } + }) +) diff --git a/publicodes/source/mecanisms/operation.tsx b/publicodes/source/mecanisms/operation.tsx index c7b7a453a..2000888dc 100644 --- a/publicodes/source/mecanisms/operation.tsx +++ b/publicodes/source/mecanisms/operation.tsx @@ -1,86 +1,31 @@ -import { map } from 'ramda' +import { + add, + divide, + equals, + fromPairs, + gt, + gte, + lt, + lte, + map, + multiply, + subtract +} from 'ramda' import React from 'react' import { Operation } from '../components/mecanisms/common' import { convertToDate } from '../date' import { typeWarning } from '../error' -import { evaluateNode, makeJsx, mergeAllMissing } from '../evaluation' +import { + evaluateNode, + makeJsx, + mergeAllMissing, + registerEvaluationFunction +} from '../evaluation' import { convertNodeToUnit } from '../nodeUnits' import { liftTemporal2, pureTemporal, temporalAverage } from '../temporal' import { inferUnit, serializeUnit } from '../units' -export default (k, operatorFunction, symbol) => (recurse, v) => { - const evaluate = (cache, situation, parsedRules, node) => { - const explanation = map( - node => evaluateNode(cache, situation, parsedRules, node), - node.explanation - ) - let [node1, node2] = explanation - const missingVariables = mergeAllMissing([node1, node2]) - - if (node1.nodeValue == null || node2.nodeValue == null) { - return { ...node, nodeValue: null, explanation, missingVariables } - } - if (!['∕', '×'].includes(node.operator)) { - try { - if (node1.unit) { - node2 = convertNodeToUnit(node1.unit, node2) - } else if (node2.unit) { - node1 = convertNodeToUnit(node2.unit, node1) - } - } catch (e) { - typeWarning( - cache._meta.contextRule, - `Dans l'expression '${ - node.operator - }', la partie gauche (unité: ${serializeUnit( - node1.unit - )}) n'est pas compatible avec la partie droite (unité: ${serializeUnit( - node2.unit - )})`, - e - ) - } - } - const baseNode = { - ...node, - explanation, - unit: inferUnit(k, [node1.unit, node2.unit]), - missingVariables - } - - const temporalValue = liftTemporal2( - (a: string | false, b: string | false) => { - if (!['≠', '='].includes(node.operator) && a === false && b === false) { - return false - } - if ( - ['<', '>', '≤', '≥', '∕', '×'].includes(node.operator) && - (a === false || b === false) - ) { - return false - } - if ( - a !== false && - b !== false && - ['≠', '=', '<', '>', '≤', '≥'].includes(node.operator) && - [a, b].every(value => value.match?.(/[\d]{2}\/[\d]{2}\/[\d]{4}/)) - ) { - return operatorFunction(convertToDate(a), convertToDate(b)) - } - return operatorFunction(a, b) - }, - node1.temporalValue ?? pureTemporal(node1.nodeValue), - node2.temporalValue ?? pureTemporal(node2.nodeValue) - ) - const nodeValue = temporalAverage(temporalValue, baseNode.unit) - - return { - ...baseNode, - nodeValue, - ...(temporalValue.length > 1 && { temporalValue }) - } - } - +const parse = (k, symbol) => (recurse, v) => { const explanation = v.explanation.map(recurse) const [node1, node2] = explanation const unit = inferUnit(k, [node1.unit, node2.unit]) @@ -96,10 +41,109 @@ export default (k, operatorFunction, symbol) => (recurse, v) => { return { ...v, - evaluate, jsx, + nodeKind: 'operation', + operationKind: k, operator: symbol || k, explanation, unit } } + +const evaluate = (cache, situation, parsedRules, node) => { + const explanation = map( + node => evaluateNode(cache, situation, parsedRules, node), + node.explanation + ) + let [node1, node2] = explanation + const missingVariables = mergeAllMissing([node1, node2]) + + if (node1.nodeValue == null || node2.nodeValue == null) { + return { ...node, nodeValue: null, explanation, missingVariables } + } + if (!['∕', '×'].includes(node.operator)) { + try { + if (node1.unit) { + node2 = convertNodeToUnit(node1.unit, node2) + } else if (node2.unit) { + node1 = convertNodeToUnit(node2.unit, node1) + } + } catch (e) { + typeWarning( + cache._meta.contextRule, + `Dans l'expression '${ + node.operator + }', la partie gauche (unité: ${serializeUnit( + node1.unit + )}) n'est pas compatible avec la partie droite (unité: ${serializeUnit( + node2.unit + )})`, + e + ) + } + } + const baseNode = { + ...node, + explanation, + unit: inferUnit(node.operationKind, [node1.unit, node2.unit]), + missingVariables + } + + const operatorFunction = knownOperations[node.operationKind][0] + + const temporalValue = liftTemporal2( + (a: string | false, b: string | false) => { + if (!['≠', '='].includes(node.operator) && a === false && b === false) { + return false + } + if ( + ['<', '>', '≤', '≥', '∕', '×'].includes(node.operator) && + (a === false || b === false) + ) { + return false + } + if ( + a !== false && + b !== false && + ['≠', '=', '<', '>', '≤', '≥'].includes(node.operator) && + [a, b].every(value => value.match?.(/[\d]{2}\/[\d]{2}\/[\d]{4}/)) + ) { + return operatorFunction(convertToDate(a), convertToDate(b)) + } + return operatorFunction(a, b) + }, + node1.temporalValue ?? pureTemporal(node1.nodeValue), + node2.temporalValue ?? pureTemporal(node2.nodeValue) + ) + const nodeValue = temporalAverage(temporalValue, baseNode.unit) + + return { + ...baseNode, + nodeValue, + ...(temporalValue.length > 1 && { temporalValue }) + } +} + +registerEvaluationFunction('operation', evaluate) + +const knownOperations = { + '*': [multiply, '×'], + '/': [divide, '∕'], + '+': [add], + '-': [subtract, '−'], + '<': [lt], + '<=': [lte, '≤'], + '>': [gt], + '>=': [gte, '≥'], + '=': [equals], + '!=': [(a, b) => !equals(a, b), '≠'] +} + +const operationDispatch = fromPairs( + Object.entries(knownOperations).map(([k, [f, symbol]]) => [ + k, + parse(k, symbol) + ]) +) + +export default operationDispatch diff --git a/publicodes/source/mecanisms/plafond.tsx b/publicodes/source/mecanisms/plafond.tsx index 46dda8c5c..6ec6e9a63 100644 --- a/publicodes/source/mecanisms/plafond.tsx +++ b/publicodes/source/mecanisms/plafond.tsx @@ -1,7 +1,12 @@ import React from 'react' import { InfixMecanism } from '../components/mecanisms/common' import { typeWarning } from '../error' -import { evaluateNode, makeJsx, mergeAllMissing } from '../evaluation' +import { + evaluateNode, + makeJsx, + mergeAllMissing, + registerEvaluationFunction +} from '../evaluation' import { convertNodeToUnit } from '../nodeUnits' function MecanismPlafond({ explanation }) { @@ -69,14 +74,16 @@ export default function Plafond(recurse, v) { plafond: recurse(v.plafond) } return { - evaluate, jsx: MecanismPlafond, explanation, category: 'mecanism', name: 'plafond', + nodeKind: 'plafond', type: 'numeric', unit: explanation.valeur.unit } } Plafond.nom = 'plafond' + +registerEvaluationFunction('plafond', evaluate) diff --git a/publicodes/source/mecanisms/plancher.tsx b/publicodes/source/mecanisms/plancher.tsx index ec144cf7a..414e8c719 100644 --- a/publicodes/source/mecanisms/plancher.tsx +++ b/publicodes/source/mecanisms/plancher.tsx @@ -1,7 +1,12 @@ import React from 'react' import { InfixMecanism } from '../components/mecanisms/common' import { typeWarning } from '../error' -import { evaluateNode, makeJsx, mergeAllMissing } from '../evaluation' +import { + evaluateNode, + makeJsx, + mergeAllMissing, + registerEvaluationFunction +} from '../evaluation' import { convertNodeToUnit } from '../nodeUnits' function MecanismPlancher({ explanation }) { @@ -73,9 +78,12 @@ export default function Plancher(recurse, v) { explanation, category: 'mecanism', name: 'plancher', + nodeKind: 'plancher', type: 'numeric', unit: explanation.valeur.unit } } Plancher.nom = 'plancher' + +registerEvaluationFunction('plancher', evaluate) diff --git a/publicodes/source/mecanisms/possibility.tsx b/publicodes/source/mecanisms/possibility.tsx index ae3494e98..f53d8d950 100644 --- a/publicodes/source/mecanisms/possibility.tsx +++ b/publicodes/source/mecanisms/possibility.tsx @@ -1,7 +1,4 @@ -import React from 'react' -import { evaluateNode } from '../evaluation' -import { RuleLinkWithContext } from '../components/RuleLink' -import { Mecanism } from '../components/mecanisms/common' +import { registerEvaluationFunction } from '../evaluation' const evaluate = (cache, situation, parsedRules, node) => { return { ...node } @@ -10,12 +7,14 @@ const evaluate = (cache, situation, parsedRules, node) => { export const mecanismPossibility = (recurse, k, v) => { return { explanation: {}, - evaluate, jsx: function Synchronisation({ explanation }) { return null }, category: 'mecanism', name: 'possibilité', + nodeKind: 'possibilité', type: 'possibilité' } } + +registerEvaluationFunction('possibilité', evaluate) diff --git a/publicodes/source/mecanisms/product.tsx b/publicodes/source/mecanisms/product.tsx index de4383ec9..d735ef5ec 100644 --- a/publicodes/source/mecanisms/product.tsx +++ b/publicodes/source/mecanisms/product.tsx @@ -1,66 +1,30 @@ import Product from '../components/mecanisms/Product' import { typeWarning } from '../error' -import { defaultNode, evaluateObject, parseObject } from '../evaluation' +import { + defaultNode, + evaluateObject, + parseObject, + registerEvaluationFunction +} from '../evaluation' import { convertNodeToUnit, simplifyNodeUnit } from '../nodeUnits' import { areUnitConvertible, convertUnit, inferUnit } from '../units' +const objectShape = { + assiette: false, + taux: defaultNode(1), + facteur: defaultNode(1), + plafond: defaultNode(Infinity) +} + export const mecanismProduct = (recurse, v) => { - const objectShape = { - assiette: false, - taux: defaultNode(1), - facteur: defaultNode(1), - plafond: defaultNode(Infinity) - } - const effect = ({ assiette, taux, facteur, plafond }, cache) => { - if (assiette.unit) { - try { - plafond = convertNodeToUnit(assiette.unit, plafond) - } catch (e) { - typeWarning( - cache._meta.contextRule, - "Impossible de convertir l'unité du plafond du produit dans celle de l'assiette", - e - ) - } - } - const mult = (base, rate, facteur, plafond) => - Math.min(base, plafond === false ? Infinity : plafond) * rate * facteur - let nodeValue = [taux, assiette, facteur].some(n => n.nodeValue === false) - ? false - : [taux, assiette, facteur].some(n => n.nodeValue === 0) - ? 0 - : [taux, assiette, facteur].some(n => n.nodeValue === null) - ? null - : mult( - assiette.nodeValue, - taux.nodeValue, - facteur.nodeValue, - plafond.nodeValue - ) - let unit = inferUnit( - '*', - [assiette, taux, facteur].map(el => el.unit) - ) - if (areUnitConvertible(unit, assiette.unit)) { - nodeValue = convertUnit(unit, assiette.unit, nodeValue) - unit = assiette.unit - } - return simplifyNodeUnit({ - nodeValue, - unit, - explanation: { - plafondActif: assiette.nodeValue > plafond.nodeValue - } - }) - } - const explanation = parseObject(recurse, objectShape, v), - evaluate = evaluateObject(objectShape, effect) + const explanation = parseObject(recurse, objectShape, v) + return { - evaluate, jsx: Product, explanation, category: 'mecanism', name: 'produit', + nodeKind: 'produit', type: 'numeric', unit: inferUnit( '*', @@ -70,3 +34,50 @@ export const mecanismProduct = (recurse, v) => { ) } } + +const effect = ({ assiette, taux, facteur, plafond }, cache) => { + if (assiette.unit) { + try { + plafond = convertNodeToUnit(assiette.unit, plafond) + } catch (e) { + typeWarning( + cache._meta.contextRule, + "Impossible de convertir l'unité du plafond du produit dans celle de l'assiette", + e + ) + } + } + const mult = (base, rate, facteur, plafond) => + Math.min(base, plafond === false ? Infinity : plafond) * rate * facteur + let nodeValue = [taux, assiette, facteur].some(n => n.nodeValue === false) + ? false + : [taux, assiette, facteur].some(n => n.nodeValue === 0) + ? 0 + : [taux, assiette, facteur].some(n => n.nodeValue === null) + ? null + : mult( + assiette.nodeValue, + taux.nodeValue, + facteur.nodeValue, + plafond.nodeValue + ) + let unit = inferUnit( + '*', + [assiette, taux, facteur].map(el => el.unit) + ) + if (areUnitConvertible(unit, assiette.unit)) { + nodeValue = convertUnit(unit, assiette.unit, nodeValue) + unit = assiette.unit + } + return simplifyNodeUnit({ + nodeValue, + unit, + explanation: { + plafondActif: assiette.nodeValue > plafond.nodeValue + } + }) +} + +const evaluate = evaluateObject(objectShape, effect) + +registerEvaluationFunction('produit', evaluate) diff --git a/publicodes/source/mecanisms/recalcul.ts b/publicodes/source/mecanisms/recalcul.ts index 409948beb..fd83976d7 100644 --- a/publicodes/source/mecanisms/recalcul.ts +++ b/publicodes/source/mecanisms/recalcul.ts @@ -1,5 +1,9 @@ import Recalcul from '../components/mecanisms/Recalcul' -import { defaultNode, evaluateNode } from '../evaluation' +import { + defaultNode, + evaluateNode, + registerEvaluationFunction +} from '../evaluation' import { serializeUnit } from '../units' const evaluateRecalcul = (cache, situation, parsedRules, node) => { @@ -64,6 +68,8 @@ export const mecanismRecalcul = dottedNameContext => (recurse, v) => { amendedSituation }, jsx: Recalcul, - evaluate: evaluateRecalcul + nodeKind: 'recalcul' } } + +registerEvaluationFunction('recalcul', evaluateRecalcul) diff --git a/publicodes/source/mecanisms/reduction.ts b/publicodes/source/mecanisms/reduction.ts index cbf66ffee..8b3c93d1b 100644 --- a/publicodes/source/mecanisms/reduction.ts +++ b/publicodes/source/mecanisms/reduction.ts @@ -1,7 +1,12 @@ import { max, min } from 'ramda' import Allègement from '../components/mecanisms/Allègement' import { typeWarning } from '../error' -import { defaultNode, evaluateObject, parseObject } from '../evaluation' +import { + defaultNode, + evaluateObject, + parseObject, + registerEvaluationFunction +} from '../evaluation' import { convertNodeToUnit } from '../nodeUnits' import { serializeUnit } from '../units' @@ -61,12 +66,14 @@ export const mecanismReduction = (recurse, v) => { const explanation = parseObject(recurse, objectShape, v) return { - evaluate, jsx: Allègement, explanation, category: 'mecanism', name: 'allègement', + nodeKind: 'allègement', type: 'numeric', unit: explanation?.assiette?.unit } } + +registerEvaluationFunction('allègement', evaluate) diff --git a/publicodes/source/mecanisms/régularisation.ts b/publicodes/source/mecanisms/régularisation.ts index bdcc152a0..8e4eb8d89 100644 --- a/publicodes/source/mecanisms/régularisation.ts +++ b/publicodes/source/mecanisms/régularisation.ts @@ -1,7 +1,7 @@ import { map } from 'ramda' import { convertToString, getYear } from '../date' import { evaluationError } from '../error' -import { evaluateNode } from '../evaluation' +import { evaluateNode, registerEvaluationFunction } from '../evaluation' import { createTemporalEvaluation, groupByYear, @@ -46,13 +46,13 @@ export default function parse(parse, v) { }) as Array<{ dottedName: Name; value: Record }> return { - evaluate, explanation: { rule, variables }, category: 'mecanism', name: 'taux progressif', + nodeKind: 'régularisation', type: 'numeric', unit: rule.unit } @@ -163,3 +163,5 @@ function evaluate(cache, situation, parsedRules, node) { unit: evaluation.unit } } + +registerEvaluationFunction('régularisation', evaluate) diff --git a/publicodes/source/mecanisms/sum.tsx b/publicodes/source/mecanisms/sum.tsx index f6bf4bf7c..8a040cb98 100644 --- a/publicodes/source/mecanisms/sum.tsx +++ b/publicodes/source/mecanisms/sum.tsx @@ -1,5 +1,5 @@ import Somme from '../components/mecanisms/Somme' -import { evaluateArray } from '../evaluation' +import { evaluateArray, registerEvaluationFunction } from '../evaluation' import { inferUnit } from '../units' const evaluate = evaluateArray( @@ -10,11 +10,11 @@ const evaluate = evaluateArray( export const mecanismSum = (recurse, v) => { const explanation = v.map(recurse) return { - evaluate, jsx: Somme, explanation, category: 'mecanism', name: 'somme', + nodeKind: 'somme', type: 'numeric', unit: inferUnit( '+', @@ -22,3 +22,5 @@ export const mecanismSum = (recurse, v) => { ) } } + +registerEvaluationFunction('somme', evaluate) diff --git a/publicodes/source/mecanisms/synchronisation.tsx b/publicodes/source/mecanisms/synchronisation.tsx index 033b37502..ce33099dc 100644 --- a/publicodes/source/mecanisms/synchronisation.tsx +++ b/publicodes/source/mecanisms/synchronisation.tsx @@ -1,7 +1,7 @@ import { path } from 'ramda' import React from 'react' -import { evaluateNode } from '../evaluation' import { RuleLinkWithContext } from '../components/RuleLink' +import { evaluateNode, registerEvaluationFunction } from '../evaluation' const evaluate = (cache, situation, parsedRules, node) => { const APIExplanation = evaluateNode( @@ -36,7 +36,6 @@ const evaluate = (cache, situation, parsedRules, node) => { export const mecanismSynchronisation = (recurse, v) => { return { explanation: { ...v, API: recurse(v.API) }, - evaluate, jsx: function Synchronisation({ explanation }) { return (

      @@ -46,6 +45,9 @@ export const mecanismSynchronisation = (recurse, v) => { ) }, category: 'mecanism', - name: 'synchronisation' + name: 'synchronisation', + nodeKind: 'synchronisation' } } + +registerEvaluationFunction('synchronisation', evaluate) diff --git a/publicodes/source/mecanisms/tauxProgressif.ts b/publicodes/source/mecanisms/tauxProgressif.ts index 287bafb1e..47b63f0f9 100644 --- a/publicodes/source/mecanisms/tauxProgressif.ts +++ b/publicodes/source/mecanisms/tauxProgressif.ts @@ -1,5 +1,10 @@ -import { defaultNode, evaluateNode, mergeAllMissing } from '../evaluation' import tauxProgressif from '../components/mecanisms/TauxProgressif' +import { + defaultNode, + evaluateNode, + mergeAllMissing, + registerEvaluationFunction +} from '../evaluation' import { convertNodeToUnit } from '../nodeUnits' import { parseUnit } from '../units' import { @@ -14,11 +19,11 @@ export default function parse(parse, v) { tranches: parseTranches(parse, v.tranches) } return { - evaluate, jsx: tauxProgressif, explanation, category: 'mecanism', name: 'taux progressif', + nodeKind: 'taux progressif', type: 'numeric', unit: parseUnit('%') } @@ -121,3 +126,5 @@ const evaluate = ( nodeValue } } + +registerEvaluationFunction('taux progressif', evaluate) diff --git a/publicodes/source/mecanisms/utils.js b/publicodes/source/mecanisms/utils.js deleted file mode 100644 index 4f03ada1c..000000000 --- a/publicodes/source/mecanisms/utils.js +++ /dev/null @@ -1,40 +0,0 @@ -import { add, dissoc, objOf } from 'ramda' -import { evaluateArrayWithFilter } from '../evaluation' -import { inferUnit } from '../units' -import Composantes from '../components/mecanisms/Composantes' - -export let decompose = (recurse, k, v) => { - let subProps = dissoc('composantes')(v), - explanation = v.composantes.map(c => ({ - ...recurse( - objOf(k, { - ...subProps, - ...dissoc('attributs')(c) - }) - ), - composante: c.nom ? { nom: c.nom } : c.attributs - })) - - let filter = situation => c => - !situation['_meta.filter'] || - !c.composante || - ((!c.composante['dû par'] || - !['employeur', 'salarié'].includes(situation['_meta.filter']) || - c.composante['dû par'] == situation['_meta.filter']) && - (!c.composante['impôt sur le revenu'] || - !['déductible', 'non déductible'].includes(situation['_meta.filter']) || - c.composante['impôt sur le revenu'] == situation['_meta.filter'])) - - return { - explanation, - jsx: Composantes, - evaluate: evaluateArrayWithFilter(filter, add, 0), - category: 'mecanism', - name: 'composantes', - type: 'numeric', - unit: inferUnit( - '+', - explanation.map(e => e.unit) - ) - } -} diff --git a/publicodes/source/mecanisms/variableTemporelle.ts b/publicodes/source/mecanisms/variableTemporelle.ts index 9a4398c8b..8bec7db94 100644 --- a/publicodes/source/mecanisms/variableTemporelle.ts +++ b/publicodes/source/mecanisms/variableTemporelle.ts @@ -1,4 +1,4 @@ -import { evaluateNode } from '../evaluation' +import { evaluateNode, registerEvaluationFunction } from '../evaluation' import { createTemporalEvaluation, narrowTemporalValue, @@ -49,7 +49,7 @@ function evaluate( export default function parseVariableTemporelle(parse, v) { const explanation = parse(v.explanation) return { - evaluate, + nodeKind: 'variable temporelle', explanation: { period: { start: v.period.start && parse(v.period.start), @@ -60,3 +60,5 @@ export default function parseVariableTemporelle(parse, v) { unit: explanation.unit } } + +registerEvaluationFunction('variable temporelle', evaluate) diff --git a/publicodes/source/mecanisms/variations.ts b/publicodes/source/mecanisms/variations.ts index 047fd5e99..dbb5e44f2 100644 --- a/publicodes/source/mecanisms/variations.ts +++ b/publicodes/source/mecanisms/variations.ts @@ -1,7 +1,12 @@ import { or } from 'ramda' import Variations from '../components/mecanisms/Variations' import { typeWarning } from '../error' -import { bonus, defaultNode, evaluateNode } from '../evaluation' +import { + bonus, + defaultNode, + evaluateNode, + registerEvaluationFunction +} from '../evaluation' import { convertNodeToUnit } from '../nodeUnits' import { liftTemporal2, @@ -22,10 +27,10 @@ export default function parse(recurse, v) { // TODO - find an appropriate representation return { explanation, - evaluate, jsx: Variations, category: 'mecanism', name: 'variations', + nodeKind: 'variations', type: 'numeric', unit: inferUnit( '+', @@ -38,10 +43,10 @@ export function devariate(recurse, k, v) { const explanation = devariateExplanation(recurse, k, v) return { explanation, - evaluate, jsx: Variations, category: 'mecanism', name: 'variations', + nodeKind: 'variations', type: 'numeric', unit: inferUnit( '+', @@ -182,3 +187,5 @@ function evaluate( ...(temporalValue.length > 1 && { temporalValue }) } } + +registerEvaluationFunction('variations', evaluate) diff --git a/publicodes/source/parse.tsx b/publicodes/source/parse.tsx index cdc815323..f87c5bd51 100644 --- a/publicodes/source/parse.tsx +++ b/publicodes/source/parse.tsx @@ -1,40 +1,25 @@ -// This should be the new way to implement mecanisms -// In a specific file -// TODO import them automatically import { Grammar, Parser } from 'nearley' -import { - add, - difference, - divide, - equals, - fromPairs, - gt, - gte, - lt, - lte, - multiply, - omit, - subtract -} from 'ramda' +import { omit } from 'ramda' import React from 'react' import { EngineError, syntaxError } from './error' import { formatValue } from './format' import grammar from './grammar.ne' +import applicable from './mecanisms/applicable' import arrondi from './mecanisms/arrondi' import barème from './mecanisms/barème' +import { decompose } from './mecanisms/composantes' import { mecanismAllOf } from './mecanisms/condition-allof' import { mecanismOneOf } from './mecanisms/condition-oneof' import durée from './mecanisms/durée' -import plafond from './mecanisms/plafond' -import plancher from './mecanisms/plancher' -import applicable from './mecanisms/applicable' -import nonApplicable from './mecanisms/nonApplicable' import grille from './mecanisms/grille' import { mecanismInversion } from './mecanisms/inversion' import { mecanismMax } from './mecanisms/max' import { mecanismMin } from './mecanisms/min' +import nonApplicable from './mecanisms/nonApplicable' import { mecanismOnePossibility } from './mecanisms/one-possibility' -import operation from './mecanisms/operation' +import operations from './mecanisms/operation' +import plafond from './mecanisms/plafond' +import plancher from './mecanisms/plancher' import { mecanismProduct } from './mecanisms/product' import { mecanismRecalcul } from './mecanisms/recalcul' import { mecanismReduction } from './mecanisms/reduction' @@ -42,7 +27,6 @@ import régularisation from './mecanisms/régularisation' import { mecanismSum } from './mecanisms/sum' import { mecanismSynchronisation } from './mecanisms/synchronisation' import tauxProgressif from './mecanisms/tauxProgressif' -import { decompose } from './mecanisms/utils' import variableTemporelle from './mecanisms/variableTemporelle' import variations, { devariate } from './mecanisms/variations' import { parseReferenceTransforms } from './parseReference' @@ -204,28 +188,8 @@ function unfoldChainedMecanisms(rawNode) { ) } -const knownOperations = { - '*': [multiply, '×'], - '/': [divide, '∕'], - '+': [add], - '-': [subtract, '−'], - '<': [lt], - '<=': [lte, '≤'], - '>': [gt], - '>=': [gte, '≥'], - '=': [equals], - '!=': [(a, b) => !equals(a, b), '≠'] -} - -const operationDispatch = fromPairs( - Object.entries(knownOperations).map(([k, [f, symbol]]) => [ - k, - operation(k, f, symbol) - ]) -) - const statelessParseFunction = { - ...operationDispatch, + ...operations, ...chainableMecanisms.reduce((acc, fn) => ({ [fn.nom]: fn, ...acc }), {}), 'une de ces conditions': mecanismOneOf, 'toutes ces conditions': mecanismAllOf, @@ -248,6 +212,7 @@ const statelessParseFunction = { type: v.type, constant: true, nodeValue: v.nodeValue, + nodeKind: 'constant', unit: v.unit, // eslint-disable-next-line jsx: (node: EvaluatedRule) => ( diff --git a/publicodes/source/parseReference.ts b/publicodes/source/parseReference.ts index 488f9d923..233e659a1 100644 --- a/publicodes/source/parseReference.ts +++ b/publicodes/source/parseReference.ts @@ -1,233 +1,6 @@ import { Leaf } from './components/mecanisms/common' -import { typeWarning } from './error' -import { evaluateApplicability } from './evaluateRule' -import { evaluateNode, mergeMissing } from './evaluation' -import { convertNodeToUnit } from './nodeUnits' import parseRule from './parseRule' import { disambiguateRuleReference } from './ruleUtils' -import { EvaluatedNode, ParsedRule } from './types' -import { areUnitConvertible, serializeUnit } from './units' - -/** - * Statically filter out replacements from `replaceBy`. - * Note: whitelist and blacklist filtering are applicable to the replacement - * itself or any parent namespace. - */ -export const getApplicableReplacedBy = (contextRuleName, replacedBy) => - replacedBy - .sort( - (replacement1, replacement2) => - +!!replacement2.whiteListedNames - +!!replacement1.whiteListedNames - ) - .filter( - ({ whiteListedNames }) => - !whiteListedNames || - whiteListedNames.some(name => contextRuleName.startsWith(name)) - ) - .filter( - ({ blackListedNames }) => - !blackListedNames || - blackListedNames.every(name => !contextRuleName.startsWith(name)) - ) - .filter(({ referenceNode }) => contextRuleName !== referenceNode.dottedName) - -/** - * Filter-out and apply all possible replacements at runtime. - */ -const getApplicableReplacements = ( - contextRuleName, - cache, - situation, - rules, - rule: ParsedRule -) => { - let missingVariableList: Array = [] - if (contextRuleName.startsWith('[evaluation]')) { - return [[], []] - } - const applicableReplacements = getApplicableReplacedBy( - contextRuleName, - rule.replacedBy - ) - // Remove remplacement defined in a not applicable node - .filter(({ referenceNode }) => { - const referenceRule = rules[referenceNode.dottedName] - const { - nodeValue: isApplicable, - missingVariables - } = evaluateApplicability(cache, situation, rules, referenceRule) - missingVariableList.push(missingVariables) - return isApplicable - }) - // Remove remplacement defined in a node whose situation value is false - .filter(({ referenceNode }) => { - const referenceRule = rules[referenceNode.dottedName] - const situationValue = situation[referenceRule.dottedName] - if (referenceNode.question && situationValue == null) { - missingVariableList.push({ [referenceNode.dottedName]: 1 }) - } - return situationValue?.nodeValue !== false - }) - // Remove remplacement defined in a boolean node whose evaluated value is false - .filter(({ referenceNode }) => { - const referenceRule = rules[referenceNode.dottedName] - if (referenceRule.formule?.explanation?.operationType !== 'comparison') { - return true - } - const { nodeValue: isApplicable, missingVariables } = evaluateNode( - cache, - situation, - rules, - referenceRule - ) - missingVariableList.push(missingVariables) - return isApplicable - }) - .map(({ referenceNode, replacementNode }) => - replacementNode != null - ? evaluateNode(cache, situation, rules, replacementNode) - : evaluateReference(cache, situation, rules, referenceNode) - ) - .map(replacementNode => { - const replacedRuleUnit = rule.unit - if (!areUnitConvertible(replacementNode.unit, replacedRuleUnit)) { - typeWarning( - contextRuleName, - `L'unité de la règle de remplacement n'est pas compatible avec celle de la règle remplacée ${rule.dottedName}` - ) - } - return { - ...replacementNode, - unit: replacementNode.unit || replacedRuleUnit - } - }) - - missingVariableList = missingVariableList.filter( - missingVariables => !!Object.keys(missingVariables).length - ) - - return [applicableReplacements, missingVariableList] -} - -const evaluateReference = (cache, situation, rules, node) => { - const rule = rules[node.dottedName] - // When a rule exists in different version (created using the `replace` mecanism), we add - // a redirection in the evaluation of references to use a potential active replacement - const [ - applicableReplacements, - replacementMissingVariableList - ] = getApplicableReplacements( - node.explanation?.contextRuleName ?? '', - cache, - situation, - rules, - rule - ) - - if (applicableReplacements.length) { - if (applicableReplacements.length > 1) { - // eslint-disable-next-line no-console - console.warn(` -Règle ${rule.dottedName}: plusieurs remplacements valides ont été trouvés : -\n\t${applicableReplacements.map(node => node.rawNode).join('\n\t')} - -Par défaut, seul le premier s'applique. Si vous voulez un autre comportement, vous pouvez : - - Restreindre son applicabilité via "applicable si" sur la règle de définition - - Restreindre sa portée en ajoutant une liste blanche (via le mot clé "dans") ou une liste noire (via le mot clé "sauf dans") -`) - } - return applicableReplacements[0] - } - const addReplacementMissingVariable = node => ({ - ...node, - missingVariables: replacementMissingVariableList.reduce( - mergeMissing, - node.missingVariables - ) - }) - const dottedName = node.dottedName - // On va vérifier dans le cache courant, dict, si la variable n'a pas été déjà évaluée - // En effet, l'évaluation dans le cas d'une variable qui a une formule, est coûteuse ! - const cacheName = - dottedName + (node.explanation.filter ? ' .' + node.explanation.filter : '') - const cached = cache[cacheName] - - if (cached) return addReplacementMissingVariable(cached) - - const cacheNode = ( - nodeValue: EvaluatedNode['nodeValue'], - missingVariables: EvaluatedNode['missingVariables'], - explanation?: Record - ) => { - cache[cacheName] = { - ...node, - nodeValue, - ...(explanation && { - explanation - }), - ...(explanation?.temporalValue && { - temporalValue: explanation.temporalValue - }), - ...(explanation?.unit && { unit: explanation.unit }), - missingVariables - } - return addReplacementMissingVariable(cache[cacheName]) - } - const applicabilityEvaluation = evaluateApplicability( - cache, - situation, - rules, - rule - ) - if (!applicabilityEvaluation.nodeValue) { - return cacheNode( - applicabilityEvaluation.nodeValue, - applicabilityEvaluation.missingVariables, - applicabilityEvaluation - ) - } - if (situation[dottedName]) { - // Conditional evaluation is required because some mecanisms like - // "synchronisation" store raw JS objects in the situation. - const situationValue = situation[dottedName]?.evaluate - ? evaluateNode(cache, situation, rules, situation[dottedName]) - : situation[dottedName] - const unit = - !situationValue.unit || serializeUnit(situationValue.unit) === '' - ? rule.unit - : situationValue.unit - return cacheNode( - situationValue?.nodeValue !== undefined - ? situationValue.nodeValue - : situationValue, - applicabilityEvaluation.missingVariables, - { - ...rule, - ...(situationValue?.nodeValue !== undefined && situationValue), - unit - } - ) - } - - if (rule.defaultValue != null) { - const evaluation = evaluateNode(cache, situation, rules, rule.defaultValue) - return cacheNode(evaluation.nodeValue ?? evaluation, { - ...evaluation.missingVariables, - [dottedName]: 1 - }) - } - - if (rule.formule != null) { - const evaluation = evaluateNode(cache, situation, rules, rule) - return cacheNode( - evaluation.nodeValue, - evaluation.missingVariables, - evaluation - ) - } - - return cacheNode(null, { [dottedName]: 2 }) -} export const parseReference = ( rules, @@ -249,7 +22,7 @@ export const parseReference = ( (!inInversionFormula && parseRule(rules, dottedName, parsedRules)) const unit = parsedRule.unit return { - evaluate: evaluateReference, + nodeKind: 'reference', jsx: Leaf, name: partialReference, category: 'reference', @@ -260,43 +33,6 @@ export const parseReference = ( } } -// This function is a wrapper that can apply : -// - unit transformations to the value of the variable. -// See the unité-temporelle.yaml test suite for details -// - filters on the variable to select one part of the variable's 'composantes' - -const evaluateReferenceTransforms = (cache, situation, parsedRules, node) => { - // Filter transformation - const filteringSituation = { - ...situation, - '_meta.filter': node.explanation.filter - } - const filteredNode = evaluateNode( - cache, - node.explanation.filter ? filteringSituation : situation, - parsedRules, - node.explanation.originalNode - ) - const { explanation, nodeValue } = filteredNode - if (!explanation || nodeValue === null) { - return filteredNode - } - const unit = node.explanation.unit - if (unit) { - try { - return convertNodeToUnit(unit, filteredNode) - } catch (e) { - typeWarning( - cache._meta.contextRule, - `Impossible de convertir la reference '${filteredNode.name}'`, - e - ) - } - } - - return filteredNode -} - type parseReferenceTransformsParameters = { variable: { fragments: Array } filter?: string @@ -318,6 +54,7 @@ export const parseReferenceTransforms = (rules, rule, parsedRules) => ({ return { ...originalNode, + nodeKind: 'referenceWithTransforms', explanation: { originalNode, filter, @@ -333,7 +70,6 @@ export const parseReferenceTransforms = (rules, rule, parsedRules) => ({ } } : {}), - evaluate: evaluateReferenceTransforms, unit: unit || originalNode.unit } } diff --git a/publicodes/source/parseRule.tsx b/publicodes/source/parseRule.tsx index 2d8531c3f..a732c17d4 100644 --- a/publicodes/source/parseRule.tsx +++ b/publicodes/source/parseRule.tsx @@ -1,11 +1,10 @@ -import { evolve, map } from 'ramda' +import { evolve } from 'ramda' import React from 'react' import { Trans } from 'react-i18next' import { Mecanism } from './components/mecanisms/common' import { RuleLinkWithContext } from './components/RuleLink' import { compilationError, warning } from './error' -import evaluate from './evaluateRule' -import { evaluateNode, makeJsx, mergeAllMissing } from './evaluation' +import { makeJsx } from './evaluation' import { parse } from './parse' import { disambiguateRuleReference, @@ -101,12 +100,11 @@ export default function( ) : null return { - evaluate: (cache, situation, parsedRules) => - node.evaluate(cache, situation, parsedRules, node), jsx, category: 'ruleProp', rulePropType: 'cond', name: 'parentDependencies', + nodeKind: 'parentDependencies', type: 'numeric', explanation: node } @@ -130,7 +128,7 @@ export default function( // "synchronisation" mecanism. This should be refactored to not use the // attribute "defaultValue" typeof value === 'object' - ? { ...value, evaluate: () => value } + ? { ...value, nodeKind: 'defaultNode' } : value, formule: value => { const child = parse(rules, rule, parsedRules)(value) @@ -138,7 +136,7 @@ export default function( const jsx = ({ explanation }) => makeJsx(explanation) return { - evaluate: evaluateFormula, + nodeKind: 'formula', jsx, category: 'ruleProp', rulePropType: 'formula', @@ -154,7 +152,7 @@ export default function( // principe que 'non applicable si' et 'formule' sont particuliers, alors // qu'ils pourraient être rangé avec les autres mécanismes ...parsedRule, - evaluate, + nodeKind: 'rule', parsed: true, unit: parsedRule.unit ?? @@ -164,7 +162,7 @@ export default function( replacedBy: [] } parsedRules[dottedName]['rendu non applicable'] = { - evaluate: evaluateDisabledBy, + nodeKind: 'disabledBy', jsx: ({ explanation: { isDisabledBy } }) => { return ( isDisabledBy.length > 0 && ( @@ -215,54 +213,6 @@ export default function( return parsedRules[dottedName] } -const evaluateFormula = (cache, situation, parsedRules, node) => { - const explanation = evaluateNode( - cache, - situation, - parsedRules, - node.explanation - ), - { nodeValue, unit, missingVariables, temporalValue } = explanation - - return { - ...node, - nodeValue, - unit, - missingVariables, - explanation, - temporalValue - } -} - -const evaluateDisabledBy = (cache, situation, parsedRules, node) => { - const isDisabledBy = node.explanation.isDisabledBy.map(disablerNode => - evaluateNode(cache, situation, parsedRules, disablerNode) - ) - const nodeValue = isDisabledBy.some( - x => x.nodeValue !== false && x.nodeValue !== null - ) - const explanation = { ...node.explanation, isDisabledBy } - return { - ...node, - explanation, - nodeValue, - missingVariables: mergeAllMissing(isDisabledBy) - } -} - -const evaluateEvolveCond = (cache, situation, parsedRules, node) => { - const explanation = evaluateNode( - cache, - situation, - parsedRules, - node.explanation - ), - nodeValue = explanation.nodeValue, - missingVariables = explanation.missingVariables - - return { ...node, nodeValue, explanation, missingVariables } -} - const evolveCond = (dottedName, rule, rules, parsedRules) => value => { const child = parse(rules, rule, parsedRules)(value) @@ -277,8 +227,8 @@ const evolveCond = (dottedName, rule, rules, parsedRules) => value => { ) return { - evaluate: evaluateEvolveCond, jsx, + nodeKind: 'condition', category: 'ruleProp', rulePropType: 'cond', dottedName,