2017-02-10 14:12:00 +00:00
import { rules , findRuleByName , parentName } from './rules'
2017-03-08 16:47:12 +00:00
import { completeVariableName , evaluateVariable , knownVariable } from './expressions'
2017-02-22 16:55:36 +00:00
import R from 'ramda'
2017-03-03 09:43:43 +00:00
import knownMecanisms from './known-mecanisms.yaml'
2017-03-07 17:25:25 +00:00
import { Parser } from 'nearley'
import Grammar from './grammar.ne'
2017-03-08 16:47:12 +00:00
let nearley = ( ) => new Parser ( Grammar . ParserRules , Grammar . ParserStart )
2017-01-09 17:17:51 +00:00
2017-03-06 16:35:30 +00:00
/ *
Dans ce fichier , les règles YAML sont parsées .
Elles expriment un langage orienté expression , les expressions étant
- préfixes quand elles sont des 'mécanismes' ( des mot - clefs représentant des calculs courants dans la loi )
- infixes pour les feuilles : des tests d 'égalité, d' inclusion , des comparaisons sur des variables ou tout simplement la variable elle - même , ou une opération effectuée sur la variable
* /
2017-01-19 15:27:27 +00:00
// L'objectif de la simulation : quelles règles voulons nous calculer ?
let selectedRules = rules . filter ( rule =>
2017-02-10 14:12:00 +00:00
R . contains ( rule . name ,
2017-02-07 19:10:04 +00:00
[
2017-03-08 16:47:12 +00:00
'CIF CDD' ,
'fin de contrat' ,
2017-03-08 16:49:22 +00:00
'majoration chômage CDD' ,
'simplifiée'
2017-02-07 19:10:04 +00:00
]
2017-01-19 15:27:27 +00:00
)
)
2017-01-09 17:17:51 +00:00
2017-02-07 19:10:04 +00:00
let transformPercentage = s =>
2017-03-01 10:21:53 +00:00
R . contains ( '%' ) ( s ) ?
+ s . replace ( '%' , '' ) / 100
: + s
2017-01-26 12:19:04 +00:00
2017-02-10 14:12:00 +00:00
2017-02-22 16:55:36 +00:00
/ *
- > Notre règle est naturellement un AST ( car notation préfixe dans le YAML )
- > préliminaire : les expression infixes devront être parsées ,
par exemple ainsi : https : //github.com/Engelberg/instaparse#transforming-the-tree
- > Notre règle entière est un AST , qu ' il faut maintenant traiter :
- faire le calcul ( déterminer les valeurs de chaque noeud )
- trouver les branches complètes pour déterminer les autres branches courtcircuitées
- ex . rule . formule est courtcircuitée si rule . non applicable est vrai
- les feuilles de "l'une de ces conditions" sont courtcircuitées si l 'une d' elle est vraie
- les feuilles de "toutes ces conditions" sont courtcircuitées si l 'une d' elle est fausse
- ...
( - bonus : utiliser ces informations pour l ' ordre de priorité des variables inconnues )
- si une branche est incomplète et qu 'elle est de type numérique, déterminer les bornes si c' est possible .
Ex . - pour une multiplication , si l 'assiette est connue mais que l ' applicabilité est inconnue ,
les bornes seront [ 0 , multiplication . value = assiette * taux ]
- si taux = effectif entreprise >= 20 ? 1 % : 2 % et que l ' applicabilité est connue ,
bornes = [ assiette * 1 % , assiette * 2 % ]
- transformer l ' arbre en JSX pour afficher le calcul * et son état en prenant en compte les variables renseignées et calculées * de façon sympathique dans un butineur Web tel que Mozilla Firefox .
- surement plein d ' autres applications ...
* /
2017-03-08 16:49:22 +00:00
let fillVariableNode = ( rule , situationGate ) => ( parseResult ) => {
let
{ fragments } = parseResult ,
variablePartialName = fragments . join ( ' . ' ) ,
variableName = completeVariableName ( rule , variablePartialName ) ,
known = knownVariable ( situationGate , variableName ) ,
nodeValue = ! known ? null : evaluateVariable ( situationGate , variableName )
return {
nodeValue ,
category : 'variable' ,
fragments : fragments ,
variableName ,
type : 'boolean | numeric' ,
explanation : null ,
missingVariables : known ? [ ] : [ variableName ]
}
}
2017-02-22 16:55:36 +00:00
let treat = ( situationGate , rule ) => rawNode => {
2017-03-08 16:47:12 +00:00
if ( R . is ( String ) ( rawNode ) ) {
/ * O n a à f a i r e à u n s t r i n g , d o n c à u n e e x p r e s s i o n i n f i x e .
Elle sera traité avec le parser obtenu grâce ) NearleyJs et notre grammaire .
On obtient un objet de type Variable ( avec potentiellement un 'modifier' ) , CalcExpression ou Comparison .
Cet objet est alors rebalancé à 'treat' .
* /
2017-02-22 16:55:36 +00:00
2017-03-08 16:49:22 +00:00
let [ parseResult , ... additionnalResults ] = nearley ( ) . feed ( rawNode ) . results
2017-03-08 16:47:12 +00:00
if ( additionnalResults && additionnalResults . length > 0 ) throw "Attention ! L'expression <" + rawNode + '> ne peut être traitée de façon univoque'
2017-03-08 16:49:22 +00:00
if ( ! R . contains ( parseResult . category ) ( [ 'variable' , 'calcExpression' , 'modifiedVariable' , 'comparison' ] ) )
2017-03-08 16:47:12 +00:00
throw "Attention ! Erreur de traitement de l'expression : " + rawNode
2017-03-08 16:49:22 +00:00
if ( parseResult . category == 'variable' )
return fillVariableNode ( rule , situationGate ) ( parseResult , rawNode )
if ( parseResult . category == 'comparison' ) {
2017-03-08 16:47:12 +00:00
let
2017-03-08 16:49:22 +00:00
// variablePartialName = parseResult.fragments.join(' . '),
// variableName = completeVariableName(rule, variablePartialName),
// known = knownVariable(situationGate, variableName)
filledExplanation = parseResult . explanation . map (
R . when ( R . propEq ( 'category' , 'variable' ) , fillVariableNode ( rule , situationGate ) )
) ,
[ { nodeValue : value1 } , { nodeValue : value2 } ] = filledExplanation ,
comparatorFunctionName = {
'<' : 'lt' ,
'<=' : 'lte' ,
'>' : 'gt' ,
'>=' : 'gte'
//TODO '='
} [ parseResult . operator ] ,
comparatorFunction = R [ comparatorFunctionName ] ,
nodeValue = value1 == null || value2 == null ?
null
: comparatorFunction ( value1 , value2 )
2017-03-08 16:47:12 +00:00
return {
2017-03-08 16:49:22 +00:00
text : rawNode ,
nodeValue : nodeValue ,
category : 'comparison' ,
type : 'boolean' ,
explanation : filledExplanation
2017-03-08 16:47:12 +00:00
}
2017-02-22 16:55:36 +00:00
}
}
2017-03-03 09:43:43 +00:00
//TODO C'est pas bien ça. Devrait être traité par le parser plus haut !
if ( R . is ( Number ) ( rawNode ) ) {
return {
category : 'number' ,
nodeValue : rawNode ,
type : 'numeric'
}
}
2017-02-22 16:55:36 +00:00
2017-03-03 09:43:43 +00:00
if ( ! R . is ( Object ) ( rawNode ) ) {
throw ' should be string or object'
}
let mecanisms = R . intersection ( R . keys ( rawNode ) , knownMecanisms )
if ( mecanisms . length != 1 ) throw 'OUPS !'
let k = R . head ( mecanisms ) ,
v = rawNode [ k ]
2017-02-22 16:55:36 +00:00
if ( k === "l'une de ces conditions" ) {
return R . pipe (
R . unless ( R . is ( Array ) , ( ) => { throw 'should be array' } ) ,
R . reduce ( ( memo , next ) => {
let { nodeValue , explanation } = memo ,
child = treat ( situationGate , rule ) ( next ) ,
{ nodeValue : nextValue } = child
return { ... memo ,
// c'est un OU logique mais avec une préférence pour null sur false
nodeValue : nodeValue || nextValue || (
nodeValue == null ? null : nextValue
) ,
explanation : [ ... explanation , child ]
}
} , {
nodeValue : false ,
category : 'mecanism' ,
name : "l'une de ces conditions" ,
type : 'boolean' ,
explanation : [ ]
} ) // Reduce but don't use R.reduced to set the nodeValue : we need to treat all the nodes
) ( v )
}
if ( k === 'toutes ces conditions' ) {
return R . pipe (
R . unless ( R . is ( Array ) , ( ) => { throw 'should be array' } ) ,
R . reduce ( ( memo , next ) => {
let { nodeValue , explanation } = memo ,
child = treat ( situationGate , rule ) ( next ) ,
{ nodeValue : nextValue } = child
return { ... memo ,
// c'est un ET logique avec une possibilité de null
nodeValue : ! nodeValue ? nodeValue : nextValue ,
explanation : [ ... explanation , child ]
}
} , {
nodeValue : true ,
category : 'mecanism' ,
name : 'toutes ces conditions' ,
type : 'boolean' ,
explanation : [ ]
} ) // Reduce but don't use R.reduced to set the nodeValue : we need to treat all the nodes
) ( v )
}
2017-03-02 15:31:24 +00:00
//TODO perf: declare this closure somewhere else ?
let treatNumericalLogicRec =
R . ifElse (
R . is ( String ) ,
rate => ( {
nodeValue : transformPercentage ( rate ) ,
type : 'numeric' ,
category : 'percentage' ,
percentage : rate ,
explanation : null
} ) ,
R . pipe (
R . unless (
v => R . is ( Object ) ( v ) && R . keys ( v ) . length >= 1 ,
( ) => { throw 'Le mécanisme "logique numérique" et ses sous-logiques doivent contenir au moins une proposition' }
) ,
R . toPairs ,
R . reduce ( ( memo , [ condition , consequence ] ) => {
2017-03-08 16:49:22 +00:00
let
{ nodeValue , explanation } = memo ,
conditionNode = treat ( situationGate , rule ) ( condition ) , // can be a 'comparison', a 'variable', TODO a 'negation'
2017-03-02 15:31:24 +00:00
childNumericalLogic = treatNumericalLogicRec ( consequence ) ,
2017-03-08 16:49:22 +00:00
nextNodeValue = conditionNode . nodeValue == null ?
2017-03-02 15:31:24 +00:00
// Si la proposition n'est pas encore résolvable
null
// Si la proposition est résolvable
2017-03-08 16:49:22 +00:00
: conditionNode . nodeValue == true ?
2017-03-02 15:31:24 +00:00
// Si elle est vraie
childNumericalLogic . nodeValue
// Si elle est fausse
: false
return { ... memo ,
nodeValue : nodeValue == null ?
null
: nodeValue !== false ?
nodeValue // l'une des propositions renvoie déjà une valeur numérique donc différente de false
: nextNodeValue ,
explanation : [ ... explanation , {
nodeValue : nextNodeValue ,
category : 'condition' ,
2017-03-08 16:49:22 +00:00
text : condition ,
condition : conditionNode ,
conditionValue : conditionNode . nodeValue ,
2017-03-02 15:31:24 +00:00
type : 'boolean' ,
explanation : childNumericalLogic
} ] ,
}
} , {
nodeValue : false ,
category : 'mecanism' ,
name : "logique numérique" ,
type : 'boolean || numeric' , // lol !
explanation : [ ]
} )
) )
2017-03-03 09:43:43 +00:00
if ( k === 'logique numérique' ) {
2017-03-02 15:31:24 +00:00
return treatNumericalLogicRec ( v )
}
2017-03-03 09:43:43 +00:00
if ( k === 'taux' ) {
2017-03-02 15:31:24 +00:00
//TODO gérer les taux historisés
if ( R . is ( String ) ( v ) )
return {
type : 'numeric' ,
category : 'percentage' ,
percentage : v ,
nodeValue : transformPercentage ( v ) ,
explanation : null
}
else {
let node = treat ( situationGate , rule ) ( v )
return {
type : 'numeric' ,
category : 'percentage' ,
percentage : node . nodeValue ,
nodeValue : node . nodeValue ,
explanation : node
}
}
}
2017-03-01 10:21:53 +00:00
if ( k === 'multiplication' ) {
2017-03-08 16:49:22 +00:00
2017-03-01 10:21:53 +00:00
let base = v [ 'assiette' ] ,
2017-03-08 16:47:12 +00:00
parsed = nearley ( ) . feed ( base ) ,
2017-03-08 16:49:22 +00:00
baseVariableFound = parsed . results [ 0 ] . category == 'variable'
if ( ! baseVariableFound ) throw "L'assiette d'une multiplication doit pour le moment être une variable"
let
variablePartialName = parsed . results [ 0 ] . fragments . join ( ' . ' ) ,
2017-03-08 16:47:12 +00:00
baseVariableName = completeVariableName ( rule , variablePartialName ) ,
2017-03-02 15:31:24 +00:00
baseValue = situationGate ( baseVariableName ) ,
rateNode = treat ( situationGate , rule ) ( { taux : v [ 'taux' ] } ) ,
rate = rateNode . nodeValue
2017-03-01 10:21:53 +00:00
return {
2017-03-08 16:49:22 +00:00
nodeValue : ( rate === 0 || rate === false || baseValue === 0 ) ?
0
: ( rate == null || baseValue == null ) ?
null
: + baseValue * rate ,
2017-03-01 10:21:53 +00:00
category : 'mecanism' ,
name : 'multiplication' ,
type : 'numeric' ,
explanation : {
base : {
type : 'numeric' ,
category : 'variable' ,
// name: 'base', déduit de la propriété de l'objet
2017-03-02 15:31:24 +00:00
nodeValue : baseValue == null ? null : + baseValue ,
2017-03-01 10:21:53 +00:00
explanation : null ,
2017-03-01 19:27:35 +00:00
variableName : baseVariableName ,
missingVariables : baseValue == null ? [ baseVariableName ] : [ ]
2017-03-01 10:21:53 +00:00
} ,
2017-03-02 15:31:24 +00:00
rate : rateNode ,
2017-03-07 17:25:25 +00:00
// prorata:
2017-03-01 10:21:53 +00:00
//TODO limit: 'plafond'
//TODO multiplier: 'multiplicateur'
}
}
}
2017-03-03 09:43:43 +00:00
if ( k === 'le maximum de' ) {
let contenders = v . map ( treat ( situationGate , rule ) ) ,
contenderValues = R . pluck ( 'nodeValue' ) ( contenders ) ,
stopEverything = R . contains ( null , contenderValues ) ,
maxValue = R . max ( ... contenderValues )
return {
type : 'numeric' ,
category : 'mecanism' ,
name : 'le maximum de' ,
nodeValue : stopEverything ? null : maxValue ,
explanation : contenders
}
}
throw "Le mécanisme qui vient d'être loggué est inconnu !"
2017-02-22 16:55:36 +00:00
}
let treatRuleRoot = ( situationGate , rule ) => R . evolve ( { // -> Voilà les attributs que peut comporter, pour l'instant, une Variable.
// 'meta': pas de traitement pour l'instant
// 'cond' : Conditions d'applicabilité de la règle
2017-03-01 10:21:53 +00:00
'non applicable si' : value => {
let child = treat ( situationGate , rule ) ( value )
return {
category : 'ruleProp' ,
rulePropType : 'cond' ,
name : 'non applicable si' ,
type : 'boolean' ,
nodeValue : child . nodeValue ,
explanation : child
}
}
2017-02-22 16:55:36 +00:00
,
// [n'importe quel mécanisme booléen] : expression booléenne (simple variable, négation, égalité, comparaison numérique, test d'inclusion court / long) || l'une de ces conditions || toutes ces conditions
// 'applicable si': // pareil mais inversé !
2017-03-01 10:21:53 +00:00
// note: pour certaines variables booléennes, ex. appartenance à régime Alsace-Moselle, la formule et le non applicable si se rejoignent
2017-02-22 16:55:36 +00:00
// [n'importe quel mécanisme numérique] : multiplication || barème en taux marginaux || le maximum de || le minimum de || ...
2017-03-01 10:21:53 +00:00
'formule' : value => {
let child = treat ( situationGate , rule ) ( value )
return {
category : 'ruleProp' ,
rulePropType : 'formula' ,
name : 'formule' ,
type : 'numeric' ,
nodeValue : child . nodeValue ,
2017-03-01 19:27:35 +00:00
explanation : child ,
shortCircuit : R . pathEq ( [ 'non applicable si' , 'nodeValue' ] , true )
2017-03-01 10:21:53 +00:00
}
}
,
2017-02-22 16:55:36 +00:00
// TODO les mécanismes de composantes et de variations utilisables un peu partout !
// TODO 'temporal': information concernant les périodes : à définir !
// TODO 'intéractions': certaines variables vont en modifier d'autres : ex. Fillon va réduire voir annuler (set 0) une liste de cotisations
// ... ?
} ) ( rule )
2017-02-10 14:12:00 +00:00
/ * A n a l y s e t h e s e t o f s e l e c t e d r u l e s , a n d a d d d e r i v e d i n f o r m a t i o n t o t h e m :
- do they need variables that are not present in the user situation ?
- if not , do they have a computed value or are they non applicable ?
* /
2017-01-26 12:19:04 +00:00
export let analyseSituation = situationGate =>
2017-02-22 16:55:36 +00:00
selectedRules . map (
rule => treatRuleRoot ( situationGate , rule )
2017-02-10 14:12:00 +00:00
)
2017-01-26 12:19:04 +00:00
2017-01-10 14:39:40 +00:00
export let variableType = name => {
2017-01-10 18:22:44 +00:00
if ( name == null ) return null
let found = findRuleByName ( name )
2017-01-10 14:39:40 +00:00
// tellement peu de variables pour l'instant
// que c'est très simpliste
2017-01-10 18:22:44 +00:00
if ( ! found ) return 'boolean'
2017-02-22 16:55:36 +00:00
let { rule } = found
2017-01-10 18:22:44 +00:00
if ( typeof rule . formule [ 'somme' ] !== 'undefined' ) return 'numeric'
2017-01-10 14:39:40 +00:00
}
2017-01-19 15:27:27 +00:00
/ * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Ce qui suit est la première tentative d ' écriture du principe du moteur et de la syntaxe * /
2017-01-09 17:17:51 +00:00
// let types = {
/ *
( expression ) :
| ( variable )
| ( négation )
| ( égalité )
| ( comparaison numérique )
| ( test d ' inclusion court )
* /
// }
/ *
Variable :
- applicable si : ( boolean logic )
- non applicable si : ( boolean logic )
- concerne : ( expression )
- ne concerne pas : ( expression )
( boolean logic ) :
toutes ces conditions : ( [ expression | boolean logic ] )
l ' une de ces conditions : ( [ expression | boolean logic ] )
conditions exclusives : ( [ expression | boolean logic ] )
"If you write a regular expression, walk away for a cup of coffee, come back, and can't easily understand what you just wrote, then you should look for a clearer way to express what you're doing."
Les expressions sont le seul mécanisme relativement embêtant pour le moteur . Dans un premier temps , il les gerera au moyen d 'expressions régulières, puis il faudra probablement mieux s' équiper avec un "javascript parser generator" :
https : //medium.com/@daffl/beyond-regex-writing-a-parser-in-javascript-8c9ed10576a6
( variable ) : ( string )
( négation ) :
! ( variable )
( égalité ) :
( variable ) = ( variable . type )
( comparaison numérique ) :
| ( variable ) < ( variable . type )
| ( variable ) <= ( variable . type )
| ( variable ) > ( variable . type )
| ( variable ) <= ( variable . type )
( test d ' inclusion court ) :
( variable ) ⊂ [ variable . type ]
in Variable . formule :
- composantes
- linéaire
- barème en taux marginaux
- test d 'inclusion: (test d' inclusion )
( test d ' inclusion ) :
variable : ( variable )
possibilités : [ variable . type ]
# pas nécessaire pour le CDD
in Variable
- variations : [ si ]
( si ) :
si : ( expression )
# corps
* /