⚙️ Ajout d'un type de nœud serialisable

Ce commit parachève la sortie de l'ensemble des functions "evaluate" de
l'AST et ajoute un "nodeKind" sur chaque nœud afin de les associer à la
bonne function d'évaluation.

L'API pour les mécanismes pourra être améliorée afin de ne pas appeler
`registerEvaluationFunction` sur chaque mécanisme mais en standardisant
l'interface exportée par les mécanismes, par exemple

  export { name, parse, evaluate, render }

Par ailleurs il devrait être facile de sortir les fonctions `jsx` en se
basant sur les mêmes "nodeKind".

Enfin, il faudra nettoyer l'AST pour supprimer les attributs inutilisés
et ajouter du typage fort.
pull/1193/head
Maxime Quandalle 2020-10-19 17:25:12 +02:00
parent 33ff99bc30
commit c66e529fb7
34 changed files with 824 additions and 653 deletions

View File

@ -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
}
}

View File

@ -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<string, unknown>
) => {
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<EvaluatedNode['missingVariables']> = []
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]
}

View File

@ -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 }
}

View File

@ -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) => (
<span className="value">{nodeValue}</span>
),
isDefault: true,
nodeKind: 'defaultNode'
})
export const defaultNode = (nodeValue: EvaluatedNode['nodeValue']) => {
const defaultNode = {
nodeValue,
// eslint-disable-next-line
jsx: ({ nodeValue }: EvaluatedNode) => (
<span className="value">{nodeValue}</span>
),
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
}

View File

@ -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)

View File

@ -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)

View File

@ -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)

View File

@ -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<Record<string, unknown>>('composantes', v)
const explanation = v.composantes.map(c => ({
...recurse(
objOf(k, {
...subProps,
...dissoc<Record<string, unknown>>('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)

View File

@ -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)

View File

@ -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)

View File

@ -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)

View File

@ -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)

View File

@ -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)

View File

@ -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 }) => (
<Mecanism name="le maximum de" value={nodeValue} unit={unit}>
<ul>
@ -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)

View File

@ -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 }) => (
<Mecanism name="le minimum de" value={nodeValue} unit={unit}>
<ul>
@ -19,12 +22,16 @@ export const mecanismMin = (recurse, v) => {
</Mecanism>
)
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)

View File

@ -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)

View File

@ -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 }
})
)

View File

@ -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

View File

@ -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)

View File

@ -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)

View File

@ -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)

View File

@ -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)

View File

@ -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)

View File

@ -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)

View File

@ -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<Name extends string>(parse, v) {
}) as Array<{ dottedName: Name; value: Record<string, unknown> }>
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)

View File

@ -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)

View File

@ -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 (
<p>
@ -46,6 +45,9 @@ export const mecanismSynchronisation = (recurse, v) => {
)
},
category: 'mecanism',
name: 'synchronisation'
name: 'synchronisation',
nodeKind: 'synchronisation'
}
}
registerEvaluationFunction('synchronisation', evaluate)

View File

@ -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)

View File

@ -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)
)
}
}

View File

@ -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)

View File

@ -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)

View File

@ -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) => (

View File

@ -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<EvaluatedNode['missingVariables']> = []
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<string, unknown>
) => {
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<string> }
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
}
}

View File

@ -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<Names extends string>(
) : 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<Names extends string>(
// "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<Names extends string>(
const jsx = ({ explanation }) => makeJsx(explanation)
return {
evaluate: evaluateFormula,
nodeKind: 'formula',
jsx,
category: 'ruleProp',
rulePropType: 'formula',
@ -154,7 +152,7 @@ export default function<Names extends string>(
// 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<Names extends string>(
replacedBy: []
}
parsedRules[dottedName]['rendu non applicable'] = {
evaluate: evaluateDisabledBy,
nodeKind: 'disabledBy',
jsx: ({ explanation: { isDisabledBy } }) => {
return (
isDisabledBy.length > 0 && (
@ -215,54 +213,6 @@ export default function<Names extends string>(
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,