🔥 Supprime Ramda du moteur

pull/1335/head
Maxime Quandalle 2020-12-31 12:29:22 +01:00
parent 61729eb334
commit 94a3714b79
22 changed files with 151 additions and 164 deletions

View File

@ -1194,7 +1194,7 @@ contrat salarié . rémunération . brut de base:
unité: €/mois
suggestions:
salaire médian: 2300 €/mois
SMIC: contrat salarié . SMIC contractuel
SMIC: SMIC contractuel
formule:
inversion numérique:
question: Quel est le salaire ?

View File

@ -31,7 +31,6 @@
"dependencies": {
"moo": "^0.5.1",
"nearley": "^2.19.2",
"ramda": "^0.27.0",
"yaml": "^1.9.2"
},
"scripts": {

View File

@ -1,5 +1,4 @@
import graphlib from '@dagrejs/graphlib'
import * as R from 'ramda'
import parsePublicodes from '../parsePublicodes'
import { RuleNode } from '../rule'
import { reduceAST } from './index'
@ -10,9 +9,10 @@ type GraphCyclesWithDependencies = Array<RulesDependencies>
function buildRulesDependencies(
parsedRules: Record<string, RuleNode>
): RulesDependencies {
const uniq = <T>(arr: Array<T>): Array<T> => [...new Set(arr)]
return Object.entries(parsedRules).map(([name, node]) => [
name,
R.uniq(buildRuleDependancies(node)),
uniq(buildRuleDependancies(node)),
])
}
@ -79,7 +79,7 @@ export function cyclicDependencies(
const rulesDependencies = buildRulesDependencies(parsedRules)
const dependenciesGraph = buildDependenciesGraph(rulesDependencies)
const cycles = (graphlib as any).alg.findCycles(dependenciesGraph)
const rulesDependenciesObject = R.fromPairs(rulesDependencies)
const rulesDependenciesObject = Object.fromEntries(rulesDependencies)
return cycles.map((cycle) => {
const c = cycle.reverse()

View File

@ -1,4 +1,3 @@
import { mapObjIndexed } from 'ramda'
import { InternalError } from '../error'
import { TrancheNodes } from '../mecanisms/trancheUtils'
import { ReplacementRule } from '../replacement'
@ -151,7 +150,9 @@ const traverseASTNode: TraverseFunction<NodeKind> = (fn, node) => {
const traverseRuleNode: TraverseFunction<'rule'> = (fn, node) => ({
...node,
replacements: node.replacements.map(fn) as Array<ReplacementRule>,
suggestions: mapObjIndexed(fn, node.suggestions),
suggestions: Object.fromEntries(
Object.entries(node.suggestions).map(([key, value]) => [key, fn(value)])
),
explanation: {
parent: node.explanation.parent && fn(node.explanation.parent),
valeur: fn(node.explanation.valeur),

View File

@ -1,13 +1,3 @@
import {
add,
evolve,
fromPairs,
keys,
map,
mapObjIndexed,
mergeWith,
reduce,
} from 'ramda'
import Engine, { EvaluationFunction } from '.'
import {
ASTNode,
@ -34,12 +24,20 @@ export const collectNodeMissing = (
): Record<string, number> =>
'missingVariables' in node ? node.missingVariables : {}
export const bonus = (missings, hasCondition = true) =>
hasCondition ? map((x) => x + 0.0001, missings || {}) : missings
export const bonus = (missings: Record<string, number> = {}) =>
Object.fromEntries(
Object.entries(missings).map(([key, value]) => [key, value + 0.0001])
)
export const mergeMissing = (
left: Record<string, number> | undefined,
right: Record<string, number> | undefined
): Record<string, number> => mergeWith(add, left || {}, right || {})
left: Record<string, number> | undefined = {},
right: Record<string, number> | undefined = {}
): Record<string, number> =>
Object.fromEntries(
[...Object.keys(left), ...Object.keys(right)].map((key) => [
key,
(left[key] ?? 0) + (right[key] ?? 0),
])
)
export const mergeAllMissing = (missings: Array<EvaluatedNode | ASTNode>) =>
missings.map(collectNodeMissing).reduce(mergeMissing, {})
@ -66,8 +64,8 @@ function convertNodesToSameUnit(nodes, contextRule, mecanismName) {
}
export const evaluateArray: <NodeName extends NodeKind>(
reducer: Parameters<typeof reduce>[0],
start: Parameters<typeof reduce>[1]
reducer,
start
) => EvaluationFunction<NodeName> = (reducer, start) =>
function (node: any) {
const evaluate = this.evaluateNode.bind(this)
@ -87,7 +85,7 @@ export const evaluateArray: <NodeName extends NodeKind>(
if (values.some((value) => value === null)) {
return null
}
return reduce(reducer, start, values)
return values.reduce(reducer, start)
}, temporalValues)
const baseEvaluation = {
@ -119,27 +117,30 @@ export const defaultNode = (nodeValue: Evaluation) =>
} as ConstantNode)
export const parseObject = (objectShape, value, context) => {
const recurseOne = (key) => (defaultValue) => {
if (value[key] == null && !defaultValue)
throw new Error(
`Il manque une clé '${key}' dans ${JSON.stringify(value)} `
)
return value[key] != null ? parse(value[key], context) : defaultValue
}
const transforms = fromPairs(
map((k) => [k, recurseOne(k)], keys(objectShape)) as any
return Object.fromEntries(
Object.entries(objectShape).map(([key, defaultValue]) => {
if (value[key] == null && !defaultValue) {
throw new Error(
`Il manque une clé '${key}' dans ${JSON.stringify(value)} `
)
}
const parsedValue =
value[key] != null ? parse(value[key], context) : defaultValue
return [key, parsedValue]
})
)
return evolve(transforms as any, objectShape)
}
export function evaluateObject<NodeName extends NodeKind>(
effet: (this: Engine, explanations: any) => any
) {
return function (node) {
const evaluate = this.evaluateNode.bind(this)
const evaluations: Record<string, EvaluatedNode> = mapObjIndexed(
evaluate as any,
(node as any).explanation
const evaluations = Object.fromEntries(
Object.entries((node as any).explanation).map(([key, value]) => [
key,
this.evaluateNode(value as any),
])
)
const temporalExplanations = mapTemporal(
Object.fromEntries,

View File

@ -1,12 +1,6 @@
import { memoizeWith } from 'ramda'
import { Evaluation, Unit } from './AST/types'
import { formatUnit, serializeUnit } from './units'
const NumberFormat = memoizeWith(
(...args) => JSON.stringify(args),
Intl.NumberFormat
)
export const numberFormatter = ({
style,
maximumFractionDigits = 2,
@ -27,7 +21,7 @@ export const numberFormatter = ({
!Number.isInteger(value)
? 2
: minimumFractionDigits
return NumberFormat(language, {
return Intl.NumberFormat(language, {
style,
currency: 'EUR',
maximumFractionDigits,

View File

@ -1,5 +1,4 @@
/* eslint-disable @typescript-eslint/ban-types */
import { compose, mapObjIndexed } from 'ramda'
import { ASTNode, EvaluatedNode, NodeKind } from './AST/types'
import { evaluationFunctions } from './evaluationFunctions'
import { simplifyNodeUnit } from './nodeUnits'
@ -104,24 +103,28 @@ export default class Engine<Name extends string = string> {
situation: Partial<Record<Name, string | number | object | ASTNode>> = {}
) {
this.resetCache()
this.parsedSituation = mapObjIndexed((value, key) => {
if (value && typeof value === 'object' && 'nodeKind' in value) {
return value as ASTNode
}
return compose(
inlineReplacements(this.replacements),
disambiguateReference(this.parsedRules)
)(
parse(value, {
dottedName: `situation [${key}]`,
parsedRules: {},
options: this.options,
})
)
}, situation)
this.parsedSituation = Object.fromEntries(
Object.entries(situation).map(([key, value]) => {
const parsedValue =
value && typeof value === 'object' && 'nodeKind' in value
? (value as ASTNode)
: this.parse(value, {
dottedName: `situation [${key}]`,
parsedRules: {},
options: this.options,
})
return [key, parsedValue]
})
)
return this
}
private parse(...args: Parameters<typeof parse>) {
return inlineReplacements(this.replacements)(
disambiguateReference(this.parsedRules)(parse(...args))
)
}
evaluate(expression: string | Object): EvaluatedNode {
/*
TODO
@ -134,16 +137,11 @@ export default class Engine<Name extends string = string> {
originalWarn(warning)
}
const result = this.evaluateNode(
compose(
inlineReplacements(this.replacements),
disambiguateReference(this.parsedRules)
)(
parse(expression, {
dottedName: "evaluation'''",
parsedRules: {},
options: this.options,
})
)
this.parse(expression, {
dottedName: "evaluation'''",
parsedRules: {},
options: this.options,
})
)
console.warn = originalWarn
return result

View File

@ -1,4 +1,3 @@
import { is } from 'ramda'
import { EvaluationFunction } from '..'
import { ASTNode } from '../AST/types'
import { mergeAllMissing } from '../evaluation'
@ -38,7 +37,7 @@ const evaluate: EvaluationFunction<'toutes ces conditions'> = function (node) {
}
export const mecanismAllOf = (v, context) => {
if (!is(Array, v)) throw new Error('should be array')
if (!Array.isArray(v)) throw new Error('should be array')
const explanation = v.map((node) => parse(node, context))
return {

View File

@ -1,4 +1,3 @@
import { is } from 'ramda'
import { EvaluationFunction } from '..'
import { ASTNode, EvaluatedNode, Evaluation } from '../AST/types'
import { mergeMissing } from '../evaluation'
@ -57,7 +56,7 @@ const evaluate: EvaluationFunction<'une de ces conditions'> = function (node) {
}
export const mecanismOneOf = (v, context) => {
if (!is(Array, v)) throw new Error('should be array')
if (!Array.isArray(v)) throw new Error('should be array')
const explanation = v.map((node) => parse(node, context))
return {

View File

@ -1,4 +1,3 @@
import { min } from 'ramda'
import { ASTNode } from '../AST/types'
import { evaluateArray } from '../evaluation'
import { registerEvaluationFunction } from '../evaluationFunctions'
@ -17,6 +16,6 @@ export const mecanismMin = (v, context) => {
} as MinNode
}
const evaluate = evaluateArray<'minimum'>(min, Infinity)
const evaluate = evaluateArray<'minimum'>((a, b) => Math.min(a, b), Infinity)
registerEvaluationFunction('minimum', evaluate)

View File

@ -1,4 +1,3 @@
import { equals, fromPairs } from 'ramda'
import { EvaluationFunction } from '..'
import { ASTNode } from '../AST/types'
import { convertToDate } from '../date'
@ -20,8 +19,8 @@ const knownOperations = {
'<=': [(a, b) => a <= b, '≤'],
'>': [(a, b) => a > b],
'>=': [(a, b) => a >= b, '≥'],
'=': [(a, b) => equals(a, b)],
'!=': [(a, b) => !equals(a, b), '≠'],
'=': [(a, b) => a === b],
'!=': [(a, b) => a !== b, '≠'],
} as const
export type OperationNode = {
@ -123,7 +122,7 @@ const evaluate: EvaluationFunction<'operation'> = function (node) {
registerEvaluationFunction('operation', evaluate)
const operationDispatch = fromPairs(
const operationDispatch = Object.fromEntries(
Object.entries(knownOperations).map(([k, [f, symbol]]) => [
k,
parseOperation(k, symbol),

View File

@ -1,4 +1,3 @@
import { max, min } from 'ramda'
import { typeWarning } from '../error'
import { defaultNode, evaluateObject, parseObject } from '../evaluation'
import { registerEvaluationFunction } from '../evaluationFunctions'
@ -48,12 +47,18 @@ const evaluate = evaluateObject<'allègement'>(function ({
? 0
: null
: serializeUnit(abattement.unit) === '%'
? max(
? Math.max(
0,
assietteValue -
min(plafond.nodeValue, (abattement.nodeValue / 100) * assietteValue)
Math.min(
plafond.nodeValue,
(abattement.nodeValue / 100) * assietteValue
)
)
: Math.max(
0,
assietteValue - Math.min(plafond.nodeValue, abattement.nodeValue)
)
: max(0, assietteValue - min(plafond.nodeValue, abattement.nodeValue))
: assietteValue
return {
nodeValue,

View File

@ -1,4 +1,3 @@
import { isEmpty } from 'ramda'
import { ASTNode, EvaluatedNode } from '../AST/types'
import { mergeAllMissing } from '../evaluation'
import { registerEvaluationFunction } from '../evaluationFunctions'
@ -50,7 +49,7 @@ registerEvaluationFunction(parseSituation.nom, function evaluate(node) {
...node,
nodeValue: valeur.nodeValue,
missingVariables:
isEmpty(missingVariables) && valeur.nodeValue === null
Object.keys(missingVariables).length === 0 && valeur.nodeValue === null
? { [situationKey]: 1 }
: missingVariables,
...(unit !== undefined && { unit }),

View File

@ -1,4 +1,3 @@
import { path } from 'ramda'
import { EvaluationFunction } from '..'
import { ASTNode } from '../AST/types'
import { registerEvaluationFunction } from '../evaluationFunctions'
@ -15,14 +14,14 @@ export type SynchronisationNode = {
const evaluate: EvaluationFunction<'synchronisation'> = function (node: any) {
const data = this.evaluateNode(node.explanation.data)
const valuePath = node.explanation.chemin.split(' . ')
const nodeValue =
data.nodeValue == null ? null : path(valuePath, data.nodeValue)
const path = (obj) => valuePath.reduce((res, prop) => res[prop], obj)
const nodeValue = data.nodeValue == null ? null : path(data.nodeValue)
// If the API gave a non null value, then some of its props may be null (the
// API can be composed of multiple API, some failing). Then this prop will be
// set to the default value defined in the API's rule
const safeNodeValue =
nodeValue == null && data.nodeValue != null
? path(valuePath, data.explanation.defaultValue)
? path(data.explanation.defaultValue)
: nodeValue
const missingVariables = {
...data.missingVariables,

View File

@ -1,4 +1,3 @@
import { evolve } from 'ramda'
import { ASTNode, Evaluation } from '../AST/types'
import { evaluationError, typeWarning } from '../error'
import { mergeAllMissing } from '../evaluation'
@ -18,13 +17,14 @@ export const parseTranches = (tranches, context): TrancheNodes => {
}
return { ...t, plafond: t.plafond ?? Infinity }
})
.map(
evolve({
taux: (node) => parse(node, context),
montant: (node) => parse(node, context),
plafond: (node) => parse(node, context),
})
)
.map((node) => ({
...node,
...(node.taux !== undefined ? { taux: parse(node.taux, context) } : {}),
...(node.montant !== undefined
? { montant: parse(node.montant, context) }
: {}),
plafond: parse(node.plafond, context),
}))
}
export function evaluatePlafondUntilActiveTranche(

View File

@ -1,4 +1,3 @@
import { or } from 'ramda'
import { EvaluationFunction } from '..'
import { ASTNode, Unit } from '../AST/types'
import { typeWarning } from '../error'
@ -101,8 +100,7 @@ const evaluate: EvaluationFunction<'variations'> = function (node) {
pureTemporal(evaluatedCondition.nodeValue)
)
evaluatedCondition.missingVariables = bonus(
evaluatedCondition.missingVariables,
true
evaluatedCondition.missingVariables
)
const currentConditionAlwaysFalse = !sometime(
(x) => x !== false,
@ -136,6 +134,7 @@ const evaluate: EvaluationFunction<'variations'> = function (node) {
evaluatedConsequence.temporalValue ??
pureTemporal(evaluatedConsequence.nodeValue)
)
const or = (a, b) => a || b
return [
liftTemporal2(or, evaluation, currentValue),
[

View File

@ -1,5 +1,4 @@
import { Grammar, Parser } from 'nearley'
import { isEmpty } from 'ramda'
import { ASTNode } from './AST/types'
import { EngineError, syntaxError } from './error'
import grammar from './grammar.ne'
@ -109,7 +108,7 @@ Cela vient probablement d'une erreur dans l'indentation
`
)
}
if (isEmpty(rawNode)) {
if (keys.length === 0) {
return { nodeKind: 'constant', nodeValue: null }
}

View File

@ -1,4 +1,3 @@
import { partial } from 'ramda'
import yaml from 'yaml'
import { ParsedRules } from '.'
import { transformAST, traverseParsedRules } from './AST'

View File

@ -1,4 +1,3 @@
import { groupBy } from 'ramda'
import { transformAST } from './AST'
import { ASTNode } from './AST/types'
import { InternalError, warning } from './error'
@ -89,15 +88,15 @@ export function parseRendNonApplicable(
export function getReplacements(
parsedRules: Record<string, RuleNode>
): Record<string, Array<ReplacementRule>> {
return groupBy(
(r: ReplacementRule) => {
return Object.values(parsedRules)
.flatMap((rule) => rule.replacements)
.reduce((acc, r: ReplacementRule) => {
if (!r.replacedReference.dottedName) {
throw new InternalError(r)
}
return r.replacedReference.dottedName
},
Object.values(parsedRules).flatMap((rule) => rule.replacements)
)
const key = r.replacedReference.dottedName
return { ...acc, [key]: [...(acc[key] ?? []), r] }
}, {})
}
export function inlineReplacements(

View File

@ -1,4 +1,3 @@
import { filter, mapObjIndexed, pick } from 'ramda'
import { ASTNode, EvaluatedNode } from './AST/types'
import { bonus, mergeMissing } from './evaluation'
import { registerEvaluationFunction } from './evaluationFunctions'
@ -79,7 +78,9 @@ export default function parseRule(
}
const ruleValue = {
...pick(mecanismKeys, rawRule),
...Object.fromEntries(
Object.entries(rawRule).filter(([key]) => mecanismKeys.includes(key))
),
...('formule' in rawRule && { valeur: rawRule.formule }),
'nom dans la situation': dottedName,
}
@ -91,22 +92,24 @@ export default function parseRule(
valeur: parse(ruleValue, ruleContext),
parent: !!parent && parse(parent, context),
}
context.parsedRules[dottedName] = filter(Boolean, {
context.parsedRules[dottedName] = {
dottedName,
replacements: [
...parseRendNonApplicable(rawRule['rend non applicable'], ruleContext),
...parseReplacements(rawRule.remplace, ruleContext),
],
title: ruleTitle,
suggestions: mapObjIndexed(
(node) => parse(node, ruleContext),
rawRule.suggestions ?? {}
suggestions: Object.fromEntries(
Object.entries(rawRule.suggestions ?? {}).map(([name, node]) => [
name,
parse(node, ruleContext),
])
),
nodeKind: 'rule',
explanation,
rawNode: rawRule,
virtualRule: !!context.dottedName,
}) as RuleNode
} as RuleNode
// We return the parsedReference
return parse(rawRule.nom, context) as ReferenceNode

View File

@ -1,10 +1,9 @@
import { last, pipe, range, take } from 'ramda'
import { syntaxError } from './error'
import { RuleNode } from './rule'
const splitName = (str: string) => str.split(' . ')
const joinName = (strs) => strs.join(' . ')
export const nameLeaf = pipe<string, string[], string>(splitName, last)
const joinName = (strs: Array<string>) => strs.join(' . ')
export const nameLeaf = (name: string) => splitName(name).slice(-1)?.[0]
export const encodeRuleName = (name) =>
name
?.replace(/\s\.\s/g, '/')
@ -15,13 +14,12 @@ export const decodeRuleName = (name) =>
.replace(/\//g, ' . ')
.replace(/-/g, ' ')
.replace(/\u2011/g, '-')
export function ruleParents<Names extends string>(
dottedName: Names
): Array<Names> {
export function ruleParents(dottedName: string): Array<string> {
const fragments = splitName(dottedName) // dottedName ex. [CDD . événements . rupture]
return range(1, fragments.length)
.map((nbEl) => take(nbEl, fragments))
.map(joinName) // -> [ [CDD . événements . rupture], [CDD . événements], [CDD
return Array(fragments.length - 1)
.fill(0)
.map((f, i) => fragments.slice(0, i + 1))
.map(joinName)
.reverse()
}

View File

@ -1,16 +1,3 @@
import {
countBy,
equals,
flatten,
isEmpty,
keys,
map,
pipe,
remove,
uniq,
unnest,
without,
} from 'ramda'
import { Evaluation, Unit } from './AST/types'
export type getUnitKey = (writtenUnit: string) => string
@ -53,8 +40,8 @@ export const serializeUnit = (
const unit = simplify(rawUnit),
{ numerators = [], denominators = [] } = unit
const n = !isEmpty(numerators)
const d = !isEmpty(denominators)
const n = numerators.length > 0
const d = denominators.length > 0
const string =
!n && !d
? ''
@ -95,8 +82,8 @@ export const inferUnit = (
}
if (operator === '*')
return simplify({
numerators: unnest(units.map((u) => u?.numerators ?? [])),
denominators: unnest(units.map((u) => u?.denominators ?? [])),
numerators: units.flatMap((u) => u?.numerators ?? []),
denominators: units.flatMap((u) => u?.denominators ?? []),
})
if (operator === '-' || operator === '+') {
@ -106,13 +93,20 @@ export const inferUnit = (
return undefined
}
const equals = <T>(a: T, b: T) => {
if (Array.isArray(a) && Array.isArray(b)) {
return a.length === b.length && a.every((_, i) => a[i] === b[i])
} else {
return a === b
}
}
export const removeOnce = <T>(
element: T,
eqFn: (a: T, b: T) => boolean = equals
) => (list: Array<T>): Array<T> => {
const index = list.findIndex((e) => eqFn(e, element))
if (index > -1) return remove<T>(index, 1)(list)
else return list
return list.filter((_, i) => i !== index)
}
const simplify = (
@ -262,8 +256,8 @@ export function simplifyUnit(unit: Unit): Unit {
return { numerators: ['%'], denominators }
}
return {
numerators: without(['%'], numerators),
denominators: without(['%'], denominators),
numerators: removePercentages(numerators),
denominators: removePercentages(denominators),
}
}
function simplifyUnitWithValue(unit: Unit, value = 1): [Unit, number] {
@ -273,24 +267,31 @@ function simplifyUnitWithValue(unit: Unit, value = 1): [Unit, number] {
return [
simplify(
{
numerators: without(['%'], numerators),
denominators: without(['%'], denominators),
numerators: removePercentages(numerators),
denominators: removePercentages(denominators),
},
areSameClass
),
value ? round(value * factor) : value,
]
}
const removePercentages = (array: Array<string>) =>
array.filter((e) => e !== '%')
export function areUnitConvertible(a: Unit | undefined, b: Unit | undefined) {
if (a == null || b == null) {
return true
}
const countByUnitClass = countBy((unit: string) => {
const classIndex = convertibleUnitClasses.findIndex((unitClass) =>
unitClass.has(unit)
)
return classIndex === -1 ? unit : '' + classIndex
})
const countByUnitClass = (units: Array<string>) =>
units.reduce((counters, unit) => {
const classIndex = convertibleUnitClasses.findIndex((unitClass) =>
unitClass.has(unit)
)
const key = classIndex === -1 ? unit : '' + classIndex
return { ...counters, [key]: 1 + (counters[key] ?? 0) }
}, {})
const [numA, denomA, numB, denomB] = [
a.numerators,
@ -298,12 +299,9 @@ export function areUnitConvertible(a: Unit | undefined, b: Unit | undefined) {
b.numerators,
b.denominators,
].map(countByUnitClass)
const unitClasses = pipe(
map(keys),
flatten,
uniq
)([numA, denomA, numB, denomB])
return unitClasses.every(
const uniq = <T>(arr: Array<T>): Array<T> => [...new Set(arr)]
const unitClasses = [numA, denomA, numB, denomB].map(Object.keys).flat()
return uniq(unitClasses).every(
(unitClass) =>
(numA[unitClass] || 0) - (denomA[unitClass] || 0) ===
(numB[unitClass] || 0) - (denomB[unitClass] || 0) || unitClass === '%'