From bc8c4d823a3df65be0039da09c5ab93369283b75 Mon Sep 17 00:00:00 2001 From: Maxime Quandalle Date: Wed, 4 Nov 2020 16:47:12 +0100 Subject: [PATCH] =?UTF-8?q?=E2=9A=99=EF=B8=8F=20Nouvelle=20API=20d'=C3=A9v?= =?UTF-8?q?aluation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Modifie l'API de la fonction `evaluate` pour transmettre le contexte avec `this`, ce qui simplifie l'interface de ces fonctions. L'objet `this` (qui contient `this.parsedRules`, `this.situation`, `this.evaluate`, etc.) est un interpréteur Publicodes, mais nous n'avons pas besoin de créer une nouvelle abstraction car cet objet présente exactement la même interface que l'objet public exposé dans `publicodes/index.ts` et c'est donc l'interface publique qui est utilisée dans les appels internes. --- .vscode/settings.json | 5 +- publicodes/source/evaluateReference.ts | 101 ++++----- publicodes/source/evaluateRule.ts | 59 ++--- publicodes/source/evaluation.tsx | 206 +++++++++--------- publicodes/source/index.ts | 40 +++- publicodes/source/mecanisms/applicable.tsx | 14 +- publicodes/source/mecanisms/arrondi.tsx | 14 +- publicodes/source/mecanisms/barème.ts | 22 +- publicodes/source/mecanisms/composantes.ts | 31 ++- .../source/mecanisms/condition-allof.tsx | 6 +- .../source/mecanisms/condition-oneof.tsx | 8 +- publicodes/source/mecanisms/durée.tsx | 14 +- publicodes/source/mecanisms/grille.ts | 17 +- publicodes/source/mecanisms/inversion.ts | 55 +++-- publicodes/source/mecanisms/nonApplicable.tsx | 14 +- .../source/mecanisms/one-possibility.tsx | 11 +- publicodes/source/mecanisms/operation.tsx | 11 +- publicodes/source/mecanisms/plafond.tsx | 16 +- publicodes/source/mecanisms/plancher.tsx | 16 +- publicodes/source/mecanisms/product.tsx | 12 +- publicodes/source/mecanisms/recalcul.ts | 52 +++-- publicodes/source/mecanisms/reduction.ts | 82 ++++--- publicodes/source/mecanisms/régularisation.ts | 31 +-- publicodes/source/mecanisms/sum.tsx | 2 +- .../source/mecanisms/synchronisation.tsx | 12 +- publicodes/source/mecanisms/tauxProgressif.ts | 17 +- .../source/mecanisms/variableTemporelle.ts | 23 +- publicodes/source/mecanisms/variations.ts | 23 +- publicodes/source/parse.tsx | 6 +- publicodes/source/types/index.ts | 5 +- 30 files changed, 400 insertions(+), 525 deletions(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index 5850abf04..71e6bdb8c 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -4,8 +4,5 @@ "spellright.documentTypes": ["yaml", "git-commit", "markdown"], "typescript.tsdk": "node_modules/typescript/lib", "editor.tabSize": 2, - "eslint.enable": true, - "editor.codeActionsOnSave": { - "source.organizeImports": true - } + "eslint.enable": true } diff --git a/publicodes/source/evaluateReference.ts b/publicodes/source/evaluateReference.ts index a769cb821..f2a898f9f 100644 --- a/publicodes/source/evaluateReference.ts +++ b/publicodes/source/evaluateReference.ts @@ -1,25 +1,23 @@ -import { EvaluatedNode, ParsedRule } from '.' +import Engine, { EvaluatedNode, evaluationFunction } from '.' import { typeWarning } from './error' import { evaluateApplicability } from './evaluateRule' -import { mergeMissing, evaluateNode } from './evaluation' +import { mergeMissing } from './evaluation' import { convertNodeToUnit } from './nodeUnits' -import { serializeUnit, areUnitConvertible } from './units' +import { ParsedRule } from './types' +import { areUnitConvertible, serializeUnit } from './units' -export const evaluateReference = (cache, situation, rules, node) => { - const rule = rules[node.dottedName] +export const evaluateReference: evaluationFunction = function(node) { + const rule = this.parsedRules[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( + ] = getApplicableReplacements.call( + this, node.explanation?.contextRuleName ?? '', - cache, - situation, - rules, rule ) - if (applicableReplacements.length) { if (applicableReplacements.length > 1) { // eslint-disable-next-line no-console @@ -32,7 +30,7 @@ Par défaut, seul le premier s'applique. Si vous voulez un autre comportement, v - 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] + return this.evaluateNode(applicableReplacements[0]) } const addReplacementMissingVariable = node => ({ ...node, @@ -46,7 +44,7 @@ Par défaut, seul le premier s'applique. Si vous voulez un autre comportement, v // 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] + const cached = this.cache[cacheName] if (cached) return addReplacementMissingVariable(cached) @@ -55,7 +53,7 @@ Par défaut, seul le premier s'applique. Si vous voulez un autre comportement, v missingVariables: EvaluatedNode['missingVariables'], explanation?: Record ) => { - cache[cacheName] = { + this.cache[cacheName] = { ...node, nodeValue, ...(explanation && { @@ -67,14 +65,10 @@ Par défaut, seul le premier s'applique. Si vous voulez un autre comportement, v ...(explanation?.unit && { unit: explanation.unit }), missingVariables } - return addReplacementMissingVariable(cache[cacheName]) + return addReplacementMissingVariable(this.cache[cacheName]) } - const applicabilityEvaluation = evaluateApplicability( - cache, - situation, - rules, - rule - ) + const applicabilityEvaluation = evaluateApplicability.call(this, rule as any) + if (!applicabilityEvaluation.nodeValue) { return cacheNode( applicabilityEvaluation.nodeValue, @@ -82,12 +76,12 @@ Par défaut, seul le premier s'applique. Si vous voulez un autre comportement, v applicabilityEvaluation ) } - if (situation[dottedName]) { + if (this.parsedSituation[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 situationValue = this.parsedSituation[dottedName]?.nodeKind + ? this.evaluateNode(this.parsedSituation[dottedName]) + : this.parsedSituation[dottedName] const unit = !situationValue.unit || serializeUnit(situationValue.unit) === '' ? rule.unit @@ -106,7 +100,7 @@ Par défaut, seul le premier s'applique. Si vous voulez un autre comportement, v } if (rule.defaultValue != null) { - const evaluation = evaluateNode(cache, situation, rules, rule.defaultValue) + const evaluation = this.evaluateNode(rule.defaultValue) return cacheNode(evaluation.nodeValue ?? evaluation, { ...evaluation.missingVariables, [dottedName]: 1 @@ -114,7 +108,7 @@ Par défaut, seul le premier s'applique. Si vous voulez un autre comportement, v } if (rule.formule != null) { - const evaluation = evaluateNode(cache, situation, rules, rule) + const evaluation = this.evaluateNode(rule) return cacheNode( evaluation.nodeValue, evaluation.missingVariables, @@ -130,23 +124,15 @@ Par défaut, seul le premier s'applique. Si vous voulez un autre comportement, v // 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 -) => { +export const evaluateReferenceTransforms: evaluationFunction = function(node) { // Filter transformation - const filteringSituation = { - ...situation, - '_meta.filter': node.explanation.filter + if (node.explanation.filter) { + this.cache._meta.filter = node.explanation.filter + } + const filteredNode = this.evaluateNode(node.explanation.originalNode) + if (node.explanation.filter) { + delete this.cache._meta.filter } - const filteredNode = evaluateNode( - cache, - node.explanation.filter ? filteringSituation : situation, - parsedRules, - node.explanation.originalNode - ) const { explanation, nodeValue } = filteredNode if (!explanation || nodeValue === null) { return filteredNode @@ -157,7 +143,7 @@ export const evaluateReferenceTransforms = ( return convertNodeToUnit(unit, filteredNode) } catch (e) { typeWarning( - cache._meta.contextRule, + this.cache._meta.contextRule, `Impossible de convertir la reference '${filteredNode.name}'`, e ) @@ -193,13 +179,11 @@ export const getApplicableReplacedBy = (contextRuleName, replacedBy) => /** * Filter-out and apply all possible replacements at runtime. */ -const getApplicableReplacements = ( - contextRuleName, - cache, - situation, - rules, +const getApplicableReplacements = function( + this: Engine, + contextRuleName: string, rule: ParsedRule -) => { +) { let missingVariableList: Array = [] if (contextRuleName.startsWith('[evaluation]')) { return [[], []] @@ -210,42 +194,37 @@ const getApplicableReplacements = ( ) // Remove remplacement defined in a not applicable node .filter(({ referenceNode }) => { - const referenceRule = rules[referenceNode.dottedName] + const referenceRule = this.parsedRules[referenceNode.dottedName] const { nodeValue: isApplicable, missingVariables - } = evaluateApplicability(cache, situation, rules, referenceRule) + } = evaluateApplicability.call(this, referenceRule as any) 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] + const referenceRule = this.parsedRules[referenceNode.dottedName] + const situationValue = this.parsedSituation[referenceRule.dottedName] if (referenceNode.question && situationValue == null) { missingVariableList.push({ [referenceNode.dottedName]: 1 }) } - return situationValue?.nodeValue !== false + return (situationValue as any)?.nodeValue !== false }) // Remove remplacement defined in a boolean node whose evaluated value is false .filter(({ referenceNode }) => { - const referenceRule = rules[referenceNode.dottedName] + const referenceRule = this.parsedRules[referenceNode.dottedName] if (referenceRule.formule?.explanation?.operationType !== 'comparison') { return true } - const { nodeValue: isApplicable, missingVariables } = evaluateNode( - cache, - situation, - rules, + const { nodeValue: isApplicable, missingVariables } = this.evaluateNode( referenceRule ) missingVariableList.push(missingVariables) return isApplicable }) .map(({ referenceNode, replacementNode }) => - replacementNode != null - ? evaluateNode(cache, situation, rules, replacementNode) - : evaluateReference(cache, situation, rules, referenceNode) + replacementNode != null ? replacementNode : referenceNode ) .map(replacementNode => { const replacedRuleUnit = rule.unit diff --git a/publicodes/source/evaluateRule.ts b/publicodes/source/evaluateRule.ts index c680c1348..a553dfa2e 100644 --- a/publicodes/source/evaluateRule.ts +++ b/publicodes/source/evaluateRule.ts @@ -1,25 +1,15 @@ import { map, pick, pipe } from 'ramda' +import { evaluationFunction } from '.' import { typeWarning } from './error' -import { - bonus, - evaluateNode, - mergeMissing, - mergeAllMissing -} from './evaluation' +import { bonus, mergeAllMissing, mergeMissing } from './evaluation' import { convertNodeToUnit } from './nodeUnits' -import { EvaluatedNode, ParsedRule } from './types' -export const evaluateApplicability = ( - cache, - situation, - parsedRules, - node: ParsedRule -): EvaluatedNode => { +export const evaluateApplicability: evaluationFunction = function(node: any) { const evaluatedAttributes = pipe( pick(['non applicable si', 'applicable si', 'rendu non applicable']) as ( x: any ) => any, - map(value => evaluateNode(cache, situation, parsedRules, value)) + map(value => this.evaluateNode(value)) )(node) as any, { 'non applicable si': notApplicable, @@ -27,7 +17,7 @@ export const evaluateApplicability = ( 'rendu non applicable': disabled } = evaluatedAttributes, parentDependencies = node.parentDependencies.map(parent => - evaluateNode(cache, situation, parsedRules, parent) + this.evaluateNode(parent) ) const anyDisabledParent = parentDependencies.find( @@ -69,14 +59,9 @@ export const evaluateApplicability = ( } } -export const evaluateFormula = (cache, situation, parsedRules, node) => { - const explanation = evaluateNode( - cache, - situation, - parsedRules, - node.explanation - ), - { nodeValue, unit, missingVariables, temporalValue } = explanation +export const evaluateFormula: evaluationFunction = function(node) { + const explanation = this.evaluateNode(node.explanation) + const { nodeValue, unit, missingVariables, temporalValue } = explanation return { ...node, @@ -88,14 +73,9 @@ export const evaluateFormula = (cache, situation, parsedRules, node) => { } } -export const evaluateRule = (cache, situation, parsedRules, node) => { - cache._meta.contextRule.push(node.dottedName) - const applicabilityEvaluation = evaluateApplicability( - cache, - situation, - parsedRules, - node - ) +export const evaluateRule: evaluationFunction = function(node: any) { + this.cache._meta.contextRule.push(node.dottedName) + const applicabilityEvaluation = evaluateApplicability.call(this, node) const { missingVariables: condMissing, nodeValue: isApplicable @@ -104,7 +84,7 @@ export const evaluateRule = (cache, situation, parsedRules, node) => { // evaluate the formula lazily, only if the applicability is known and true let evaluatedFormula = isApplicable && node.formule - ? evaluateNode(cache, situation, parsedRules, node.formule) + ? this.evaluateNode(node.formule) : node.formule if (node.unit) { @@ -124,7 +104,7 @@ export const evaluateRule = (cache, situation, parsedRules, node) => { ) const temporalValue = evaluatedFormula.temporalValue - cache._meta.contextRule.pop() + this.cache._meta.contextRule.pop() return { ...node, ...applicabilityEvaluation, @@ -137,9 +117,9 @@ export const evaluateRule = (cache, situation, parsedRules, node) => { } } -export const evaluateDisabledBy = (cache, situation, parsedRules, node) => { +export const evaluateDisabledBy: evaluationFunction = function(node) { const isDisabledBy = node.explanation.isDisabledBy.map(disablerNode => - evaluateNode(cache, situation, parsedRules, disablerNode) + this.evaluateNode(disablerNode) ) const nodeValue = isDisabledBy.some( x => x.nodeValue !== false && x.nodeValue !== null @@ -153,13 +133,8 @@ export const evaluateDisabledBy = (cache, situation, parsedRules, node) => { } } -export const evaluateCondition = (cache, situation, parsedRules, node) => { - const explanation = evaluateNode( - cache, - situation, - parsedRules, - node.explanation - ) +export const evaluateCondition: evaluationFunction = function(node) { + const explanation = this.evaluateNode(node.explanation) const nodeValue = explanation.nodeValue const missingVariables = explanation.missingVariables diff --git a/publicodes/source/evaluation.tsx b/publicodes/source/evaluation.tsx index 17d426a98..ab4c5217b 100644 --- a/publicodes/source/evaluation.tsx +++ b/publicodes/source/evaluation.tsx @@ -1,5 +1,6 @@ import { add, evolve, fromPairs, keys, map, mergeWith, reduce } from 'ramda' import React from 'react' +import Engine, { evaluationFunction } from '.' import { typeWarning } from './error' import { evaluateReference, @@ -37,15 +38,6 @@ export const mergeAllMissing = missings => export const mergeMissing = (left, right) => mergeWith(add, left || {}, right || {}) -export const evaluateNode = (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) { const firstNodeWithUnit = nodes.find(node => !!node.unit) if (!firstNodeWithUnit) { @@ -67,49 +59,49 @@ function convertNodesToSameUnit(nodes, contextRule, mecanismName) { }) } -export const evaluateArray = (reducer, start) => ( - cache, - situation, - parsedRules, - node -) => { - const evaluate = evaluateNode.bind(null, cache, situation, parsedRules) - const evaluatedNodes = convertNodesToSameUnit( - node.explanation.map(evaluate), - cache._meta.contextRule, - node.name - ) - - const temporalValues = concatTemporals( - evaluatedNodes.map( - ({ temporalValue, nodeValue }) => temporalValue ?? pureTemporal(nodeValue) +export const evaluateArray: ( + reducer: Parameters[0], + start: Parameters[1] +) => evaluationFunction = (reducer, start) => + function(node: any) { + const evaluate = this.evaluateNode.bind(this) + const evaluatedNodes = convertNodesToSameUnit( + node.explanation.map(evaluate), + this.cache._meta.contextRule, + node.name ) - ) - const temporalValue = mapTemporal(values => { - if (values.some(value => value === null)) { - return null - } - return reduce(reducer, start, values) - }, temporalValues) - const baseEvaluation = { - ...node, - missingVariables: mergeAllMissing(evaluatedNodes), - explanation: evaluatedNodes, - ...(evaluatedNodes[0] && { unit: evaluatedNodes[0].unit }) - } - if (temporalValue.length === 1) { + const temporalValues = concatTemporals( + evaluatedNodes.map( + ({ temporalValue, nodeValue }) => + temporalValue ?? pureTemporal(nodeValue) + ) + ) + const temporalValue = mapTemporal(values => { + if (values.some(value => value === null)) { + return null + } + return reduce(reducer, start, values) + }, temporalValues) + + const baseEvaluation = { + ...node, + missingVariables: mergeAllMissing(evaluatedNodes), + explanation: evaluatedNodes, + ...(evaluatedNodes[0] && { unit: evaluatedNodes[0].unit }) + } + if (temporalValue.length === 1) { + return { + ...baseEvaluation, + nodeValue: temporalValue[0].value + } + } return { ...baseEvaluation, - nodeValue: temporalValue[0].value + temporalValue, + nodeValue: temporalAverage(temporalValue as any) } } - return { - ...baseEvaluation, - temporalValue, - nodeValue: temporalAverage(temporalValue) - } -} export const defaultNode = (nodeValue: EvaluatedNode['nodeValue']) => ({ nodeValue, @@ -121,9 +113,10 @@ export const defaultNode = (nodeValue: EvaluatedNode['nodeValue']) => ({ nodeKind: 'defaultNode' }) -const evaluateDefaultNode = (cache, situation, parsedRules, node) => node -const evaluateExplanationNode = (cache, situation, parsedRules, node) => - evaluateNode(cache, situation, parsedRules, node.explanation) +const evaluateDefaultNode: evaluationFunction = node => node +const evaluateExplanationNode: evaluationFunction = function(node) { + return this.evaluateNode(node.explanation) +} export const parseObject = (recurse, objectShape, value) => { const recurseOne = key => defaultValue => { @@ -139,71 +132,69 @@ export const parseObject = (recurse, objectShape, value) => { return evolve(transforms as any, objectShape) } -export const evaluateObject = (objectShape, effect) => ( - cache, - situation, - parsedRules, - node -) => { - const evaluate = evaluateNode.bind(null, cache, situation, parsedRules) - const evaluations = map(evaluate, node.explanation) - const temporalExplanations = mapTemporal( - Object.fromEntries, - concatTemporals( - Object.entries(evaluations).map(([key, node]) => - zipTemporals(pureTemporal(key), liftTemporalNode(node)) +export const evaluateObject: ( + effet: (this: Engine, explanations: any) => any +) => evaluationFunction = effect => + function(node: any) { + const evaluate = this.evaluateNode.bind(this) + const evaluations = map(evaluate, node.explanation) + const temporalExplanations = mapTemporal( + Object.fromEntries, + concatTemporals( + Object.entries(evaluations).map(([key, node]) => + zipTemporals(pureTemporal(key), liftTemporalNode(node)) + ) ) ) - ) - const temporalExplanation = mapTemporal(explanations => { - const evaluation = effect(explanations, cache, situation, parsedRules) - return { - ...evaluation, - explanation: { - ...explanations, - ...evaluation.explanation + const temporalExplanation = mapTemporal(explanations => { + const evaluation = effect.call(this, explanations) + return { + ...evaluation, + explanation: { + ...explanations, + ...evaluation.explanation + } + } + }, temporalExplanations) + + const sameUnitTemporalExplanation: Temporal> = convertNodesToSameUnit( + temporalExplanation.map(x => x.value), + this.cache._meta.contextRule, + node.name + ).map((node, i) => ({ + ...temporalExplanation[i], + value: simplifyNodeUnit(node) + })) + + const temporalValue = mapTemporal( + ({ nodeValue }) => nodeValue, + sameUnitTemporalExplanation + ) + const nodeValue = temporalAverage(temporalValue) + const baseEvaluation = { + ...node, + nodeValue, + unit: sameUnitTemporalExplanation[0].value.unit, + explanation: evaluations, + missingVariables: mergeAllMissing(Object.values(evaluations)) + } + if (sameUnitTemporalExplanation.length === 1) { + return { + ...baseEvaluation, + explanation: sameUnitTemporalExplanation[0].value.explanation } } - }, temporalExplanations) - - const sameUnitTemporalExplanation: Temporal> = convertNodesToSameUnit( - temporalExplanation.map(x => x.value), - cache._meta.contextRule, - node.name - ).map((node, i) => ({ - ...temporalExplanation[i], - value: simplifyNodeUnit(node) - })) - - const temporalValue = mapTemporal( - ({ nodeValue }) => nodeValue, - sameUnitTemporalExplanation - ) - const nodeValue = temporalAverage(temporalValue) - const baseEvaluation = { - ...node, - nodeValue, - unit: sameUnitTemporalExplanation[0].value.unit, - explanation: evaluations, - missingVariables: mergeAllMissing(Object.values(evaluations)) - } - if (sameUnitTemporalExplanation.length === 1) { return { ...baseEvaluation, - explanation: sameUnitTemporalExplanation[0].value.explanation + temporalValue, + temporalExplanation } } - return { - ...baseEvaluation, - temporalValue, - temporalExplanation - } -} -const evaluationFunctions = { +export const evaluationFunctions = { rule: evaluateRule, formula: evaluateFormula, disabledBy: evaluateDisabledBy, @@ -215,7 +206,10 @@ const evaluationFunctions = { defaultNode: evaluateDefaultNode } -export function registerEvaluationFunction(nodeKind, evaluationFunction) { +export function registerEvaluationFunction( + nodeKind: string, + evaluationFunction: any // TODO: type evaluationFunction +) { if (evaluationFunctions[nodeKind]) { throw Error( `Multiple evaluation functions registered for the nodeKind \x1b[4m${nodeKind}` diff --git a/publicodes/source/index.ts b/publicodes/source/index.ts index 372c19122..3db6e09a5 100644 --- a/publicodes/source/index.ts +++ b/publicodes/source/index.ts @@ -1,13 +1,13 @@ /* eslint-disable @typescript-eslint/ban-types */ import { map } from 'ramda' import { evaluationError, warning } from './error' -import { evaluateNode } from './evaluation' +import { evaluationFunctions } from './evaluation' import { convertNodeToUnit, simplifyNodeUnit } from './nodeUnits' import { parse } from './parse' import parseRules from './parseRules' +import * as utils from './ruleUtils' import { EvaluatedNode, EvaluatedRule, ParsedRules, Rules } from './types' import { parseUnit } from './units' -import * as utils from './ruleUtils' const emptyCache = () => ({ _meta: { contextRule: [] } @@ -16,10 +16,14 @@ const emptyCache = () => ({ type Cache = { _meta: { contextRule: Array - inversionFail?: { - given: string - estimated: string - } + inversionFail?: + | { + given: string + estimated: string + } + | true + inRecalcul?: boolean + filter?: string } } @@ -30,21 +34,26 @@ export type EvaluationOptions = Partial<{ }> export * from './components' +export { default as cyclesLib } from './cyclesLib/index' export { formatValue, serializeValue } from './format' export { default as translateRules } from './translateRules' -export { default as cyclesLib } from './cyclesLib/index' export * from './types' export { parseRules } export { utils } +export type evaluationFunction = ( + this: Engine, + node: EvaluatedNode +) => EvaluatedNode export default class Engine { parsedRules: ParsedRules parsedSituation: ParsedSituation = {} - private cache: Cache + cache: Cache private warnings: Array = [] constructor(rules: string | Rules | ParsedRules) { this.cache = emptyCache() + this.resetCache() this.parsedRules = typeof rules === 'string' || !(Object.values(rules)[0] as any)?.dottedName ? parseRules(rules) @@ -67,10 +76,7 @@ export default class Engine { originalWarn(warning) } const result = simplifyNodeUnit( - evaluateNode( - this.cache, - this.parsedSituation, - this.parsedRules, + this.evaluateNode( parse( this.parsedRules, { dottedName: context }, @@ -156,4 +162,14 @@ export default class Engine { getParsedRules(): ParsedRules { return this.parsedRules } + + evaluateNode(node) { + if (!node.nodeKind) { + throw Error('The provided node must have a "nodeKind" attribute') + } else if (!evaluationFunctions[node.nodeKind]) { + throw Error(`Unknown "nodeKind": ${node.nodeKind}`) + } + + return evaluationFunctions[node.nodeKind].call(this, node) + } } diff --git a/publicodes/source/mecanisms/applicable.tsx b/publicodes/source/mecanisms/applicable.tsx index d93715620..fae9cfb3e 100644 --- a/publicodes/source/mecanisms/applicable.tsx +++ b/publicodes/source/mecanisms/applicable.tsx @@ -1,8 +1,8 @@ import React from 'react' +import { evaluationFunction } from '..' import { InfixMecanism } from '../components/mecanisms/common' import { bonus, - evaluateNode, makeJsx, mergeMissing, registerEvaluationFunction @@ -19,17 +19,11 @@ function MecanismApplicable({ explanation }) { ) } -const evaluate = (cache, situation, parsedRules, node) => { - const evaluateAttribute = evaluateNode.bind( - null, - cache, - situation, - parsedRules - ) - const condition = evaluateAttribute(node.explanation.condition) +const evaluate: evaluationFunction = function(node) { + const condition = this.evaluateNode(node.explanation.condition) let valeur = node.explanation.valeur if (condition.nodeValue !== false) { - valeur = evaluateAttribute(valeur) + valeur = this.evaluateNode(valeur) } return { ...node, diff --git a/publicodes/source/mecanisms/arrondi.tsx b/publicodes/source/mecanisms/arrondi.tsx index 5aa816b59..bb497adfe 100644 --- a/publicodes/source/mecanisms/arrondi.tsx +++ b/publicodes/source/mecanisms/arrondi.tsx @@ -1,7 +1,7 @@ import React from 'react' +import { evaluationFunction } from '..' import { InfixMecanism } from '../components/mecanisms/common' import { - evaluateNode, makeJsx, mergeAllMissing, registerEvaluationFunction @@ -28,18 +28,12 @@ function roundWithPrecision(n: number, fractionDigits: number) { return +n.toFixed(fractionDigits) } -const evaluate = (cache, situation, parsedRules, node) => { - const evaluateAttribute = evaluateNode.bind( - null, - cache, - situation, - parsedRules - ) - const valeur = evaluateAttribute(node.explanation.valeur) +const evaluate: evaluationFunction = function(node) { + const valeur = this.evaluateNode(node.explanation.valeur) const nodeValue = valeur.nodeValue let arrondi = node.explanation.arrondi if (nodeValue !== false) { - arrondi = evaluateAttribute(arrondi) + arrondi = this.evaluateNode(arrondi) } return { diff --git a/publicodes/source/mecanisms/barème.ts b/publicodes/source/mecanisms/barème.ts index 516f2f396..431d80b7a 100644 --- a/publicodes/source/mecanisms/barème.ts +++ b/publicodes/source/mecanisms/barème.ts @@ -1,8 +1,8 @@ +import { evaluationFunction } from '..' import Barème from '../components/mecanisms/Barème' import { evaluationError } from '../error' import { defaultNode, - evaluateNode, mergeAllMissing, registerEvaluationFunction } from '../evaluation' @@ -76,31 +76,27 @@ function evaluateBarème(tranches, assiette, evaluate, cache) { } }) } -const evaluate = ( - cache, - situation, - parsedRules, - node: ReturnType -) => { - const evaluate = evaluateNode.bind(null, cache, situation, parsedRules) - const assiette = evaluate(node.explanation.assiette) - const multiplicateur = evaluate(node.explanation.multiplicateur) +const evaluate: evaluationFunction = function(node) { + const evaluateNode = this.evaluateNode.bind(this) + const assiette = this.evaluateNode(node.explanation.assiette) + const multiplicateur = this.evaluateNode(node.explanation.multiplicateur) const temporalTranchesPlafond = liftTemporal2( (assiette, multiplicateur) => evaluatePlafondUntilActiveTranche( - evaluate, + evaluateNode, { parsedTranches: node.explanation.tranches, assiette, multiplicateur }, - cache + this.cache ), liftTemporalNode(assiette), liftTemporalNode(multiplicateur) ) const temporalTranches = liftTemporal2( - (tranches, assiette) => evaluateBarème(tranches, assiette, evaluate, cache), + (tranches, assiette) => + evaluateBarème(tranches, assiette, evaluateNode, this.cache), temporalTranchesPlafond, liftTemporalNode(assiette) ) diff --git a/publicodes/source/mecanisms/composantes.ts b/publicodes/source/mecanisms/composantes.ts index 499fe1f75..b020dd002 100644 --- a/publicodes/source/mecanisms/composantes.ts +++ b/publicodes/source/mecanisms/composantes.ts @@ -1,28 +1,25 @@ import { add, dissoc, filter, objOf } from 'ramda' +import { evaluationFunction } from '..' +import Composantes from '../components/mecanisms/Composantes' import { evaluateArray, registerEvaluationFunction } from '../evaluation' import { inferUnit } from '../units' -import Composantes from '../components/mecanisms/Composantes' -export const evaluateComposantes = (cache, situation, parsedRules, node) => { +export const evaluateComposantes: evaluationFunction = function(node) { const evaluationFilter = c => - !situation['_meta.filter'] || + !this.cache._meta.filter || !c.composante || ((!c.composante['dû par'] || - !['employeur', 'salarié'].includes(situation['_meta.filter']) || - c.composante['dû par'] == situation['_meta.filter']) && + !['employeur', 'salarié'].includes(this.cache._meta.filter as any) || + c.composante['dû par'] == this.cache._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) - } - ) + !['déductible', 'non déductible'].includes( + this.cache._meta.filter as any + ) || + c.composante['impôt sur le revenu'] == this.cache._meta.filter)) + return evaluateArray(add as any, 0).call(this, { + ...node, + explanation: filter(evaluationFilter, node.explanation) + }) } export const decompose = (recurse, k, v) => { diff --git a/publicodes/source/mecanisms/condition-allof.tsx b/publicodes/source/mecanisms/condition-allof.tsx index f5330fd0c..78412b2a0 100644 --- a/publicodes/source/mecanisms/condition-allof.tsx +++ b/publicodes/source/mecanisms/condition-allof.tsx @@ -1,20 +1,20 @@ import { is, map } from 'ramda' import React from 'react' +import { evaluationFunction } from '..' import { Mecanism } from '../components/mecanisms/common' import { - evaluateNode, makeJsx, mergeAllMissing, registerEvaluationFunction } from '../evaluation' -const evaluate = (cache, situation, parsedRules, node) => { +const evaluate: evaluationFunction = function(node) { const [nodeValue, explanation] = node.explanation.reduce( ([nodeValue, explanation], node) => { if (nodeValue === false) { return [nodeValue, [...explanation, node]] } - const evaluatedNode = evaluateNode(cache, situation, parsedRules, node) + const evaluatedNode = this.evaluateNode(node) return [ nodeValue === false || nodeValue === null ? nodeValue diff --git a/publicodes/source/mecanisms/condition-oneof.tsx b/publicodes/source/mecanisms/condition-oneof.tsx index e35dc744a..63e342f02 100644 --- a/publicodes/source/mecanisms/condition-oneof.tsx +++ b/publicodes/source/mecanisms/condition-oneof.tsx @@ -1,17 +1,15 @@ import { is, map, max, mergeWith, reduce } from 'ramda' import React from 'react' +import { evaluationFunction } from '..' import { Mecanism } from '../components/mecanisms/common' import { collectNodeMissing, - evaluateNode, makeJsx, registerEvaluationFunction } from '../evaluation' -const evaluate = (cache, situation, parsedRules, node) => { - const evaluateOne = child => - evaluateNode(cache, situation, parsedRules, child) - const explanation = map(evaluateOne, node.explanation) +const evaluate: evaluationFunction = function(node) { + const explanation = node.explanation.map(child => this.evaluateNode(child)) const anyTrue = explanation.find(e => e.nodeValue === true) const anyNull = explanation.find(e => e.nodeValue === null) diff --git a/publicodes/source/mecanisms/durée.tsx b/publicodes/source/mecanisms/durée.tsx index 10c2df9e9..e88cf3fdb 100644 --- a/publicodes/source/mecanisms/durée.tsx +++ b/publicodes/source/mecanisms/durée.tsx @@ -1,9 +1,9 @@ import React from 'react' +import { evaluationFunction } from '..' import { Mecanism } from '../components/mecanisms/common' import { convertToDate, convertToString } from '../date' import { defaultNode, - evaluateNode, makeJsx, mergeAllMissing, parseObject, @@ -34,15 +34,9 @@ const objectShape = { "jusqu'à": defaultNode(todayString) } -const evaluate = (cache, situation, parsedRules, node) => { - const evaluateAttribute = evaluateNode.bind( - null, - cache, - situation, - parsedRules - ) - const from = evaluateAttribute(node.explanation.depuis) - const to = evaluateAttribute(node.explanation["jusqu'à"]) +const evaluate: evaluationFunction = function(node) { + const from = this.evaluateNode(node.explanation.depuis) + const to = this.evaluateNode(node.explanation["jusqu'à"]) let nodeValue if ([from, to].some(({ nodeValue }) => nodeValue === null)) { nodeValue = null diff --git a/publicodes/source/mecanisms/grille.ts b/publicodes/source/mecanisms/grille.ts index e17f012de..c5ba30c17 100644 --- a/publicodes/source/mecanisms/grille.ts +++ b/publicodes/source/mecanisms/grille.ts @@ -1,8 +1,8 @@ import { lensPath, over } from 'ramda' +import { evaluationFunction } from '..' import grille from '../components/mecanisms/Grille' import { defaultNode, - evaluateNode, mergeAllMissing, registerEvaluationFunction } from '../evaluation' @@ -52,15 +52,10 @@ const evaluateGrille = (tranches, evaluate) => } }) -const evaluate = ( - cache, - situation, - parsedRules, - node: ReturnType -) => { - const evaluate = evaluateNode.bind(null, cache, situation, parsedRules) - const assiette = evaluate(node.explanation.assiette) - const multiplicateur = evaluate(node.explanation.multiplicateur) +const evaluate: evaluationFunction = function(node: any) { + const evaluate = this.evaluateNode.bind(this) + const assiette = this.evaluateNode(node.explanation.assiette) + const multiplicateur = this.evaluateNode(node.explanation.multiplicateur) const temporalTranchesPlafond = liftTemporal2( (assiette, multiplicateur) => evaluatePlafondUntilActiveTranche( @@ -70,7 +65,7 @@ const evaluate = ( assiette, multiplicateur }, - cache + this.cache ), liftTemporalNode(assiette), liftTemporalNode(multiplicateur) diff --git a/publicodes/source/mecanisms/inversion.ts b/publicodes/source/mecanisms/inversion.ts index 199be857e..7c4d90671 100644 --- a/publicodes/source/mecanisms/inversion.ts +++ b/publicodes/source/mecanisms/inversion.ts @@ -1,13 +1,14 @@ +import { evaluationFunction } from '..' import InversionNumérique from '../components/mecanisms/InversionNumérique' -import { evaluateNode, registerEvaluationFunction } from '../evaluation' +import { registerEvaluationFunction } from '../evaluation' import { convertNodeToUnit } from '../nodeUnits' import uniroot from '../uniroot' import { parseUnit } from '../units' -export const evaluateInversion = (oldCache, situation, parsedRules, node) => { +export const evaluateInversion: evaluationFunction = function(node) { // TODO : take applicability into account here let inversedWith = node.explanation.inversionCandidates.find( - n => situation[n.dottedName] != undefined + n => this.parsedSituation[n.dottedName] != undefined ) if (!inversedWith) { return { @@ -21,28 +22,24 @@ export const evaluateInversion = (oldCache, situation, parsedRules, node) => { nodeValue: null } } - inversedWith = evaluateNode(oldCache, situation, parsedRules, inversedWith) - let inversionCache - function resetInversionCache() { - inversionCache = { - _meta: { ...oldCache._meta } + inversedWith = this.evaluateNode(inversedWith) + const originalCache = { ...this.cache } + const originalSituation = { ...this.parsedSituation } + const evaluateWithValue = (n: number) => { + this.cache = { + _meta: { ...originalCache._meta } } - return inversionCache + this.parsedSituation = { + ...originalSituation, + [inversedWith.dottedName]: undefined, + [node.explanation.ruleToInverse]: { + nodeValue: n, + unit: this.parsedRules[node.explanation.ruleToInverse].unit + } + } + return this.evaluateNode(inversedWith) } - const evaluateWithValue = (n: number) => - evaluateNode( - resetInversionCache(), - { - ...situation, - [inversedWith.dottedName]: undefined, - [node.explanation.ruleToInverse]: { - nodeValue: n, - unit: parsedRules[node.explanation.ruleToInverse].unit - } - }, - parsedRules, - inversedWith - ) + // si fx renvoie null pour une valeur numérique standard, disons 2000, on peut // considérer que l'inversion est impossible du fait de variables manquantes // TODO fx peut être null pour certains x, et valide pour d'autres : on peut implémenter ici le court-circuit @@ -68,13 +65,15 @@ export const evaluateInversion = (oldCache, situation, parsedRules, node) => { 1 ) if (nodeValue === undefined) { - oldCache._meta.inversionFail = true + originalCache._meta.inversionFail = true } else { // For performance reason, we transfer the inversion cache - Object.entries(inversionCache).forEach(([k, value]) => { - oldCache[k] = value + Object.entries(this.cache).forEach(([k, value]) => { + originalCache[k] = value }) } + this.cache = originalCache + this.parsedSituation = originalSituation return { ...node, nodeValue: nodeValue ?? null, @@ -103,9 +102,9 @@ export const mecanismInversion = dottedName => (recurse, v) => { jsx: InversionNumérique, category: 'mecanism', name: 'inversion numérique', - nodeKind: 'inversion numérique', + nodeKind: 'inversion', type: 'numeric' } } -registerEvaluationFunction('inversion numérique', evaluateInversion) +registerEvaluationFunction('inversion', evaluateInversion) diff --git a/publicodes/source/mecanisms/nonApplicable.tsx b/publicodes/source/mecanisms/nonApplicable.tsx index aae5472c4..c24be2d0c 100644 --- a/publicodes/source/mecanisms/nonApplicable.tsx +++ b/publicodes/source/mecanisms/nonApplicable.tsx @@ -1,8 +1,8 @@ import React from 'react' +import { evaluationFunction } from '..' import { InfixMecanism } from '../components/mecanisms/common' import { bonus, - evaluateNode, makeJsx, mergeMissing, registerEvaluationFunction @@ -19,17 +19,11 @@ function MecanismNonApplicable({ explanation }) { ) } -const evaluate = (cache, situation, parsedRules, node) => { - const evaluateAttribute = evaluateNode.bind( - null, - cache, - situation, - parsedRules - ) - const condition = evaluateAttribute(node.explanation.condition) +const evaluate: evaluationFunction = function(node) { + const condition = this.evaluateNode(node.explanation.condition) let valeur = node.explanation.valeur if (condition.nodeValue !== true) { - valeur = evaluateAttribute(valeur) + valeur = this.evaluateNode(valeur) } return { ...node, diff --git a/publicodes/source/mecanisms/one-possibility.tsx b/publicodes/source/mecanisms/one-possibility.tsx index 65e4e04b7..13c30525b 100644 --- a/publicodes/source/mecanisms/one-possibility.tsx +++ b/publicodes/source/mecanisms/one-possibility.tsx @@ -8,10 +8,7 @@ export const mecanismOnePossibility = dottedName => (recurse, v) => ({ nodeKind: 'une possibilité' }) -registerEvaluationFunction( - 'une possibilité', - (cache, situation, parsedRules, node) => ({ - ...node, - missingVariables: { [node.context]: 1 } - }) -) +registerEvaluationFunction('une possibilité', node => ({ + ...node, + missingVariables: { [node.context]: 1 } +})) diff --git a/publicodes/source/mecanisms/operation.tsx b/publicodes/source/mecanisms/operation.tsx index 2000888dc..2e09ab8e9 100644 --- a/publicodes/source/mecanisms/operation.tsx +++ b/publicodes/source/mecanisms/operation.tsx @@ -12,11 +12,11 @@ import { subtract } from 'ramda' import React from 'react' +import { evaluationFunction } from '..' import { Operation } from '../components/mecanisms/common' import { convertToDate } from '../date' import { typeWarning } from '../error' import { - evaluateNode, makeJsx, mergeAllMissing, registerEvaluationFunction @@ -50,11 +50,8 @@ const parse = (k, symbol) => (recurse, v) => { } } -const evaluate = (cache, situation, parsedRules, node) => { - const explanation = map( - node => evaluateNode(cache, situation, parsedRules, node), - node.explanation - ) +const evaluate: evaluationFunction = function(node: any) { + const explanation = map(node => this.evaluateNode(node), node.explanation) let [node1, node2] = explanation const missingVariables = mergeAllMissing([node1, node2]) @@ -70,7 +67,7 @@ const evaluate = (cache, situation, parsedRules, node) => { } } catch (e) { typeWarning( - cache._meta.contextRule, + this.cache._meta.contextRule, `Dans l'expression '${ node.operator }', la partie gauche (unité: ${serializeUnit( diff --git a/publicodes/source/mecanisms/plafond.tsx b/publicodes/source/mecanisms/plafond.tsx index 6ec6e9a63..02443e225 100644 --- a/publicodes/source/mecanisms/plafond.tsx +++ b/publicodes/source/mecanisms/plafond.tsx @@ -1,8 +1,8 @@ import React from 'react' +import { evaluationFunction } from '..' import { InfixMecanism } from '../components/mecanisms/common' import { typeWarning } from '../error' import { - evaluateNode, makeJsx, mergeAllMissing, registerEvaluationFunction @@ -26,25 +26,19 @@ function MecanismPlafond({ explanation }) { ) } -const evaluate = (cache, situation, parsedRules, node) => { - const evaluateAttribute = evaluateNode.bind( - null, - cache, - situation, - parsedRules - ) - const valeur = evaluateAttribute(node.explanation.valeur) +const evaluate: evaluationFunction = function(node) { + const valeur = this.evaluateNode(node.explanation.valeur) let nodeValue = valeur.nodeValue let plafond = node.explanation.plafond if (nodeValue !== false) { - plafond = evaluateAttribute(plafond) + plafond = this.evaluateNode(plafond) if (valeur.unit) { try { plafond = convertNodeToUnit(valeur.unit, plafond) } catch (e) { typeWarning( - cache._meta.contextRule, + this.cache._meta.contextRule, "L'unité du plafond n'est pas compatible avec celle de la valeur à encadrer", e ) diff --git a/publicodes/source/mecanisms/plancher.tsx b/publicodes/source/mecanisms/plancher.tsx index 414e8c719..0beb33c5d 100644 --- a/publicodes/source/mecanisms/plancher.tsx +++ b/publicodes/source/mecanisms/plancher.tsx @@ -1,8 +1,8 @@ import React from 'react' +import { evaluationFunction } from '..' import { InfixMecanism } from '../components/mecanisms/common' import { typeWarning } from '../error' import { - evaluateNode, makeJsx, mergeAllMissing, registerEvaluationFunction @@ -26,24 +26,18 @@ function MecanismPlancher({ explanation }) { ) } -const evaluate = (cache, situation, parsedRules, node) => { - const evaluateAttribute = evaluateNode.bind( - null, - cache, - situation, - parsedRules - ) - const valeur = evaluateAttribute(node.explanation.valeur) +const evaluate: evaluationFunction = function(node) { + const valeur = this.evaluateNode(node.explanation.valeur) let nodeValue = valeur.nodeValue let plancher = node.explanation.plancher if (nodeValue !== false) { - plancher = evaluateAttribute(plancher) + plancher = this.evaluateNode(plancher) if (valeur.unit) { try { plancher = convertNodeToUnit(valeur.unit, plancher) } catch (e) { typeWarning( - cache._meta.contextRule, + this.cache._meta.contextRule, "L'unité du plancher n'est pas compatible avec celle de la valeur à encadrer", e ) diff --git a/publicodes/source/mecanisms/product.tsx b/publicodes/source/mecanisms/product.tsx index d735ef5ec..32a09c2e0 100644 --- a/publicodes/source/mecanisms/product.tsx +++ b/publicodes/source/mecanisms/product.tsx @@ -1,3 +1,4 @@ +import { evaluationFunction } from '..' import Product from '../components/mecanisms/Product' import { typeWarning } from '../error' import { @@ -35,13 +36,18 @@ export const mecanismProduct = (recurse, v) => { } } -const effect = ({ assiette, taux, facteur, plafond }, cache) => { +const productEffect: evaluationFunction = function({ + assiette, + taux, + facteur, + plafond +}: any) { if (assiette.unit) { try { plafond = convertNodeToUnit(assiette.unit, plafond) } catch (e) { typeWarning( - cache._meta.contextRule, + this.cache._meta.contextRule, "Impossible de convertir l'unité du plafond du produit dans celle de l'assiette", e ) @@ -78,6 +84,6 @@ const effect = ({ assiette, taux, facteur, plafond }, cache) => { }) } -const evaluate = evaluateObject(objectShape, effect) +const evaluate = evaluateObject(productEffect) registerEvaluationFunction('produit', evaluate) diff --git a/publicodes/source/mecanisms/recalcul.ts b/publicodes/source/mecanisms/recalcul.ts index fd83976d7..09af180af 100644 --- a/publicodes/source/mecanisms/recalcul.ts +++ b/publicodes/source/mecanisms/recalcul.ts @@ -1,20 +1,18 @@ +import { evaluationFunction } from '..' import Recalcul from '../components/mecanisms/Recalcul' -import { - defaultNode, - evaluateNode, - registerEvaluationFunction -} from '../evaluation' +import { defaultNode, registerEvaluationFunction } from '../evaluation' +import { EvaluatedNode } from '../types' import { serializeUnit } from '../units' -const evaluateRecalcul = (cache, situation, parsedRules, node) => { - if (cache._meta.inRecalcul) { - return defaultNode(false) +const evaluateRecalcul: evaluationFunction = function(node) { + if (this.cache._meta.inRecalcul) { + return (defaultNode(false) as any) as EvaluatedNode } const amendedSituation = node.explanation.amendedSituation .map(([originRule, replacement]) => [ - evaluateNode(cache, situation, parsedRules, originRule), - evaluateNode(cache, situation, parsedRules, replacement) + this.evaluateNode(originRule), + this.evaluateNode(replacement) ]) .filter( ([originRule, replacement]) => @@ -22,25 +20,25 @@ const evaluateRecalcul = (cache, situation, parsedRules, node) => { serializeUnit(originRule.unit) !== serializeUnit(replacement.unit) ) + const originalCache = this.cache + const originalSituation = this.parsedSituation // Optimisation : no need for recalcul if situation is the same - const recalculCache = Object.keys(amendedSituation).length - ? { _meta: { ...cache._meta, inRecalcul: true } } // Create an empty cache - : cache + this.cache = Object.keys(amendedSituation).length + ? { _meta: { ...this.cache._meta, inRecalcul: true } } // Create an empty cache + : this.cache + this.parsedSituation = { + ...this.parsedSituation, + ...Object.fromEntries( + amendedSituation.map(([originRule, replacement]) => [ + originRule.dottedName, + replacement + ]) + ) + } - const evaluatedNode = evaluateNode( - recalculCache, - { - ...situation, - ...Object.fromEntries( - amendedSituation.map(([originRule, replacement]) => [ - originRule.dottedName, - replacement - ]) - ) - }, - parsedRules, - node.explanation.recalcul - ) + const evaluatedNode = this.evaluateNode(node.explanation.recalcul) + this.cache = originalCache + this.parsedSituation = originalSituation return { ...node, nodeValue: evaluatedNode.nodeValue, diff --git a/publicodes/source/mecanisms/reduction.ts b/publicodes/source/mecanisms/reduction.ts index 8b3c93d1b..d50f27a80 100644 --- a/publicodes/source/mecanisms/reduction.ts +++ b/publicodes/source/mecanisms/reduction.ts @@ -16,51 +16,49 @@ const objectShape = { plafond: defaultNode(Infinity) } -const evaluate = evaluateObject( - objectShape, - ({ assiette, abattement, plafond }, cache) => { - const assietteValue = assiette.nodeValue - if (assietteValue == null) return { nodeValue: null } - if (assiette.unit) { - try { - plafond = convertNodeToUnit(assiette.unit, plafond) - if (serializeUnit(abattement.unit) !== '%') { - abattement = convertNodeToUnit(assiette.unit, abattement) - } - } catch (e) { - typeWarning( - cache._meta.contextRule, - "Impossible de convertir les unités de l'allègement entre elles", - e - ) - } - } - const nodeValue = abattement - ? abattement.nodeValue == null - ? assietteValue === 0 - ? 0 - : null - : serializeUnit(abattement.unit) === '%' - ? max( - 0, - assietteValue - - min( - plafond.nodeValue, - (abattement.nodeValue / 100) * assietteValue - ) - ) - : max(0, assietteValue - min(plafond.nodeValue, abattement.nodeValue)) - : assietteValue - return { - nodeValue, - unit: assiette.unit, - explanation: { - plafond, - abattement +const evaluate = evaluateObject(function({ + assiette, + abattement, + plafond +}: any) { + const assietteValue = assiette.nodeValue + if (assietteValue == null) return { nodeValue: null } + if (assiette.unit) { + try { + plafond = convertNodeToUnit(assiette.unit, plafond) + if (serializeUnit(abattement.unit) !== '%') { + abattement = convertNodeToUnit(assiette.unit, abattement) } + } catch (e) { + typeWarning( + this.cache._meta.contextRule, + "Impossible de convertir les unités de l'allègement entre elles", + e + ) } } -) + const nodeValue = abattement + ? abattement.nodeValue == null + ? assietteValue === 0 + ? 0 + : null + : serializeUnit(abattement.unit) === '%' + ? max( + 0, + assietteValue - + min(plafond.nodeValue, (abattement.nodeValue / 100) * assietteValue) + ) + : max(0, assietteValue - min(plafond.nodeValue, abattement.nodeValue)) + : assietteValue + return { + nodeValue, + unit: assiette.unit, + explanation: { + plafond, + abattement + } + } +}) export const mecanismReduction = (recurse, v) => { const explanation = parseObject(recurse, objectShape, v) diff --git a/publicodes/source/mecanisms/régularisation.ts b/publicodes/source/mecanisms/régularisation.ts index 8e4eb8d89..211a5b128 100644 --- a/publicodes/source/mecanisms/régularisation.ts +++ b/publicodes/source/mecanisms/régularisation.ts @@ -1,7 +1,8 @@ import { map } from 'ramda' +import { evaluationFunction } from '..' import { convertToString, getYear } from '../date' import { evaluationError } from '../error' -import { evaluateNode, registerEvaluationFunction } from '../evaluation' +import { registerEvaluationFunction } from '../evaluation' import { createTemporalEvaluation, groupByYear, @@ -84,19 +85,19 @@ function getMonthlyCumulatedValuesOverYear( return cumulatedPeriods } -function evaluate(cache, situation, parsedRules, node) { - const evaluate = evaluateNode.bind(null, cache, situation, parsedRules) - - function recalculWith(newSituation, node) { - return evaluateNode( - { _meta: cache._meta }, - { ...situation, ...newSituation }, - parsedRules, - node - ) +const evaluate: evaluationFunction = function(node) { + const recalculWith = (newSituation, node) => { + const originalCache = this.cache + const originalSituation = this.parsedSituation + this.cache = { _meta: originalCache._meta } + this.parsedSituation = { ...originalSituation, ...newSituation } + const res = this.evaluateNode(node) + this.cache = originalCache + this.parsedSituation = originalSituation + return res } - function regulariseYear(temporalEvaluation: Temporal>) { + const regulariseYear = (temporalEvaluation: Temporal>) => { if (temporalEvaluation.filter(({ value }) => value !== false).length <= 1) { return temporalEvaluation } @@ -107,10 +108,10 @@ function evaluate(cache, situation, parsedRules, node) { value: Record }>).reduce>>>( (acc, { dottedName, value }) => { - const evaluation = evaluate(value) + const evaluation = this.evaluateNode(value) if (!evaluation.unit.denominators.some(unit => unit === 'mois')) { evaluationError( - cache._meta.contextRule, + this.cache._meta.contextRule, `Dans le mécanisme régularisation, la valeur cumulée '${dottedName}' n'est pas une variable numérique définie sur le mois` ) } @@ -147,7 +148,7 @@ function evaluate(cache, situation, parsedRules, node) { return temporalRégularisée as Temporal> } - const evaluation = evaluate(node.explanation.rule) + const evaluation = this.evaluateNode(node.explanation.rule) const temporalValue = evaluation.temporalValue const evaluationWithRegularisation = groupByYear( temporalValue as Temporal> diff --git a/publicodes/source/mecanisms/sum.tsx b/publicodes/source/mecanisms/sum.tsx index 8a040cb98..fab095993 100644 --- a/publicodes/source/mecanisms/sum.tsx +++ b/publicodes/source/mecanisms/sum.tsx @@ -3,7 +3,7 @@ import { evaluateArray, registerEvaluationFunction } from '../evaluation' import { inferUnit } from '../units' const evaluate = evaluateArray( - (x, y) => (x === false && y === false ? false : x + y), + (x: any, y: any) => (x === false && y === false ? false : x + y), false ) diff --git a/publicodes/source/mecanisms/synchronisation.tsx b/publicodes/source/mecanisms/synchronisation.tsx index ce33099dc..698e7d74b 100644 --- a/publicodes/source/mecanisms/synchronisation.tsx +++ b/publicodes/source/mecanisms/synchronisation.tsx @@ -1,15 +1,11 @@ import { path } from 'ramda' import React from 'react' +import { evaluationFunction } from '..' import { RuleLinkWithContext } from '../components/RuleLink' -import { evaluateNode, registerEvaluationFunction } from '../evaluation' +import { registerEvaluationFunction } from '../evaluation' -const evaluate = (cache, situation, parsedRules, node) => { - const APIExplanation = evaluateNode( - cache, - situation, - parsedRules, - node.explanation.API - ) +const evaluate: evaluationFunction = function(node: any) { + const APIExplanation = this.evaluateNode(node.explanation.API) const valuePath = node.explanation.chemin.split(' . ') const nodeValue = APIExplanation.nodeValue == null diff --git a/publicodes/source/mecanisms/tauxProgressif.ts b/publicodes/source/mecanisms/tauxProgressif.ts index 47b63f0f9..0728d6382 100644 --- a/publicodes/source/mecanisms/tauxProgressif.ts +++ b/publicodes/source/mecanisms/tauxProgressif.ts @@ -1,7 +1,7 @@ +import { evaluationFunction } from '..' import tauxProgressif from '../components/mecanisms/TauxProgressif' import { defaultNode, - evaluateNode, mergeAllMissing, registerEvaluationFunction } from '../evaluation' @@ -29,15 +29,10 @@ export default function parse(parse, v) { } } -const evaluate = ( - cache, - situation, - parsedRules, - node: ReturnType -) => { - const evaluate = evaluateNode.bind(null, cache, situation, parsedRules) - const assiette = evaluate(node.explanation.assiette) - const multiplicateur = evaluate(node.explanation.multiplicateur) +const evaluate: evaluationFunction = function(node: any) { + const evaluate = this.evaluateNode.bind(this) + const assiette = this.evaluateNode(node.explanation.assiette) + const multiplicateur = this.evaluateNode(node.explanation.multiplicateur) const tranches = evaluatePlafondUntilActiveTranche( evaluate, { @@ -45,7 +40,7 @@ const evaluate = ( assiette, multiplicateur }, - cache + this.cache ) const evaluatedNode = { diff --git a/publicodes/source/mecanisms/variableTemporelle.ts b/publicodes/source/mecanisms/variableTemporelle.ts index 8bec7db94..eba03dd03 100644 --- a/publicodes/source/mecanisms/variableTemporelle.ts +++ b/publicodes/source/mecanisms/variableTemporelle.ts @@ -1,4 +1,5 @@ -import { evaluateNode, registerEvaluationFunction } from '../evaluation' +import { evaluationFunction } from '..' +import { registerEvaluationFunction } from '../evaluation' import { createTemporalEvaluation, narrowTemporalValue, @@ -6,26 +7,14 @@ import { temporalAverage } from '../temporal' -function evaluate( - cache: any, - situation: any, - parsedRules: any, - node: ReturnType -) { - const evaluateAttribute = evaluateNode.bind( - null, - cache, - situation, - parsedRules - ) - +const evaluate: evaluationFunction = function(node: any) { const start = node.explanation.period.start && - evaluateAttribute(node.explanation.period.start) + this.evaluateNode(node.explanation.period.start) const end = node.explanation.period.end && - evaluateAttribute(node.explanation.period.end) - const value = evaluateAttribute(node.explanation.value) + this.evaluateNode(node.explanation.period.end) + const value = this.evaluateNode(node.explanation.value) const period = { start: start?.nodeValue || null, end: end?.nodeValue || null diff --git a/publicodes/source/mecanisms/variations.ts b/publicodes/source/mecanisms/variations.ts index dbb5e44f2..a048f98bb 100644 --- a/publicodes/source/mecanisms/variations.ts +++ b/publicodes/source/mecanisms/variations.ts @@ -1,12 +1,8 @@ import { or } from 'ramda' +import { evaluationFunction } from '..' import Variations from '../components/mecanisms/Variations' import { typeWarning } from '../error' -import { - bonus, - defaultNode, - evaluateNode, - registerEvaluationFunction -} from '../evaluation' +import { bonus, defaultNode, registerEvaluationFunction } from '../evaluation' import { convertNodeToUnit } from '../nodeUnits' import { liftTemporal2, @@ -82,14 +78,7 @@ const devariateExplanation = ( return explanation } -function evaluate( - cache, - situation, - parsedRules, - node: ReturnType -) { - const evaluate = evaluateNode.bind(null, cache, situation, parsedRules) - +const evaluate: evaluationFunction = function(node: any) { const [temporalValue, explanation, unit] = node.explanation.reduce( ( [evaluation, explanations, unit, previousConditions], @@ -108,7 +97,7 @@ function evaluate( previousConditions ] } - const evaluatedCondition = evaluate(condition) + const evaluatedCondition = this.evaluateNode(condition) const currentCondition = liftTemporal2( (previousCond, currentCond) => previousCond === null ? previousCond : !previousCond && currentCond, @@ -132,13 +121,13 @@ function evaluate( previousConditions ] } - let evaluatedConsequence = evaluate(consequence) + let evaluatedConsequence = this.evaluateNode(consequence) try { evaluatedConsequence = convertNodeToUnit(unit, evaluatedConsequence) } catch (e) { typeWarning( - cache._meta.contexRule, + this.cache._meta.contextRule, `L'unité de la branche n° ${i + 1} du mécanisme 'variations' n'est pas compatible avec celle d'une branche précédente`, e diff --git a/publicodes/source/parse.tsx b/publicodes/source/parse.tsx index f87c5bd51..b83db32f6 100644 --- a/publicodes/source/parse.tsx +++ b/publicodes/source/parse.tsx @@ -45,16 +45,14 @@ Vérifiez que tous les champs à droite des deux points sont remplis` syntaxError( rule.dottedName, ` -Les valeure booléenes true / false ne sont acceptée. +Les valeurs booléennes true / false ne sont acceptées. Utilisez leur contrepartie française : 'oui' / 'non'` ) } const node = typeof rawNode === 'object' ? rawNode : parseExpression(rule, '' + rawNode) - const parsedNode = parseMecanism(rules, rule, parsedRules)(node) - parsedNode.evaluate = parsedNode.evaluate ?? ((_, __, ___, node) => node) - return parsedNode + return parseMecanism(rules, rule, parsedRules)(node) } const compiledGrammar = Grammar.fromCompiled(grammar) diff --git a/publicodes/source/types/index.ts b/publicodes/source/types/index.ts index 645b8e968..94ef9b148 100644 --- a/publicodes/source/types/index.ts +++ b/publicodes/source/types/index.ts @@ -27,6 +27,7 @@ export type ParsedRule = Rule & { dottedName: Name name: string title: string + nodeKind: string parentDependencies: Array rawRule: Rule unit?: Unit @@ -37,7 +38,6 @@ export type ParsedRule = Rule & { API?: string icons?: string formule?: any - evaluate?: () => EvaluatedRule explanation?: any isDisabledBy: Array replacedBy: Array<{ @@ -74,10 +74,11 @@ export type EvaluatedNode< T extends Types = Types > = { nodeValue: Evaluation - explanation?: Record + explanation: Record isDefault?: boolean jsx: React.FunctionComponent category?: string + dottedName: Names missingVariables: Partial> } & (T extends number ? {