⚙️🔥 Ajoute un AST bien typé pour publicodes

- Introduction de nouveaux mécanismes
- Réecriture de l'evaluation et du parsing des règles.
- Les règles peuvent apparaître dans les formules de calcul
- Introduction d'un AST en bonne et due forme
- Réecriture de buildRuleDependancies.
- Ajout d'une passe pour la désambiguation des références
- Réecriture de rendNonApplicable et de remplace
- Réimplémentation de parentDependancy

Voir #1191
pull/1275/head
Johan Girod 2020-11-04 18:05:46 +01:00
parent 9bc80e4158
commit ba01ae2d4f
99 changed files with 2595 additions and 3438 deletions

View File

@ -89,11 +89,13 @@ artiste-auteur . cotisations . vieillesse:
produit:
assiette: assiette
composantes:
- nom: plafonnée
taux: contrat salarié . vieillesse . taux salarié plafonné - 0.75%
- attributs:
nom: plafonnée
taux: contrat salarié . vieillesse . salarié . plafonnée . taux - 0.75%
plafond: contrat salarié . plafond sécurité sociale
- nom: déplafonnée
taux: contrat salarié . vieillesse . taux salarié déplafonné - 0.4%
- attributs:
nom: déplafonnée
taux: contrat salarié . vieillesse . salarié . déplafonnée . taux - 0.4%
artiste-auteur . cotisations . CSG-CRDS:
formule:

View File

@ -38,13 +38,13 @@ contrat salarié . convention collective . BTP . catégorie du salarié . cadre:
contrat salarié . convention collective . BTP . retraite complémentaire:
non applicable si: catégorie du salarié = 'etam'
remplace:
- règle: retraite complémentaire . taux employeur tranche 1
- règle: retraite complémentaire . employeur . taux tranche 1
par: 4.72%
- règle: retraite complémentaire . taux employeur tranche 2
- règle: retraite complémentaire . employeur . taux tranche 2
par: 12.95%
- règle: retraite complémentaire . taux salarié tranche 1
- règle: retraite complémentaire . salarié . taux tranche 1
par: 3.15%
- règle: retraite complémentaire . taux salarié tranche 2
- règle: retraite complémentaire . salarié . taux tranche 2
par: 8.64%
contrat salarié . convention collective . BTP . retraite complémentaire . etam:
@ -52,13 +52,13 @@ contrat salarié . convention collective . BTP . retraite complémentaire . etam
description: >-
Répartition conventionnelle fixée par larticle 5 de lAccord du BTP du 13 décembre 1990.
remplace:
- règle: retraite complémentaire . taux employeur tranche 1
- règle: retraite complémentaire . employeur . taux tranche 1
par: 4.47%
- règle: retraite complémentaire . taux employeur tranche 2
- règle: retraite complémentaire . employeur . taux tranche 2
par: 12.70%
- règle: retraite complémentaire . taux salarié tranche 1
- règle: retraite complémentaire . salarié . taux tranche 1
par: 3.40%
- règle: retraite complémentaire . taux salarié tranche 2
- règle: retraite complémentaire . salarié . taux tranche 2
par: 8.89%
contrat salarié . convention collective . BTP . prévoyance complémentaire:

View File

@ -74,7 +74,7 @@ contrat salarié . intermittents du spectacle . formation professionnelle:
somme:
- 50 €/mois
- produit:
assiette: rémunération . brut [€/mois]
assiette: rémunération . brut
taux: 2.10%
contrat salarié . intermittents du spectacle . caisse des congés spectacle:
@ -92,9 +92,9 @@ contrat salarié . intermittents du spectacle . retraite complémentaire technic
- statut cadre
- technicien
remplace:
- règle: retraite complémentaire . taux employeur tranche 1
- règle: retraite complémentaire . employeur . taux tranche 1
par: 3.94%
- règle: retraite complémentaire . taux salarié tranche 1
- règle: retraite complémentaire . salarié . taux tranche 1
par: 3.93%
références:
audiens.org: https://www.audiens.org/solutions/entreprises-la-retraite-complementaire-agirc-arcco-au-1er-janvier-2019.html
@ -105,9 +105,9 @@ contrat salarié . intermittents du spectacle . technicien:
contrat salarié . intermittents du spectacle . technicien . non cadre:
applicable si: statut cadre = non
remplace:
- règle: retraite complémentaire . taux employeur tranche 2
- règle: retraite complémentaire . employeur . taux tranche 2
par: 10.80%
- règle: retraite complémentaire . taux salarié tranche 2
- règle: retraite complémentaire . salarié . taux tranche 2
par: 10.79%
- règle: plafond sécurité sociale
par: plafond sécurité sociale temps plein
@ -148,13 +148,13 @@ contrat salarié . intermittents du spectacle . artiste . non cadre:
- retraite complémentaire
- contribution d'équilibre général
- contribution d'équilibre technique
- règle: retraite complémentaire . taux employeur tranche 1
- règle: retraite complémentaire . employeur . taux tranche 1
par: 4.45%
- règle: retraite complémentaire . taux employeur tranche 2
- règle: retraite complémentaire . employeur . taux tranche 2
par: 10.80%
- règle: retraite complémentaire . taux salarié tranche 1
- règle: retraite complémentaire . salarié . taux tranche 1
par: 4.44%
- règle: retraite complémentaire . taux salarié tranche 2
- règle: retraite complémentaire . salarié . taux tranche 2
par: 10.79%
références:
audiens.org: https://www.audiens.org/solutions/entreprises-la-retraite-complementaire-agirc-arcco-au-1er-janvier-2019.html
@ -175,14 +175,14 @@ contrat salarié . intermittents du spectacle . artiste . réduction de taux:
par: maladie . taux employeur * réduction de taux
- règle: maladie . taux salarié
par: maladie . taux salarié * réduction de taux
- règle: vieillesse . taux employeur plafonné
par: vieillesse . taux employeur plafonné * réduction de taux
- règle: vieillesse . taux employeur déplafonné
par: vieillesse . taux employeur déplafonné * réduction de taux
- règle: vieillesse . taux salarié plafonné
par: vieillesse . taux salarié plafonné * réduction de taux
- règle: vieillesse . taux salarié déplafonné
par: vieillesse . taux salarié déplafonné * réduction de taux
- règle: vieillesse . employeur . plafonnée . taux
par: vieillesse . employeur . plafonnée . taux * réduction de taux
- règle: vieillesse . employeur . déplafonnée . taux
par: vieillesse . employeur . déplafonnée . taux * réduction de taux
- règle: vieillesse . salarié . plafonnée . taux
par: vieillesse . salarié . plafonnée . taux * réduction de taux
- règle: vieillesse . salarié . déplafonnée . taux
par: vieillesse . salarié . déplafonnée . taux * réduction de taux
- règle: allocations familiales . taux
par: allocations familiales . taux * réduction de taux
- règle: établissement . taux du versement transport

View File

@ -14,7 +14,7 @@ contrat salarié . convention collective . sport . cotisations . patronales:
- règle: cotisations . patronales . conventionnelles
formule:
somme:
- prévoyance .employeur
- prévoyance . employeur
- financement du paritarisme
contrat salarié . convention collective . sport . cotisations . financement du paritarisme:
@ -29,11 +29,11 @@ contrat salarié . convention collective . sport . cotisations . financement du
contrat salarié . convention collective . sport . cotisations . prévoyance:
remplace:
- règle: cotisations . salariales . conventionnelles
par: prévoyance .salarié
par: prévoyance . salarié
- règle: avantages sociaux
par:
somme:
- prévoyance .employeur
- prévoyance . employeur
- avantages sociaux
formule:
produit:
@ -41,10 +41,10 @@ contrat salarié . convention collective . sport . cotisations . prévoyance:
plafond: 8 * plafond sécurité sociale
composantes:
- attributs:
dû par: employeur
nom: employeur
taux: 0.29%
- attributs:
dû par: salarié
nom: salarié
taux: 0.29%
références:
Article 10.8 de la CCNS (IDCC 2511): https://www.legifrance.gouv.fr/affichIDCCArticle.do;?idArticle=KALIARTI000033304755&cidTexte=KALITEXT000017577657&dateTexte=29990101&categorieLien=id
@ -188,7 +188,7 @@ contrat salarié . convention collective . sport . cotisations . assiette forfai
remplace: contrat salarié . cotisations . assiette forfaitaire
formule:
grille:
assiette: assiette franchisée [€/mois]
assiette: assiette franchisée
multiplicateur: SMIC horaire / 1 mois
unité: €/mois
tranches:

View File

@ -82,6 +82,7 @@ dirigeant . auto-entrepreneur:
dirigeant . auto-entrepreneur . base des cotisations:
formule: entreprise . chiffre d'affaires
unité: €/mois
dirigeant . auto-entrepreneur . contrôle seuil de CA dépassé:
type: notification
@ -234,11 +235,11 @@ dirigeant . auto-entrepreneur . cotisations et contributions . cotisations:
- si: entreprise . ACRE
alors:
produit:
assiette: base des cotisations [€/mois]
assiette: base des cotisations
taux: taux ACRE * taux de cotisation
- sinon:
produit:
assiette: base des cotisations [€/mois]
assiette: base des cotisations
taux [ref taux de cotisation]:
variations:
- si: vente ou hébergement
@ -550,7 +551,7 @@ dirigeant . indépendant . revenu net de cotisations:
somme:
- valeur: revenu professionnel
arrondi: oui
- (- cotisations et contributions . CSG et CRDS .non déductible)
- (- cotisations et contributions . CSG et CRDS . non déductible)
- (- contrats madelin . part non-déductible fiscalement)
résumé: Avant déduction de l'impôt sur le revenu
question: Quel revenu avant impôt voulez-vous toucher ?
@ -1068,22 +1069,23 @@ dirigeant . indépendant . cotisations et contributions . CSG et CRDS:
assiette: assiette
composantes:
- attributs:
impôt sur le revenu: non déductible
taux: 2.9%
nom: non déductible
arrondi: oui
composantes:
- taux: 2.9%
- attributs:
nom: revenus de remplacement
assiette: dirigeant . indépendant . IJSS . total
taux: 2.9%
- attributs:
impôt sur le revenu: déductible
taux: 6.8%
- attributs:
nom: revenus de remplacement
impôt sur le revenu: non déductible
assiette: dirigeant . indépendant . IJSS . total
taux: 2.9%
- attributs:
nom: revenus de remplacement
impôt sur le revenu: déductible
assiette: dirigeant . indépendant . IJSS . total
taux: 3.8%
arrondi: oui
nom: déductible
arrondi: oui
composantes:
- taux: 6.8%
- attributs:
nom: revenus de remplacement
assiette: dirigeant . indépendant . IJSS . total
taux: 3.8%
références:
fiche URSSAF: https://www.urssaf.fr/portail/home/indépendant/mes-cotisations/quelles-cotisations/les-contributions-csg-crds/taux-de-la-csg-crds.html
@ -1123,7 +1125,7 @@ dirigeant . indépendant . cotisations et contributions . CSG et CRDS . assiette
dirigeant . indépendant . cotisations et contributions . formation professionnelle:
formule:
produit:
assiette: plafond sécurité sociale temps plein [€/an]
assiette: plafond sécurité sociale temps plein
taux:
variations:
- si: entreprise . catégorie d'activité = 'artisanale'
@ -1194,7 +1196,7 @@ dirigeant . indépendant . cotisations et contributions . exonérations . ZFU .
titre: taux exonération ZFU
formule:
taux progressif:
assiette: établissement . ZFU . durée d'implantation en fin d'année [an]
assiette: établissement . ZFU . durée d'implantation en fin d'année
retourne seulement le taux: oui
variations:
- si: entreprise . effectif < 5

View File

@ -121,7 +121,7 @@ aide déclaration revenu indépendant 2019 . revenu net fiscal:
aide déclaration revenu indépendant 2019 . CSG déductible:
titre: CSG déductible
résumé: '[B]'
formule: dirigeant . indépendant . cotisations et contributions . CSG et CRDS .déductible
formule: dirigeant . indépendant . cotisations et contributions . CSG et CRDS . déductible
aide déclaration revenu indépendant 2019 . cotisations sociales déductible:
titre: cotisations sociales obligatoires déductibles

View File

@ -406,48 +406,6 @@ entreprise . catégorie d'activité . libérale règlementée:
références:
Liste des activités libérales: https://bpifrance-creation.fr/encyclopedie/trouver-proteger-tester-son-idee/verifiertester-son-idee/liste-professions-liberales
entreprise . catégorie d'activité . libérale règlementée . type d'activité libérale règlementée:
formule:
une possibilité:
choix obligatoire: oui
possibilités:
- Administrateur judiciaire
- Agent général d'assurance
- Architecte
- Architecte d'intérieur
- Avocat
- Avocat au conseil d'Etat et à la Cour de Cassation
- Avoué auprès des cours d'appel
- Chiropracteur
- Chirurgien-dentiste
- Commissaire aux comptes
- Commissaire-priseur
- Conseil en investissements financiers
- Conseil en propriété industrielle
- Diététicien
- Ergothérapeute
- Expert agricole, foncier et expert forestier
- Expert devant les tribunaux
- Expert-comptable
- Géomètre-expert
- Greffier auprès des tribunaux de commerce
- Huissier de justice
- Infirmier libéral
- Directeur de laboratoire d'analyses médicales
- Mandataire judiciaire
- Mandataire judiciaire à la protecion des majeurs
- Masseur-kinésithérapeute
- Médecin
- Notaire
- Orthophoniste
- Orthoptiste
- Ostéopathe
- Pédicure-podologue
- Psychologue
- Psychomotricien
- Psychothérapeute
- Sage-femme
- Vétérinaire
entreprise . catégorie d'activité . débit de tabac:
applicable si: catégorie d'activité = 'commerciale ou industrielle'
@ -499,16 +457,17 @@ entreprise . auto entreprise impossible:
question: Dans quelle commune l'établissement est-il implanté ?
API: commune
par défaut:
code: 29019
nom: Non renseignée
departement:
nom: Non renseigné
taux du versement transport: 0.018
objet:
code: 29019
nom: Non renseignée
departement:
nom: Non renseigné
taux du versement transport: 0.018
établissement . localisation . code commune:
formule:
synchronisation:
API: localisation
data: localisation
chemin: code
établissement . localisation . commune:
@ -517,19 +476,19 @@ entreprise . auto entreprise impossible:
calculées à l'échelle de l'établissement et sont fonction de règlementations locales.
formule:
synchronisation:
API: localisation
data: localisation
chemin: nom
établissement . taux du versement transport:
formule:
synchronisation:
API: localisation
data: localisation
chemin: taux du versement transport
établissement . localisation . département:
formule:
synchronisation:
API: localisation
data: localisation
chemin: departement . nom
établissement . localisation . outre-mer:

View File

@ -6,9 +6,9 @@ impôt:
formule:
somme:
- produit:
assiette: revenu imposable [€/an]
assiette: revenu imposable
taux: taux d'imposition
- dirigeant . auto-entrepreneur . impôt . versement libératoire . montant [€/an]
- dirigeant . auto-entrepreneur . impôt . versement libératoire . montant
arrondi: oui
impôt . taux d'imposition:

View File

@ -44,7 +44,7 @@ const rules: Rules = {
...professionLibérale,
...entrepriseEtablissement,
...protectionSociale,
...salarié,
... salarié,
...CCBatiment,
...CCHotels,
...CCOptique,

View File

@ -311,7 +311,7 @@ protection sociale . santé . indemnités journalières . auto-entrepreneur:
alors: 0 €/jour
- sinon:
produit:
assiette: revenu moyen [€/jour]
assiette: revenu moyen
taux: 50%
plafond: 55.51 €/jour
reférences:
@ -320,10 +320,9 @@ protection sociale . santé . indemnités journalières . auto-entrepreneur:
protection sociale . santé . indemnités journalières . indépendant:
applicable si: dirigeant . indépendant
unité: €/jour
formule:
produit:
assiette: revenu moyen [€/jour]
assiette: revenu moyen
taux: 50%
plancher: 21 €/jour
plafond: 55.51 €/jour
@ -337,9 +336,9 @@ protection sociale . santé . indemnités journalières . salarié:
applicable si: contrat salarié
formule:
produit:
assiette: revenu moyen [€/jour]
assiette: revenu moyen
taux: 50%
plafond: 1.8 * SMIC temps plein [€/jour]
plafond: 1.8 * SMIC temps plein
reférences:
service-public.fr: https://www.service-public.fr/particuliers/vosdroits/F3053
@ -409,12 +408,12 @@ protection sociale . accidents du travail et maladies professionnelles:
applicable si: contrat salarié
formule:
produit:
assiette: revenu moyen [€/jour]
assiette: revenu moyen
taux: 60%
plafond:
202.78 €/jour
# TODO
# - 0.834% * plafond sécurité sociale temps plein [€/an]
# - 0.834% * plafond sécurité sociale temps plein
références:
ameli.fr: https://www.ameli.fr/paris/entreprise/cotisations/mp-tarification-calculs-baremes/compte-mp

View File

@ -64,7 +64,7 @@ contrat salarié . frais professionnels:
formule:
somme:
- indemnité kilométrique vélo . montant
- titres-restaurant . montant .employeur
- titres-restaurant . montant . employeur
contrat salarié . frais professionnels . part déductible:
titre: Frais professionnels déductibles
@ -104,16 +104,16 @@ contrat salarié . frais professionnels . titres-restaurant . montant:
facteur: titres-restaurant par mois
composantes:
- attributs:
dû par: employeur
nom: employeur
taux: taux participation employeur
- attributs:
dû par: salarié
nom: salarié
taux: 100% - taux participation employeur
contrat salarié . frais professionnels . titres-restaurant . part déductible:
titre: Titres-restaurant (déductible)
formule:
valeur: montant .employeur
valeur: montant . employeur
plafond:
produit:
assiette: titres-restaurant par mois
@ -1138,7 +1138,8 @@ contrat salarié . cotisations . assiette:
titre: Assiette des cotisations sociales
description: |
L'assiette des cotisations sociales est la base de calcul d'un grand nombre de cotisations sur le travail salarié. Elle comprend notamment les rémunérations en espèces (salaire de base, indemnité, primes...) et les avantages en nature (logement, véhicule...).
référence: https://www.urssaf.fr/portail/home/employeur/calculer-les-cotisations/la-base-de-calcul.html
références:
Fiche Urssaf: https://www.urssaf.fr/portail/home/employeur/calculer-les-cotisations/la-base-de-calcul.html
formule:
allègement:
assiette: rémunération . brut
@ -1436,7 +1437,7 @@ contrat salarié . avantages sociaux:
- prévoyance . employeur
- retraite supplémentaire . employeur
- prévoyance obligatoire cadre
- complémentaire santé .employeur
- complémentaire santé . employeur
contrat salarié . rémunération . avantages en nature:
icônes: 🛏️🚗🥗📱
@ -1628,15 +1629,15 @@ contrat salarié . cotisations . salariales:
formule:
somme:
- vieillesse .salarié
- maladie .salarié
- retraite complémentaire .salarié
- contribution d'équilibre général .salarié
- contribution d'équilibre technique .salarié
- chômage .salarié
- vieillesse . salarié
- maladie . salarié
- retraite complémentaire . salarié
- contribution d'équilibre général . salarié
- contribution d'équilibre technique . salarié
- chômage . salarié
- CSG et CRDS
- APEC .salarié
- complémentaire santé .salarié
- APEC . salarié
- complémentaire santé . salarié
- conventionnelles
- (- réductions de cotisations)
@ -1644,17 +1645,17 @@ contrat salarié . cotisations . patronales:
titre: cotisations patronales
formule:
somme:
- maladie .employeur
- maladie . employeur
- ATMP
- prévoyance obligatoire cadre
- vieillesse .employeur
- retraite complémentaire .employeur
- complémentaire santé .employeur
- contribution d'équilibre général .employeur
- contribution d'équilibre technique .employeur
- vieillesse . employeur
- retraite complémentaire . employeur
- complémentaire santé . employeur
- contribution d'équilibre général . employeur
- contribution d'équilibre technique . employeur
- allocations familiales
- chômage .employeur
- APEC .employeur
- chômage . employeur
- APEC . employeur
- AGS
- FNAL
- participation effort de construction
@ -1867,9 +1868,9 @@ contrat salarié . cotisations . salariales . réduction heures supplémentaires
produit:
assiette:
somme:
- vieillesse .salarié
- retraite complémentaire .salarié
- contribution d'équilibre général .salarié
- vieillesse . salarié
- retraite complémentaire . salarié
- contribution d'équilibre général . salarié
facteur: 1 / assiette
plafond: 11.31%
références:
@ -2288,8 +2289,8 @@ contrat salarié . statut JEI . exonération de cotisations:
formule:
somme:
- allocations familiales
- maladie .employeur
- vieillesse .employeur
- maladie . employeur
- vieillesse . employeur
plafond:
recalcul:
avec:
@ -2346,9 +2347,9 @@ contrat salarié . réduction générale . T:
formule:
somme:
- T sécurité sociale et chômage
- valeur: retraite complémentaire . taux employeur tranche 1
- valeur: retraite complémentaire . employeur . taux tranche 1
plafond: 4.72%
- valeur: contribution d'équilibre général . taux employeur tranche 1
- valeur: contribution d'équilibre général . employeur . taux tranche 1
plafond: 1.29%
contrat salarié . réduction générale . T sécurité sociale et chômage:
@ -2357,12 +2358,12 @@ contrat salarié . réduction générale . T sécurité sociale et chômage:
somme:
- maladie . taux employeur
- allocations familiales . taux
- vieillesse . taux employeur déplafonné
- vieillesse . taux employeur plafonné
- vieillesse . employeur . déplafonnée . taux
- vieillesse . employeur . plafonnée . taux
- maladie . taux solidarité autonomie
- ATMP . taux minimum
- FNAL . taux
- chômage . taux employeur
- chômage . employeur . taux
contrat salarié . réduction générale . imputation sécurité sociale:
formule:
@ -2402,15 +2403,14 @@ contrat salarié . contribution d'équilibre général:
multiplicateur: plafond sécurité sociale
composantes:
- attributs:
dû par: employeur
nom: employeur
tranches:
- taux [ref taux employeur tranche 1]: 1.29%
- taux [ref taux tranche 1]: 1.29%
plafond: 1
- taux: 1.62%
plafond: 8
- attributs:
dû par: salarié
nom: salarié
assiette: cotisations . assiette . salariale
tranches:
- taux: 0.86%
@ -2434,10 +2434,10 @@ contrat salarié . contribution d'équilibre technique:
plafond: 8 * plafond sécurité sociale
composantes:
- attributs:
dû par: employeur
taux [ref taux employeur]: 0.21%
nom: employeur
taux [ref]: 0.21%
- attributs:
dû par: salarié
nom: salarié
taux: 0.14%
références:
calcul des cotisations: https://www.agirc-arrco.fr/ce-qui-change-au-1er-janvier-2019/vous-etes-une-entreprise-tiers-declarant/
@ -2455,19 +2455,19 @@ contrat salarié . retraite complémentaire:
multiplicateur: plafond sécurité sociale
composantes:
- attributs:
dû par: employeur
nom: employeur
tranches:
- taux [ref taux employeur tranche 1]: 4.72%
- taux [ref taux tranche 1]: 4.72%
plafond: 1
- taux [ref taux employeur tranche 2]: 12.95%
- taux [ref taux tranche 2]: 12.95%
plafond: 8
- attributs:
dû par: salarié
nom: salarié
assiette: cotisations . assiette . salariale
tranches:
- taux [ref taux salarié tranche 1]: 3.15%
- taux [ref taux tranche 1]: 3.15%
plafond: 1
- taux [ref taux salarié tranche 2]: 8.64%
- taux [ref taux tranche 2]: 8.64%
plafond: 8
références:
calcul des cotisations: https://www.agirc-arrco.fr/ce-qui-change-au-1er-janvier-2019/vous-etes-une-entreprise-tiers-declarant/
@ -2565,10 +2565,10 @@ contrat salarié . APEC:
plafond: 4 * plafond sécurité sociale
composantes:
- attributs:
dû par: employeur
nom: employeur
taux: 0.036%
- attributs:
dû par: salarié
nom: salarié
taux: 0.024%
contrat salarié . chômage:
@ -2587,11 +2587,11 @@ contrat salarié . chômage:
plafond: 4 * plafond sécurité sociale
composantes:
- attributs:
dû par: salarié
taux [ref taux salarié]: 0%
nom: salarié
taux [ref]: 0%
- attributs:
dû par: employeur
taux [ref taux employeur]: 4.05%
nom: employeur
taux [ref]: 4.05%
exemples:
- nom: SMIC
situation:
@ -2619,10 +2619,10 @@ contrat salarié . complémentaire santé:
composantes:
# Répartition arbitraire, en sachant que l'employeur doit prendre en charge au minimum 50%
- attributs:
dû par: employeur
nom: employeur
taux: part employeur
- attributs:
dû par: salarié
nom: salarié
taux: part salarié
exemples:
- nom: forfait à 40€
@ -2780,7 +2780,7 @@ contrat salarié . CSG et CRDS . non déductible:
titre: CSG non déductible et CRDS
formule:
somme:
- CSG . base .non déductible
- CSG . base . non déductible
- CSG . heures supplémentaires et complémentaires défiscalisées
- CRDS
- revenus de remplacement . CSG non déductible
@ -2837,10 +2837,10 @@ contrat salarié . CSG et CRDS . CSG . base:
assiette: assiette de base
composantes:
- attributs:
impôt sur le revenu: déductible
nom: déductible
taux: taux déductible
- attributs:
impôt sur le revenu: non déductible
nom: non déductible
taux: taux non déductible
contrat salarié . CSG et CRDS . CSG . heures supplémentaires et complémentaires défiscalisées:
@ -2954,11 +2954,10 @@ contrat salarié . FNAL:
- si: éligible taux réduit
alors: 0.1%
- sinon: 0.5%
variations:
- si: éligible taux réduit
alors:
plafond: plafond sécurité sociale
- sinon: rien
plafond:
applicable si: éligible taux réduit
valeur: plafond sécurité sociale
exemples:
- nom: SMIC
situation:
@ -3020,18 +3019,19 @@ contrat salarié . maladie:
assiette: cotisations . assiette
composantes:
- attributs:
nom: maladie, maternité, invalidité, décès
dû par: employeur
taux: taux employeur
nom: employeur
composantes:
- attributs:
nom: maladie, maternité, invalidité, décès
taux: taux employeur
- attributs:
nom: contribution solidarité autonomie
taux: taux solidarité autonomie
- attributs:
nom: maladie, maternité, invalidité, décès
dû par: salarié
nom: salarié
titre: maladie, maternité, invalidité, décès salarié
taux: taux salarié
- attributs:
nom: Contribution Solidarité Autonomie
dû par: employeur
taux: taux solidarité autonomie
contrat salarié . maladie . taux solidarité autonomie:
acronyme: CSA
formule: 0.3%
@ -3301,14 +3301,14 @@ contrat salarié . profession spécifique . journaliste:
contrat salarié . profession spécifique . journaliste . réduction de taux:
applicable si: profession spécifique = 'journaliste'
remplace:
- règle: vieillesse . taux employeur plafonné
par: vieillesse . taux employeur plafonné * réduction de taux
- règle: vieillesse . taux employeur déplafonné
par: vieillesse . taux employeur déplafonné * réduction de taux
- règle: vieillesse . taux salarié plafonné
par: vieillesse . taux salarié plafonné * réduction de taux
- règle: vieillesse . taux salarié déplafonné
par: vieillesse . taux salarié déplafonné * réduction de taux
- règle: vieillesse . employeur . plafonnée . taux
par: vieillesse . employeur . plafonnée . taux * réduction de taux
- règle: vieillesse . employeur . déplafonnée . taux
par: vieillesse . employeur . déplafonnée . taux * réduction de taux
- règle: vieillesse . salarié . plafonnée . taux
par: vieillesse . salarié . plafonnée . taux * réduction de taux
- règle: vieillesse . salarié . déplafonnée . taux
par: vieillesse . salarié . déplafonnée . taux * réduction de taux
- règle: allocations familiales . taux
par: allocations familiales . taux * réduction de taux
@ -3442,21 +3442,25 @@ contrat salarié . vieillesse:
assiette: cotisations . assiette
composantes:
- attributs:
dû par: salarié
nom: salarié
assiette: cotisations . assiette . salariale
composantes:
- nom: déplafonnée
taux [ref taux salarié déplafonné]: 0.4%
- nom: plafonnée
taux [ref taux salarié plafonné]: 6.90%
- attributs:
nom: déplafonnée
taux [ref]: 0.4%
- attributs:
nom: plafonnée
taux [ref]: 6.90%
plafond: plafond sécurité sociale
- attributs:
dû par: employeur
nom: employeur
composantes:
- nom: déplafonnée
taux [ref taux employeur déplafonné]: 1.9%
- nom: plafonnée
taux [ref taux employeur plafonné]: 8.55%
- attributs:
nom: déplafonnée
taux [ref]: 1.9%
- attributs:
nom: plafonnée
taux [ref]: 8.55%
plafond: plafond sécurité sociale
exemples:
@ -3533,15 +3537,15 @@ contrat salarié . lodeom . réduction outre-mer:
formule:
somme:
- allocations familiales
- FNAL .employeur
- maladie .employeur
- vieillesse .employeur
- FNAL
- maladie . employeur
- vieillesse . employeur
- produit:
assiette: cotisations . assiette
taux: ATMP . taux minimum
- retraite complémentaire .employeur
- contribution d'équilibre général .employeur
- chômage .employeur
- retraite complémentaire . employeur
- contribution d'équilibre général . employeur
- chômage . employeur
plafond:
variations:
- si:
@ -3719,7 +3723,7 @@ contrat salarié . cotisations . assiette forfaitaire . rémunération réelle:
contrat salarié . convention collective:
par défaut: "'droit commun'"
question: "Quelle convention collective est applicable à l'entreprise ? [beta] "
question: "Quelle convention collective est applicable à l'entreprise ?"
formule:
une possibilité:
choix obligatoire: oui

View File

@ -14,7 +14,7 @@ questions:
- contrat salarié . profession spécifique
- établissement . localisation
unités par défaut: [€/mois]
unité par défaut: €/mois
situation:
dirigeant: non
contrat salarié . activité partielle: oui

View File

@ -1,11 +1,11 @@
import { AssertionError } from 'chai'
import Engine, { parseRules } from 'publicodes'
import Engine, { parsePublicodes } from 'publicodes'
import { disambiguateRuleReference } from '../../publicodes/source/ruleUtils'
import rules from 'Rules'
// les variables dans les tests peuvent être exprimées relativement à l'espace de nom de la règle,
// comme dans sa formule
let parsedRules = parseRules(rules)
let parsedRules = parsePublicodes(rules)
const engine = new Engine(parsedRules)
let runExamples = (examples, rule) =>
examples.map(ex => {
@ -13,14 +13,14 @@ let runExamples = (examples, rule) =>
const situation = Object.entries(ex.situation).reduce(
(acc, [name, value]) => ({
...acc,
[disambiguateRuleReference(parsedRules, rule.dottedName, name)]: value
[disambiguateRuleReference(engine.parsedRules, rule.dottedName, name)]: value
}),
{}
)
const evaluation = engine
.setSituation(situation)
.evaluate(rule.dottedName, {
unit: ex['unités par défaut']?.[0] ?? rule['unité par défaut']
unit: rule['unité par défaut']
})
const ok =
evaluation.nodeValue === expected

View File

@ -379,17 +379,17 @@ lodeom innovation et croissance:
taux spécifiques retraite complémentaire:
- contrat salarié . rémunération . brut de base: 1521.22 €/mois
contrat salarié . retraite complémentaire . taux employeur tranche 1: 5.59
contrat salarié . retraite complémentaire . taux salarié tranche 1: 2.28
contrat salarié . retraite complémentaire . employeur . taux tranche 1: 5.59
contrat salarié . retraite complémentaire . salarié . taux tranche 1: 2.28
- contrat salarié . rémunération . brut de base: 2500 €/mois
contrat salarié . retraite complémentaire . taux employeur tranche 1: 5.59
contrat salarié . retraite complémentaire . taux salarié tranche 1: 2.28
contrat salarié . retraite complémentaire . employeur . taux tranche 1: 5.59
contrat salarié . retraite complémentaire . salarié . taux tranche 1: 2.28
- contrat salarié . rémunération . brut de base: 1521.22 €/mois
contrat salarié . retraite complémentaire . taux employeur tranche 1: 3.94
contrat salarié . retraite complémentaire . taux salarié tranche 1: 3.93
contrat salarié . retraite complémentaire . employeur . taux tranche 1: 3.94
contrat salarié . retraite complémentaire . salarié . taux tranche 1: 3.93
- contrat salarié . rémunération . brut de base: 2500 €/mois
contrat salarié . retraite complémentaire . taux employeur tranche 1: 3.94
contrat salarié . retraite complémentaire . taux salarié tranche 1: 3.93
contrat salarié . retraite complémentaire . employeur . taux tranche 1: 3.94
contrat salarié . retraite complémentaire . salarié . taux tranche 1: 3.93
CCN batiment:
- contrat salarié . rémunération . brut de base: 2500 €/mois

View File

@ -1,11 +1,11 @@
import { expect } from 'chai'
import { parseRules } from 'publicodes'
import { parsePublicodes } from 'publicodes'
import { uniq } from 'ramda'
import rawRules from '../source/rules'
import unitsTranslations from '../../publicodes/source/locales/units.yaml'
it('use unit that exists in publicode', () => {
const rules = parseRules(rawRules)
const rules = parsePublicodes(rawRules)
const units = uniq(
Object.keys(rules).reduce(
(prev, name) => [

View File

@ -118,16 +118,17 @@ prime faible salaire:
formule: 300€
```
<<<<<<< HEAD
On peut forcer la conversion des unités via la propriété `unité`, ou la notation
suffixée `[...]`.
=======
On peut forcer la conversion des unités via la propriété `unité`
>>>>>>> 30d2971e (:WIP: Délimitation des pistes de refacto (on y va à la masse de destruction))
```yaml
salaire:
unité: €/mois
formule: 3200
salaire annuel:
formule: salaire [k€/an]
formule: 3200 €/mois
unité: €/an
```
**Types de base disponibles pour la conversion :**

View File

@ -0,0 +1,82 @@
import graphlib from '@dagrejs/graphlib'
import * as R from 'ramda'
import parsePublicodes from '../parsePublicodes'
import { RuleNode } from '../rule'
import { reduceAST } from './index'
type RulesDependencies = Array<[string, Array<string>]>
type GraphCycles = Array<Array<string>>
type GraphCyclesWithDependencies = Array<RulesDependencies>
export function buildRulesDependencies(
parsedRules: Record<string, RuleNode>
): RulesDependencies {
return Object.entries(parsedRules).map(([name, node]) => [
name,
buildRuleDependancies(node)
])
}
function buildRuleDependancies(rule: RuleNode): Array<string> {
return reduceAST<string[]>(
(acc, node, fn) => {
switch (node.nodeKind) {
case 'replacement':
case 'inversion':
case 'une possibilité':
return acc
case 'reference':
return [...acc, node.dottedName as string]
case 'rule':
// Cycle from parent dependancies are ignored at runtime
return fn(rule.explanation.valeur)
}
},
[],
rule
)
}
function buildDependenciesGraph(rulesDeps: RulesDependencies): graphlib.Graph {
const g = new graphlib.Graph()
rulesDeps.forEach(([ruleDottedName, dependencies]) => {
dependencies.forEach(depDottedName => {
g.setEdge(ruleDottedName, depDottedName)
})
})
return g
}
type ArgsType<T> = T extends (...args: infer U) => any ? U : never
type RawRules = ArgsType<typeof parsePublicodes>[0]
export function cyclesInDependenciesGraph(rawRules: RawRules): GraphCycles {
const parsedRules = parsePublicodes(rawRules)
const rulesDependencies = buildRulesDependencies(parsedRules)
const dependenciesGraph = buildDependenciesGraph(rulesDependencies)
const cycles = graphlib.alg.findCycles(dependenciesGraph)
return cycles
}
/**
* This function is useful so as to print the dependencies at each node of the
* cycle.
* Indeed, the graphlib.findCycles function returns the cycle found using the
* Tarjan method, which is **not necessarily the smallest cycle**. However, the
* smallest cycle would be the most legibe one
*/
export function cyclicDependencies<Names extends string>(
rawRules: RawRules
): GraphCyclesWithDependencies {
const parsedRules = parsePublicodes(rawRules)
const rulesDependencies = buildRulesDependencies(parsedRules)
const dependenciesGraph = buildDependenciesGraph(rulesDependencies)
const cycles = graphlib.alg.findCycles(dependenciesGraph)
const rulesDependenciesObject = R.fromPairs(rulesDependencies)
return cycles.map(cycle =>
cycle.map(ruleName => [ruleName, rulesDependenciesObject[ruleName]])
)
}

View File

@ -0,0 +1,312 @@
import { mapObjIndexed } from 'ramda'
import { InternalError } from '../error'
import { TrancheNodes } from '../mecanisms/trancheUtils'
import { ReplacementNode } from '../replacement'
import { RuleNode } from '../rule'
import { ASTNode, NodeKind, TraverseFunction } from './types'
export function updateAST(
fn: (n: ASTNode, fn: (n: ASTNode) => ASTNode) => ASTNode | undefined | false
): (n: ASTNode) => ASTNode {
function traverseFn(node: ASTNode) {
const updatedNode = fn(node, traverseFn)
if (updatedNode === false) {
return node
}
if (updatedNode === undefined) {
return traverseASTNode(traverseFn, node)
}
return updatedNode
}
return traverseFn
}
export function reduceAST<T>(
fn: (acc: T, n: ASTNode, fn: (n: ASTNode) => T) => T | undefined,
start: T,
node: ASTNode
): T {
function traverseFn(acc: T, node: ASTNode): T {
const result = fn(acc, node, traverseFn.bind(null, start))
if (result === undefined) {
return gatherNodes(node).reduce(traverseFn, acc)
}
return result
}
return traverseFn(start, node)
}
function gatherNodes(node: ASTNode): ASTNode[] {
const nodes: ASTNode[] = []
traverseASTNode(node => {
nodes.push(node)
return node
}, node)
return nodes
}
export function traverseParsedRules(
fn: (n: ASTNode) => ASTNode,
parsedRules: Record<string, RuleNode>
): Record<string, ASTNode> {
return Object.fromEntries(
Object.entries(parsedRules).map(([name, rule]) => [name, fn(rule)])
)
}
const traverseASTNode: TraverseFunction<NodeKind> = (fn, node) => {
switch (node.nodeKind) {
case 'rule':
return traverseRuleNode(fn, node)
case 'reference':
case 'constant':
return traverseLeafNode(fn, node)
case 'applicable si':
case 'non applicable si':
return traverseApplicableNode(fn, node)
case 'arrondi':
return traverseArrondiNode(fn, node)
case 'barème':
case 'taux progressif':
case 'grille':
return traverseNodeWithTranches(fn, node)
case 'somme':
case 'une de ces conditions':
case 'une possibilité':
case 'toutes ces conditions':
case 'minimum':
case 'maximum':
return traverseArrayNode(fn, node)
case 'durée':
return traverseDuréeNode(fn, node)
case 'inversion':
return traverseInversionNode(fn, node)
case 'operation':
return traverseOperationNode(fn, node)
case 'par défaut':
return traverseParDéfautNode(fn, node)
case 'plancher':
return traversePlancherNode(fn, node)
case 'plafond':
return traversePlafondNode(fn, node)
case 'produit':
return traverseProductNode(fn, node)
case 'recalcul':
return traverseRecalculNode(fn, node)
case 'allègement':
return traverseReductionNode(fn, node)
case 'nom dans la situation':
return traverseSituationNode(fn, node)
case 'synchronisation':
return traverseSynchronisationNode(fn, node)
case 'unité':
return traverseUnitéNode(fn, node)
case 'variations':
return traverseVariationNode(fn, node)
case 'variable temporelle':
return traverseVariableTemporelle(fn, node)
case 'replacement':
return traverseReplacementNode(fn, node)
default:
throw new InternalError(node)
}
}
const traverseRuleNode: TraverseFunction<'rule'> = (fn, node) => ({
...node,
replacements: node.replacements.map(fn) as Array<ReplacementNode>,
suggestions: mapObjIndexed(fn, node.suggestions),
explanation: {
parent: node.explanation.parent && fn(node.explanation.parent),
valeur: fn(node.explanation.valeur)
}
})
const traverseReplacementNode: TraverseFunction<'replacement'> = (fn, node) =>
({
...node,
definitionRule: fn(node.definitionRule),
replacedReference: fn(node.replacedReference),
replacementNode: fn(node.replacementNode),
whiteListedNames: node.whiteListedNames.map(fn),
blackListedNames: node.blackListedNames.map(fn)
} as ReplacementNode)
const traverseLeafNode: TraverseFunction<'reference' | 'constant'> = (
_,
node
) => node
const traverseApplicableNode: TraverseFunction<
'applicable si' | 'non applicable si'
> = (fn, node) => ({
...node,
explanation: {
condition: fn(node.explanation.condition),
valeur: fn(node.explanation.valeur)
}
})
function traverseTranche(fn: (n: ASTNode) => ASTNode, tranches: TrancheNodes) {
return tranches.map(tranche => ({
...tranche,
...(tranche.plafond && { plafond: fn(tranche.plafond) }),
...('montant' in tranche && { montant: fn(tranche.montant) }),
...('taux' in tranche && { taux: fn(tranche.taux) })
}))
}
const traverseNodeWithTranches: TraverseFunction<
'barème' | 'taux progressif' | 'grille'
> = (fn, node) => ({
...node,
explanation: {
assiette: fn(node.explanation.assiette),
multiplicateur: fn(node.explanation.multiplicateur),
tranches: traverseTranche(fn, node.explanation.tranches)
}
})
const traverseArrayNode: TraverseFunction<
| 'maximum'
| 'minimum'
| 'somme'
| 'toutes ces conditions'
| 'une de ces conditions'
| 'une possibilité'
> = (fn, node) => ({
...node,
explanation: node.explanation.map(fn)
})
const traverseOperationNode: TraverseFunction<'operation'> = (fn, node) => ({
...node,
explanation: [fn(node.explanation[0]), fn(node.explanation[1])]
})
const traverseDuréeNode: TraverseFunction<'durée'> = (fn, node) => ({
...node,
explanation: {
depuis: fn(node.explanation.depuis),
"jusqu'à": fn(node.explanation["jusqu'à"])
}
})
const traverseInversionNode: TraverseFunction<'inversion'> = (fn, node) => ({
...node,
explanation: {
...node.explanation,
inversionCandidates: node.explanation.inversionCandidates.map(fn) as any // TODO
}
})
const traverseParDéfautNode: TraverseFunction<'par défaut'> = (fn, node) => ({
...node,
explanation: {
valeur: fn(node.explanation.valeur),
parDéfaut: fn(node.explanation.parDéfaut)
}
})
const traverseArrondiNode: TraverseFunction<'arrondi'> = (fn, node) => ({
...node,
explanation: {
valeur: fn(node.explanation.valeur),
arrondi: fn(node.explanation.arrondi)
}
})
const traversePlancherNode: TraverseFunction<'plancher'> = (fn, node) => ({
...node,
explanation: {
valeur: fn(node.explanation.valeur),
plancher: fn(node.explanation.plancher)
}
})
const traversePlafondNode: TraverseFunction<'plafond'> = (fn, node) => ({
...node,
explanation: {
valeur: fn(node.explanation.valeur),
plafond: fn(node.explanation.plafond)
}
})
const traverseProductNode: TraverseFunction<'produit'> = (fn, node) => ({
...node,
explanation: {
assiette: fn(node.explanation.assiette),
taux: fn(node.explanation.taux),
facteur: fn(node.explanation.facteur),
plafond: fn(node.explanation.plafond)
}
})
const traverseRecalculNode: TraverseFunction<'recalcul'> = (fn, node) => ({
...node,
explanation: {
amendedSituation: node.explanation.amendedSituation.map(([name, value]) => [
fn(name),
fn(value)
]) as any, //TODO
recalcul: fn(node.explanation.recalcul)
}
})
const traverseReductionNode: TraverseFunction<'allègement'> = (fn, node) => ({
...node,
explanation: {
assiette: fn(node.explanation.assiette),
abattement: fn(node.explanation.abattement),
plafond: fn(node.explanation.plafond)
}
})
const traverseSituationNode: TraverseFunction<'nom dans la situation'> = (
fn,
node
) => ({
...node,
explanation: {
...node.explanation,
...(node.explanation.situationValeur && {
situationValeur: fn(node.explanation.situationValeur)
}),
valeur: fn(node.explanation.valeur)
}
})
const traverseSynchronisationNode: TraverseFunction<'synchronisation'> = (
fn,
node
) => ({
...node,
explanation: {
...node.explanation,
data: fn(node.explanation.data)
}
})
const traverseUnitéNode: TraverseFunction<'unité'> = (fn, node) => ({
...node,
explanation: fn(node.explanation)
})
const traverseVariationNode: TraverseFunction<'variations'> = (fn, node) => ({
...node,
explanation: node.explanation.map(({ condition, consequence }) => ({
condition: fn(condition),
consequence: fn(consequence)
}))
})
const traverseVariableTemporelle: TraverseFunction<'variable temporelle'> = (
fn,
node
) => ({
...node,
explanation: {
period: {
end: node.explanation.period.end && fn(node.explanation.period.end),
start: node.explanation.period.start && fn(node.explanation.period.start)
},
value: fn(node.explanation.value)
}
})

View File

@ -0,0 +1,103 @@
import { ApplicableSiNode } from '../mecanisms/applicable'
import { ArrondiNode } from '../mecanisms/arrondi'
import { OperationNode } from '../mecanisms/operation'
import { BarèmeNode } from '../mecanisms/barème'
import { ReferenceNode } from '../reference'
import { RuleNode } from '../rule'
import { TouteCesConditionsNode } from '../mecanisms/condition-allof'
import { UneDeCesConditionsNode } from '../mecanisms/condition-oneof'
import { DuréeNode } from '../mecanisms/durée'
import { GrilleNode } from '../mecanisms/grille'
import { InversionNode } from '../mecanisms/inversion'
import { MaxNode } from '../mecanisms/max'
import { PlafondNode } from '../mecanisms/plafond'
import { MinNode } from '../mecanisms/min'
import { NonApplicableSiNode } from '../mecanisms/nonApplicable'
import { ParDéfautNode } from '../mecanisms/parDéfaut'
import { PlancherNode } from '../mecanisms/plancher'
import { ProductNode } from '../mecanisms/product'
import { RecalculNode } from '../mecanisms/recalcul'
import { ReductionNode } from '../mecanisms/reduction'
import { PossibilityNode } from '../mecanisms/one-possibility'
import { SituationNode } from '../mecanisms/situation'
import { SommeNode } from '../mecanisms/sum'
import { SynchronisationNode } from '../mecanisms/synchronisation'
import { TauxProgressifNode } from '../mecanisms/tauxProgressif'
import { UnitéNode } from '../mecanisms/unité'
import { VariableTemporelleNode } from '../mecanisms/variableTemporelle'
import { VariationNode } from '../mecanisms/variations'
import { ReplacementNode } from '../replacement'
import { Temporal } from '../temporal'
export type ConstantNode = {
type: 'boolean' | 'objet' | 'number' | 'string'
nodeValue: boolean | Object | number | string | null
jsx: any
nodeKind: 'constant'
isDefault: boolean
}
export type ASTNode = (
| RuleNode
| ReferenceNode
| ApplicableSiNode
| ArrondiNode
| BarèmeNode
| TouteCesConditionsNode
| UneDeCesConditionsNode
| DuréeNode
| GrilleNode
| MaxNode
| InversionNode
| MinNode
| NonApplicableSiNode
| OperationNode
| ParDéfautNode
| PossibilityNode
| PlafondNode
| PlancherNode
| ProductNode
| RecalculNode
| ReductionNode
| SituationNode
| SommeNode
| SynchronisationNode
| TauxProgressifNode
| UnitéNode
| VariableTemporelleNode
| VariationNode
| ConstantNode
| ReplacementNode
) &
(EvaluationDecoration | {}) // TODO : separate type for evaluated AST Tree
export type MecanismNode = Exclude<
ASTNode,
RuleNode | ConstantNode | ReferenceNode
>
export type NodeKind = ASTNode['nodeKind']
export type Value = ASTNode & {
nodeValue: number | string | boolean
}
export type TraverseFunction<Kind extends NodeKind> = (
fn: (n: ASTNode) => ASTNode,
node: ASTNode & { nodeKind: Kind }
) => ASTNode & { nodeKind: Kind }
type BaseUnit = string
export type Unit = {
numerators: Array<BaseUnit>
denominators: Array<BaseUnit>
}
export type Types = number | boolean | string | Object
// Idée : une évaluation est un n-uple : (value, unit, missingVariable, isApplicable)
// Une temporalEvaluation est une liste d'evaluation sur chaque période. : [(Evaluation, Period)]
export type Evaluation<T extends Types = Types> = T | false | null
export type EvaluationDecoration<T extends Types = Types> = {
nodeValue: Evaluation<T>
missingVariables: Partial<Record<string, number>>
unit?: Unit
temporalValue?: Temporal<Evaluation>
}

View File

@ -45,7 +45,9 @@ export function RuleLink<Name extends string>({
return (
<Link to={newPath} {...props}>
{children || rule.title}{' '}
{displayIcon && rule.icons && <span>{emoji(rule.icons)} </span>}
{displayIcon && rule.rawNode.icônes && (
<span>{emoji(rule.rawNode.icônes)} </span>
)}
</Link>
)
} else {

View File

@ -14,17 +14,17 @@ export { RuleLink } from './RuleLink'
import References from './rule/References'
export { References }
type DocumentationProps<Names extends string> = {
type DocumentationProps = {
documentationPath: string
engine: Engine<Names>
engine: Engine
language: 'fr' | 'en'
}
export function Documentation<Names extends string>({
export function Documentation({
documentationPath,
engine,
language = 'fr'
}: DocumentationProps<Names>) {
}: DocumentationProps) {
useEffect(() => {
if (language !== i18n.language) {
i18n.changeLanguage(language)

View File

@ -1,4 +1,4 @@
import React, { useContext, useState } from 'react'
import React, { useState } from 'react'
import { Trans } from 'react-i18next'
import styled from 'styled-components'
import mecanismsDoc from '../../../docs/mecanisms.yaml'
@ -6,18 +6,20 @@ import { makeJsx } from '../../evaluation'
import { formatValue } from '../../format'
import { simplifyNodeUnit } from '../../nodeUnits'
import {
EvaluatedNode,
ASTNode,
ConstantNode,
Evaluation,
EvaluatedRule,
EvaluationDecoration,
Types,
Unit
} from '../../types'
} from '../../AST/types'
import { capitalise0 } from '../../utils'
import { EngineContext } from '../contexts'
import Overlay from '../Overlay'
import { RuleLinkWithContext } from '../RuleLink'
import mecanismColors from './colors'
import MecanismExplanation from './Explanation'
import { ReferenceNode } from '../../reference'
import { RuleNode } from '../../rule'
type NodeValuePointerProps = {
data: Evaluation<Types>
unit: Unit
@ -105,7 +107,7 @@ export const InfixMecanism = ({
prefixed,
children
}: {
value: EvaluatedNode
value: ASTNode & EvaluationDecoration
children: React.ReactNode
prefixed?: boolean
}) => {
@ -229,22 +231,22 @@ const StyledMecanismName = styled.button<{ name: string; inline?: boolean }>`
`
// Un élément du graphe de calcul qui a une valeur interprétée (à afficher)
export function Leaf({
dottedName,
acronyme,
name,
explanation: { title },
nodeValue,
export function Leaf(
node: ReferenceNode &
EvaluationDecoration & { explanation: RuleNode; dottedName: string }
) {
const { dottedName, name, nodeValue, explanation: rule, unit } = node
unit
}: EvaluatedRule) {
const ruleTitle = title || capitalise0(name)
return (
<span className="variable filtered leaf">
<span className="nodeHead">
<RuleLinkWithContext dottedName={dottedName}>
<span className="name">
{acronyme ? <abbr title={ruleTitle}>{acronyme}</abbr> : ruleTitle}
{rule.rawRule.acronyme ? (
<abbr title={rule.title}>{rule.rawRule.acronyme}</abbr>
) : (
rule.title
)}
</span>
</RuleLinkWithContext>

View File

@ -1,7 +1,6 @@
import { any, identity, path } from 'ramda'
import React from 'react'
import { Trans } from 'react-i18next'
import { EvaluatedRule } from '../../types'
import { makeJsx } from '../../evaluation'
const Conditions = ({
@ -9,7 +8,7 @@ const Conditions = ({
parentDependencies,
'applicable si': applicable,
'non applicable si': notApplicable
}: EvaluatedRule) => {
}: any) => {
const listElements = [
...parentDependencies.map(
parentDependency =>
@ -21,7 +20,7 @@ const Conditions = ({
)
),
...disabledBy?.explanation?.isDisabledBy?.map(
(dependency: EvaluatedRule, i: number) =>
(dependency: any, i: number) =>
dependency?.nodeValue === true && (
<ShowIfDisabled dependency={dependency} key={`dependency ${i}`} />
)
@ -47,7 +46,7 @@ const Conditions = ({
) : null
}
function ShowIfDisabled({ dependency }: { dependency: EvaluatedRule }) {
function ShowIfDisabled({ dependency }: { dependency: any }) {
return (
<li>
<span style={{ background: 'var(--lighterColor)', fontWeight: 'bold' }}>
@ -58,7 +57,7 @@ function ShowIfDisabled({ dependency }: { dependency: EvaluatedRule }) {
)
}
export default function Algorithm({ rule }: { rule: EvaluatedRule }) {
export default function Algorithm({ rule }: { rule: any }) {
const formula =
rule.formule ||
(rule.category === 'variable' && rule.explanation.formule),

View File

@ -16,6 +16,7 @@ export default function Rule({ dottedName, engine, language }) {
return <p>Cette règle est introuvable dans la base</p>
}
const rule = engine.evaluate(dottedName)
// TODO affichage inline vs page
const isSetInStituation = engine.parsedSituation[dottedName] !== undefined
const { description, question } = rule
@ -107,12 +108,12 @@ export default function Rule({ dottedName, engine, language }) {
)
}
function AssociatedRules<Name extends string>({
function AssociatedRules({
dottedName,
engine
}: {
dottedName: Name
engine: Engine<Name>
dottedName: string
engine: Engine
}) {
const namespaceRules = Object.keys(engine.getParsedRules())
.filter(

View File

@ -4,20 +4,17 @@ import yaml from 'yaml'
import Engine, { formatValue } from '../../index'
import PublicodesBlock from '../PublicodesBlock'
type Props<Rules extends string> = { dottedName: Rules; engine: Engine<Rules> }
export default function RuleSource<Rules extends string>({
engine,
dottedName
}: Props<Rules>) {
type Props = { dottedName: string; engine: Engine }
export default function RuleSource({ engine, dottedName }: Props) {
const [showSource, setShowSource] = useState(false)
const { rawRule, dependencies } = engine.getParsedRules()[dottedName]
// When we import a rule in the Publicode Studio, we need to provide a
// simplified definition of its dependencies to avoid undefined references.
// We use the current situation value as their simplified definition.
const dependenciesValues = Object.fromEntries(
[...dependencies.values()].map(dottedNameDependency => [
dependencies.map(dottedNameDependency => [
dottedNameDependency,
formatValueForStudio(engine.evaluate(dottedNameDependency))
formatValueForStudio(engine.evaluate(dottedNameDependency as string))
])
)

View File

@ -1,585 +0,0 @@
/**
* Note: all here is strictly based on duck typing.
* We don't exepect the parent rule to explain the type of the contained formula, for example.
*/
import * as R from 'ramda'
import { ParsedRule } from '../types'
import { ArrondiExplanation } from '../mecanisms/arrondi'
export type OnOff = 'oui' | 'non'
export function isOnOff(a: string): a is OnOff {
return a === 'oui' || a === 'non'
}
// Note: to build type-guards, we would need to have a `isNames` guard. That's
// pretty cumbersome, so for now we rely on this.
export type WannabeDottedName = string
export function isWannabeDottedName(a: string): a is WannabeDottedName {
return typeof a === 'string'
}
export type ASTNode = { [_: string]: any | undefined }
export type RuleNode<Names extends string> = ASTNode & ParsedRule<Names>
export type RuleProp = ASTNode & {
category: 'ruleProp'
rulePropType: string
}
export function isRuleProp(node: ASTNode): node is RuleProp {
return (
(node as RuleProp).category === 'ruleProp' &&
typeof (node as RuleProp).rulePropType === 'string'
)
}
export type Formule<Names extends string> = RuleProp & {
name: 'formule'
rulePropType: 'formula'
explanation: FormuleExplanation<Names>
}
export function isFormule<Names extends string>(
node: ASTNode
): node is Formule<Names> {
const formule = node as Formule<Names>
return (
isRuleProp(node) &&
formule.name === 'formule' &&
formule.rulePropType === 'formula' &&
isFormuleExplanation<Names>(formule.explanation)
)
}
export type FormuleExplanation<Names extends string> =
| Value
| Operation
| Possibilities
| Possibilities2
| Reference<Names>
| AnyMechanism<Names>
export function isFormuleExplanation<Names extends string>(
node: ASTNode
): node is FormuleExplanation<Names> {
return (
isValue(node) ||
isOperation(node) ||
isReference(node) ||
isPossibilities(node) ||
isPossibilities2(node) ||
isAnyMechanism<Names>(node)
)
}
export type Value = ASTNode & {
nodeValue: number | string | boolean
}
export function isValue(node: ASTNode): node is Value {
const value = node as Value
return (
typeof value.nodeValue === 'string' ||
typeof value.nodeValue === 'number' ||
typeof value.nodeValue === 'boolean'
)
}
export type Operation = ASTNode & {
operationType: 'comparison' | 'calculation'
explanation: Array<ASTNode>
}
export function isOperation(node: ASTNode): node is Operation {
return R.includes((node as Operation).operationType, [
'comparison',
'calculation'
])
}
export type Possibilities = ASTNode & {
possibilités: Array<string>
'choix obligatoire'?: OnOff
'une possibilité': OnOff
}
export function isPossibilities(node: ASTNode): node is Possibilities {
const possibilities = node as Possibilities
return (
possibilities.possibilités instanceof Array &&
possibilities.possibilités.every(it => typeof it === 'string') &&
(possibilities['choix obligatoire'] === undefined ||
isOnOff(possibilities['choix obligatoire'])) &&
isOnOff(possibilities['une possibilité'])
)
}
export type Possibilities2 = ASTNode & {
[index: number]: string // short dotted name
'choix obligatoire'?: OnOff
'une possibilité': OnOff
}
export function isPossibilities2(node: ASTNode): node is Possibilities2 {
const possibilities2 = node as Possibilities2
return (
Object.entries(possibilities2).every(
([k, v]) => isNaN(parseInt(k, 10)) || typeof v === 'string'
) &&
(possibilities2['choix obligatoire'] === undefined ||
isOnOff(possibilities2['choix obligatoire'])) &&
isOnOff(possibilities2['une possibilité'])
)
}
export type Reference<Names extends string> = ASTNode & {
category: 'reference'
}
export function isReference<Names extends string>(
node: ASTNode
): node is Reference<Names> {
const reference = node as Reference<Names>
return reference.category === 'reference'
}
export type AbstractMechanism = ASTNode & {
category: 'mecanism'
name: string
}
export function isAbstractMechanism(node: ASTNode): node is AbstractMechanism {
return (
(node as AbstractMechanism).category === 'mecanism' &&
typeof (node as AbstractMechanism).name === 'string'
)
}
export type RecalculMech<Names extends string> = AbstractMechanism & {
explanation: {
recalcul: Reference<Names>
amendedSituation: Record<Names, Reference<Names>>
}
}
export function isRecalculMech<Names extends string>(
node: ASTNode
): node is RecalculMech<Names> {
const recalculMech = node as RecalculMech<Names>
const isReferenceSpec = isReference as (
node: ASTNode
) => node is Reference<Names>
return (
typeof recalculMech.explanation === 'object' &&
typeof recalculMech.explanation.recalcul === 'object' &&
isReferenceSpec(recalculMech.explanation.recalcul as ASTNode) &&
typeof recalculMech.explanation.amendedSituation === 'object'
)
}
export type PlafondMech = AbstractMechanism & {
name: 'plafond'
explanation: {
valeur: ASTNode
plafond: ASTNode
}
}
export function isPlafondMech(node: ASTNode): node is PlafondMech {
const encadrementMech = node as PlafondMech
return (
isAbstractMechanism(encadrementMech) &&
encadrementMech.name == 'plafond' &&
typeof encadrementMech.explanation === 'object' &&
encadrementMech.explanation.valeur !== undefined &&
encadrementMech.explanation.plafond !== undefined
)
}
export type PlancherMech = AbstractMechanism & {
name: 'plancher'
explanation: {
valeur: ASTNode
plancher: ASTNode
}
}
export function isPlancherMech(node: ASTNode): node is PlancherMech {
const encadrementMech = node as PlancherMech
return (
isAbstractMechanism(encadrementMech) &&
encadrementMech.name == 'plancher' &&
typeof encadrementMech.explanation === 'object' &&
encadrementMech.explanation.valeur !== undefined &&
encadrementMech.explanation.plancher !== undefined
)
}
export type ApplicableMech = AbstractMechanism & {
name: 'applicable si'
explanation: {
valeur: ASTNode
condition: ASTNode
}
}
export function isApplicableMech(node: ASTNode): node is ApplicableMech {
const mech = node as ApplicableMech
return (
isAbstractMechanism(mech) &&
mech.name == 'applicable si' &&
typeof mech.explanation === 'object' &&
mech.explanation.valeur !== undefined &&
mech.explanation.condition !== undefined
)
}
export type NonApplicableMech = AbstractMechanism & {
name: 'non applicable si'
explanation: {
valeur: ASTNode
condition: ASTNode
}
}
export function isNonApplicableMech(node: ASTNode): node is NonApplicableMech {
const mech = node as NonApplicableMech
return (
isAbstractMechanism(mech) &&
mech.name == 'non applicable si' &&
typeof mech.explanation === 'object' &&
mech.explanation.valeur !== undefined &&
mech.explanation.condition !== undefined
)
}
export type SommeMech = AbstractMechanism & {
name: 'somme'
explanation: Array<ASTNode>
}
export function isSommeMech(node: ASTNode): node is SommeMech {
const sommeMech = node as SommeMech
return (
isAbstractMechanism(sommeMech) &&
sommeMech.name === 'somme' &&
sommeMech.explanation instanceof Array
)
}
export type ProduitMech = AbstractMechanism & {
name: 'produit'
explanation: {
assiette: ASTNode
plafond: ASTNode
facteur: ASTNode
taux: ASTNode
}
}
export function isProduitMech(node: ASTNode): node is ProduitMech {
const produitMech = node as ProduitMech
return (
isAbstractMechanism(produitMech) &&
produitMech.name === 'produit' &&
typeof produitMech.explanation === 'object' &&
typeof produitMech.explanation.assiette === 'object' &&
typeof produitMech.explanation.plafond === 'object' &&
typeof produitMech.explanation.facteur === 'object' &&
typeof produitMech.explanation.taux === 'object'
)
}
export type VariationsMech = AbstractMechanism & {
name: 'variations'
explanation: {
condition: ASTNode
consequence: ASTNode
}[]
}
export function isVariationsMech(node: ASTNode): node is VariationsMech {
const variationsMech = node as VariationsMech
return (
isAbstractMechanism(variationsMech) &&
variationsMech.name === 'variations' &&
variationsMech.explanation instanceof Array &&
variationsMech.explanation.every(
variation =>
typeof variation === 'object' &&
variation.condition !== undefined &&
variation.consequence !== undefined
)
)
}
export type AllegementMech = AbstractMechanism & {
name: 'allègement'
explanation: {
abattement: ASTNode
assiette: ASTNode
plafond: ASTNode
}
}
export function isAllegementMech(node: ASTNode): node is AllegementMech {
const allegementMech = node as AllegementMech
return (
isAbstractMechanism(allegementMech) &&
allegementMech.name === 'allègement' &&
typeof allegementMech.explanation === 'object' &&
allegementMech.explanation.abattement !== undefined &&
allegementMech.explanation.assiette !== undefined &&
allegementMech.explanation.plafond !== undefined
)
}
export type BaremeMech = AbstractMechanism & {
name: 'barème'
explanation: {
assiette: ASTNode
multiplicateur: ASTNode
tranches: {
plafond: ASTNode
taux: ASTNode
}[]
}
}
export function isBaremeMech(node: ASTNode): node is BaremeMech {
const baremeMech = node as BaremeMech
return (
isAbstractMechanism(baremeMech) &&
baremeMech.name === 'barème' &&
typeof baremeMech.explanation === 'object' &&
baremeMech.explanation.assiette !== undefined &&
baremeMech.explanation.multiplicateur !== undefined &&
baremeMech.explanation.tranches instanceof Array &&
baremeMech.explanation.tranches.every(
tranche =>
typeof tranche === 'object' &&
tranche.plafond !== undefined &&
tranche.taux !== undefined
)
)
}
export type InversionNumMech<Names extends string> = AbstractMechanism & {
name: 'inversion numérique'
explanation: {
inversionCandidates: Array<Reference<Names>>
}
}
export function isInversionNumMech<Names extends string>(
node: ASTNode
): node is InversionNumMech<Names> {
const inversionNumMech = node as InversionNumMech<Names>
const isReferenceSpec = isReference as (
node: ASTNode
) => node is Reference<Names>
return (
isAbstractMechanism(inversionNumMech) &&
inversionNumMech.name === 'inversion numérique' &&
typeof inversionNumMech.explanation === 'object' &&
inversionNumMech.explanation.inversionCandidates instanceof Array &&
inversionNumMech.explanation.inversionCandidates.every(isReferenceSpec)
)
}
export type ArrondiMech = AbstractMechanism & {
name: 'arrondi'
explanation: Record<keyof ArrondiExplanation, ASTNode>
}
export function isArrondiMech(node: ASTNode): node is ArrondiMech {
const arrondiMech = node as ArrondiMech
return (
isAbstractMechanism(arrondiMech) &&
arrondiMech.name === 'arrondi' &&
typeof arrondiMech.explanation === 'object' &&
arrondiMech.explanation.arrondi !== undefined &&
arrondiMech.explanation.valeur !== undefined
)
}
export type MaxMech = AbstractMechanism & {
name: 'le maximum de'
explanation: Array<ASTNode>
}
export function isMaxMech(node: ASTNode): node is MaxMech {
const maxMech = node as MaxMech
return (
isAbstractMechanism(maxMech) &&
maxMech.name === 'le maximum de' &&
maxMech.explanation instanceof Array
)
}
export type MinMech = AbstractMechanism & {
name: 'le minimum de'
explanation: Array<ASTNode>
}
export function isMinMech(node: ASTNode): node is MinMech {
const minMech = node as MinMech
return (
isAbstractMechanism(minMech) &&
minMech.name === 'le minimum de' &&
minMech.explanation instanceof Array
)
}
export type ComposantesMech = AbstractMechanism & {
name: 'composantes'
explanation: Array<ASTNode>
}
export function isComposantesMech(node: ASTNode): node is ComposantesMech {
const composantesMech = node as ComposantesMech
return (
isAbstractMechanism(composantesMech) &&
composantesMech.name === 'composantes' &&
composantesMech.explanation instanceof Array
)
}
export type UneConditionsMech = AbstractMechanism & {
name: 'une de ces conditions'
explanation: Array<ASTNode>
}
export function isUneConditionsMech(node: ASTNode): node is UneConditionsMech {
const uneConditionsMech = node as UneConditionsMech
return (
isAbstractMechanism(uneConditionsMech) &&
uneConditionsMech.name === 'une de ces conditions' &&
uneConditionsMech.explanation instanceof Array
)
}
export type ToutesConditionsMech = AbstractMechanism & {
name: 'toutes ces conditions'
explanation: Array<ASTNode>
}
export function isToutesConditionsMech(
node: ASTNode
): node is ToutesConditionsMech {
const toutesConditionsMech = node as ToutesConditionsMech
return (
isAbstractMechanism(toutesConditionsMech) &&
toutesConditionsMech.name === 'toutes ces conditions' &&
toutesConditionsMech.explanation instanceof Array
)
}
export type SyncMech = AbstractMechanism & {
name: 'synchronisation'
API: any
}
export function isSyncMech(node: ASTNode): node is SyncMech {
const syncMech = node as SyncMech
return isAbstractMechanism(syncMech) && syncMech.name === 'synchronisation'
}
export type GrilleMech = AbstractMechanism & {
name: 'grille'
explanation: {
assiette: ASTNode
multiplicateur: ASTNode
tranches: {
montant: ASTNode
plafond: ASTNode
}[]
}
}
export function isGrilleMech(node: ASTNode): node is GrilleMech {
const grilleMech = node as GrilleMech
return (
isAbstractMechanism(grilleMech) &&
grilleMech.name === 'grille' &&
typeof grilleMech.explanation === 'object' &&
grilleMech.explanation.assiette !== undefined &&
grilleMech.explanation.multiplicateur !== undefined &&
grilleMech.explanation.tranches instanceof Array &&
grilleMech.explanation.tranches.every(
tranche =>
typeof tranche === 'object' &&
tranche.montant !== undefined &&
tranche.plafond !== undefined
)
)
}
export type TauxProgMech = AbstractMechanism & {
name: 'taux progressif'
explanation: {
assiette: ASTNode
multiplicateur: ASTNode
tranches: {
plafond: ASTNode
taux: ASTNode
}[]
}
}
export function isTauxProgMech(node: ASTNode): node is TauxProgMech {
const tauxProgMech = node as TauxProgMech
return (
isAbstractMechanism(tauxProgMech) &&
tauxProgMech.name === 'taux progressif' &&
typeof tauxProgMech.explanation === 'object' &&
tauxProgMech.explanation.assiette !== undefined &&
tauxProgMech.explanation.multiplicateur !== undefined &&
tauxProgMech.explanation.tranches instanceof Array &&
tauxProgMech.explanation.tranches.every(
tranche =>
typeof tranche === 'object' &&
tranche.plafond !== undefined &&
tranche.taux !== undefined
)
)
}
export type DureeMech = AbstractMechanism & {
name: 'Durée'
explanation: {
depuis: ASTNode
"jusqu'à": ASTNode
}
}
export function isDureeMech(node: ASTNode): node is DureeMech {
const dureeMech = node as DureeMech
return (
isAbstractMechanism(dureeMech) &&
dureeMech.name === 'Durée' &&
typeof dureeMech.explanation === 'object' &&
dureeMech.explanation.depuis !== undefined &&
dureeMech.explanation["jusqu'à"] !== undefined
)
}
export type AnyMechanism<Names extends string> =
| RecalculMech<Names>
| PlancherMech
| PlafondMech
| ApplicableMech
| NonApplicableMech
| SommeMech
| ProduitMech
| VariationsMech
| AllegementMech
| BaremeMech
| InversionNumMech<Names>
| ArrondiMech
| MaxMech
| MinMech
| ComposantesMech
| UneConditionsMech
| ToutesConditionsMech
| SyncMech
| GrilleMech
| TauxProgMech
| DureeMech
export function isAnyMechanism<Names extends string>(
node: ASTNode
): node is AnyMechanism<Names> {
return (
isRecalculMech<Names>(node) ||
isPlafondMech(node) ||
isPlancherMech(node) ||
isApplicableMech(node) ||
isNonApplicableMech(node) ||
isSommeMech(node) ||
isProduitMech(node) ||
isVariationsMech(node) ||
isAllegementMech(node) ||
isBaremeMech(node) ||
isInversionNumMech<Names>(node) ||
isArrondiMech(node) ||
isMaxMech(node) ||
isMinMech(node) ||
isComposantesMech(node) ||
isUneConditionsMech(node) ||
isToutesConditionsMech(node) ||
isSyncMech(node) ||
isGrilleMech(node) ||
isTauxProgMech(node) ||
isDureeMech(node)
)
}

View File

@ -1,62 +0,0 @@
import * as R from 'ramda'
import graphlib from '@dagrejs/graphlib'
import parseRules from '../parseRules'
import { Rules } from '../types'
import {
buildRulesDependencies,
RuleDependencies,
RulesDependencies
} from './rulesDependencies'
type GraphNodeRepr<Names extends string> = Names
type GraphCycles<Names extends string> = Array<Array<GraphNodeRepr<Names>>>
type GraphCyclesWithDependencies<Names extends string> = Array<
Array<[GraphNodeRepr<Names>, RuleDependencies<Names>]>
>
function buildDependenciesGraph<Names extends string>(
rulesDeps: RulesDependencies<Names>
): graphlib.Graph {
const g = new graphlib.Graph()
rulesDeps.forEach(([ruleDottedName, dependencies]) => {
dependencies.forEach(depDottedName => {
g.setEdge(ruleDottedName, depDottedName)
})
})
return g
}
export function cyclesInDependenciesGraph<Names extends string>(
rawRules: Rules<Names> | string
): GraphCycles<Names> {
const parsedRules = parseRules(rawRules)
const rulesDependencies = buildRulesDependencies(parsedRules)
const dependenciesGraph = buildDependenciesGraph(rulesDependencies)
const cycles = graphlib.alg.findCycles(dependenciesGraph)
return cycles
}
/**
* This function is useful so as to print the dependencies at each node of the
* cycle.
* Indeed, the graphlib.findCycles function returns the cycle found using the
* Tarjan method, which is **not necessarily the smallest cycle**. However, the
* smallest cycle would be the most legibe one
*/
export function cyclicDependencies<Names extends string>(
rawRules: Rules<Names> | string
): GraphCyclesWithDependencies<Names> {
const parsedRules = parseRules(rawRules)
const rulesDependencies = buildRulesDependencies(parsedRules)
const dependenciesGraph = buildDependenciesGraph(rulesDependencies)
const cycles = graphlib.alg.findCycles(dependenciesGraph)
const rulesDependenciesObject = R.fromPairs(rulesDependencies)
return cycles.map(cycle =>
cycle.map(ruleName => [ruleName, rulesDependenciesObject[ruleName]])
)
}

View File

@ -1,3 +0,0 @@
import { cyclicDependencies } from './graph'
export default { cyclicDependencies }

View File

@ -1,418 +0,0 @@
import * as R from 'ramda'
import { ParsedRules } from '../types'
import * as ASTTypes from './ASTTypes'
export type RuleDependencies<Names extends string> = Array<Names>
export type RulesDependencies<Names extends string> = Array<
[Names, RuleDependencies<Names>]
>
export function ruleDepsOfNode<Names extends string>(
ruleName: Names,
node: ASTTypes.ASTNode
): RuleDependencies<Names> {
function ruleDepsOfFormule(
formule: ASTTypes.Formule<Names>
): RuleDependencies<Names> {
return ruleDepsOfNode(ruleName, formule.explanation)
}
function ruleDepsOfValue(value: ASTTypes.Value): RuleDependencies<Names> {
return []
}
function ruleDepsOfOperation(
operation: ASTTypes.Operation
): RuleDependencies<Names> {
return operation.explanation.flatMap(
R.partial<Names, ASTTypes.ASTNode, RuleDependencies<Names>>(
ruleDepsOfNode,
[ruleName]
)
)
}
function ruleDepsOfPossibilities(
possibilities: ASTTypes.Possibilities
): RuleDependencies<Names> {
return []
}
function ruleDepsOfPossibilities2(
possibilities: ASTTypes.Possibilities2
): RuleDependencies<Names> {
return []
}
function ruleDepsOfReference(
reference: ASTTypes.Reference<Names>
): RuleDependencies<Names> {
return [reference.dottedName]
}
function ruleDepsOfRecalculMech(
recalculMech: ASTTypes.RecalculMech<Names>
): RuleDependencies<Names> {
const ruleReference = recalculMech.explanation.recalcul.partialReference
return ruleReference === ruleName ? [] : [ruleReference]
}
function ruleDepsOfPlafondMech(
encadrementMech: ASTTypes.PlafondMech
): RuleDependencies<Names> {
const result = [
encadrementMech.explanation.plafond,
encadrementMech.explanation.valeur
].flatMap(
R.partial<Names, ASTTypes.ASTNode, RuleDependencies<Names>>(
ruleDepsOfNode,
[ruleName]
)
)
return result
}
function ruleDepsOfPlancherMech(
mech: ASTTypes.PlancherMech
): RuleDependencies<Names> {
const result = [mech.explanation.plancher, mech.explanation.valeur].flatMap(
R.partial<Names, ASTTypes.ASTNode, RuleDependencies<Names>>(
ruleDepsOfNode,
[ruleName]
)
)
return result
}
function ruleDepsOfApplicableMech(
mech: ASTTypes.ApplicableMech | ASTTypes.NonApplicableMech
): RuleDependencies<Names> {
const result = [
mech.explanation.condition,
mech.explanation.valeur
].flatMap(
R.partial<Names, ASTTypes.ASTNode, RuleDependencies<Names>>(
ruleDepsOfNode,
[ruleName]
)
)
return result
}
function ruleDepsOfSommeMech(
sommeMech: ASTTypes.SommeMech
): RuleDependencies<Names> {
const result = sommeMech.explanation.flatMap(
R.partial<Names, ASTTypes.ASTNode, RuleDependencies<Names>>(
ruleDepsOfNode,
[ruleName]
)
)
return result
}
function ruleDepsOfProduitMech(
produitMech: ASTTypes.ProduitMech
): RuleDependencies<Names> {
const result = [
produitMech.explanation.assiette,
produitMech.explanation.plafond,
produitMech.explanation.facteur,
produitMech.explanation.taux
].flatMap(
R.partial<Names, ASTTypes.ASTNode, RuleDependencies<Names>>(
ruleDepsOfNode,
[ruleName]
)
)
return result
}
function ruleDepsOfVariationsMech(
variationsMech: ASTTypes.VariationsMech
): RuleDependencies<Names> {
function ruleOfVariation({
condition,
consequence
}: {
condition: ASTTypes.ASTNode
consequence: ASTTypes.ASTNode
}): RuleDependencies<Names> {
return R.concat(
ruleDepsOfNode<Names>(ruleName, condition),
ruleDepsOfNode<Names>(ruleName, consequence)
)
}
const result = variationsMech.explanation.flatMap(ruleOfVariation)
return result
}
function ruleDepsOfAllegementMech(
allegementMech: ASTTypes.AllegementMech
): RuleDependencies<Names> {
const subNodes = [
allegementMech.explanation.abattement,
allegementMech.explanation.assiette,
allegementMech.explanation.plafond
]
const result = subNodes.flatMap(
R.partial<Names, ASTTypes.ASTNode, RuleDependencies<Names>>(
ruleDepsOfNode,
[ruleName]
)
)
return result
}
function ruleDepsOfBaremeMech(
baremeMech: ASTTypes.BaremeMech
): RuleDependencies<Names> {
const tranchesNodes = baremeMech.explanation.tranches.flatMap(
({ plafond, taux }) => [plafond, taux]
)
const result = R.concat(
[baremeMech.explanation.assiette, baremeMech.explanation.multiplicateur],
tranchesNodes
).flatMap(
R.partial<Names, ASTTypes.ASTNode, RuleDependencies<Names>>(
ruleDepsOfNode,
[ruleName]
)
)
return result
}
/**
* Returns 0 dependency for _inversion numérique_ as it's not creating a logical dependency.
*/
function ruleDepsOfInversionNumMech(
inversionNumMech: ASTTypes.InversionNumMech<Names>
): RuleDependencies<Names> {
return []
}
function ruleDepsOfArrondiMech(
arrondiMech: ASTTypes.ArrondiMech
): RuleDependencies<Names> {
const result = [
arrondiMech.explanation.arrondi,
arrondiMech.explanation.valeur
].flatMap(
R.partial<Names, ASTTypes.ASTNode, RuleDependencies<Names>>(
ruleDepsOfNode,
[ruleName]
)
)
return result
}
function ruleDepsOfMaxMech(
maxMech: ASTTypes.MaxMech
): RuleDependencies<Names> {
const result = maxMech.explanation.flatMap(
R.partial<Names, ASTTypes.ASTNode, RuleDependencies<Names>>(
ruleDepsOfNode,
[ruleName]
)
)
return result
}
function ruleDepsOfMinMech(
minMech: ASTTypes.MinMech
): RuleDependencies<Names> {
const result = minMech.explanation.flatMap(
R.partial<Names, ASTTypes.ASTNode, RuleDependencies<Names>>(
ruleDepsOfNode,
[ruleName]
)
)
return result
}
function ruleDepsOfComposantesMech(
composantesMech: ASTTypes.ComposantesMech
): RuleDependencies<Names> {
const result = composantesMech.explanation.flatMap(
R.partial<Names, ASTTypes.ASTNode, RuleDependencies<Names>>(
ruleDepsOfNode,
[ruleName]
)
)
return result
}
function ruleDepsOfUneConditionsMech(
uneConditionsMech: ASTTypes.UneConditionsMech
): RuleDependencies<Names> {
const result = uneConditionsMech.explanation.flatMap(
R.partial<Names, ASTTypes.ASTNode, RuleDependencies<Names>>(
ruleDepsOfNode,
[ruleName]
)
)
return result
}
function ruleDepsOfToutesConditionsMech(
toutesConditionsMech: ASTTypes.ToutesConditionsMech
): RuleDependencies<Names> {
const result = toutesConditionsMech.explanation.flatMap(
R.partial<Names, ASTTypes.ASTNode, RuleDependencies<Names>>(
ruleDepsOfNode,
[ruleName]
)
)
return result
}
function ruleDepsOfSyncMech(_: ASTTypes.SyncMech): RuleDependencies<Names> {
return []
}
function ruleDepsOfGrilleMech(
grilleMech: ASTTypes.GrilleMech
): RuleDependencies<Names> {
const tranchesNodes = grilleMech.explanation.tranches.flatMap(
({ montant, plafond }) => [montant, plafond]
)
const result = R.concat(
[grilleMech.explanation.assiette, grilleMech.explanation.multiplicateur],
tranchesNodes
).flatMap(
R.partial<Names, ASTTypes.ASTNode, RuleDependencies<Names>>(
ruleDepsOfNode,
[ruleName]
)
)
return result
}
function ruleDepsOfTauxProgMech(
tauxProgMech: ASTTypes.TauxProgMech
): RuleDependencies<Names> {
const tranchesNodes = tauxProgMech.explanation.tranches.flatMap(
({ plafond, taux }) => [plafond, taux]
)
const result = R.concat(
[
tauxProgMech.explanation.assiette,
tauxProgMech.explanation.multiplicateur
],
tranchesNodes
).flatMap(
R.partial<Names, ASTTypes.ASTNode, RuleDependencies<Names>>(
ruleDepsOfNode,
[ruleName]
)
)
return result
}
function ruleDepsOfDureeMech(
dureeMech: ASTTypes.DureeMech
): RuleDependencies<Names> {
const result = [
dureeMech.explanation.depuis,
dureeMech.explanation["jusqu'à"]
].flatMap(
R.partial<Names, ASTTypes.ASTNode, RuleDependencies<Names>>(
ruleDepsOfNode,
[ruleName]
)
)
return result
}
let result
if (ASTTypes.isFormule<Names>(node)) {
result = ruleDepsOfFormule(node)
} else if (ASTTypes.isValue(node)) {
result = ruleDepsOfValue(node)
} else if (ASTTypes.isOperation(node)) {
result = ruleDepsOfOperation(node)
} else if (ASTTypes.isReference<Names>(node)) {
result = ruleDepsOfReference(node)
} else if (ASTTypes.isPossibilities(node)) {
result = ruleDepsOfPossibilities(node)
} else if (ASTTypes.isPossibilities2(node)) {
result = ruleDepsOfPossibilities2(node)
} else if (ASTTypes.isRecalculMech<Names>(node)) {
result = ruleDepsOfRecalculMech(node)
} else if (ASTTypes.isApplicableMech(node)) {
result = ruleDepsOfApplicableMech(node)
} else if (ASTTypes.isNonApplicableMech(node)) {
result = ruleDepsOfApplicableMech(node)
} else if (ASTTypes.isPlafondMech(node)) {
result = ruleDepsOfPlafondMech(node)
} else if (ASTTypes.isPlancherMech(node)) {
result = ruleDepsOfPlancherMech(node)
} else if (ASTTypes.isSommeMech(node)) {
result = ruleDepsOfSommeMech(node)
} else if (ASTTypes.isProduitMech(node)) {
result = ruleDepsOfProduitMech(node)
} else if (ASTTypes.isVariationsMech(node)) {
result = ruleDepsOfVariationsMech(node)
} else if (ASTTypes.isAllegementMech(node)) {
result = ruleDepsOfAllegementMech(node)
} else if (ASTTypes.isBaremeMech(node)) {
result = ruleDepsOfBaremeMech(node)
} else if (ASTTypes.isInversionNumMech<Names>(node)) {
result = ruleDepsOfInversionNumMech(node)
} else if (ASTTypes.isArrondiMech(node)) {
result = ruleDepsOfArrondiMech(node)
} else if (ASTTypes.isMaxMech(node)) {
result = ruleDepsOfMaxMech(node)
} else if (ASTTypes.isMinMech(node)) {
result = ruleDepsOfMinMech(node)
} else if (ASTTypes.isComposantesMech(node)) {
result = ruleDepsOfComposantesMech(node)
} else if (ASTTypes.isUneConditionsMech(node)) {
result = ruleDepsOfUneConditionsMech(node)
} else if (ASTTypes.isToutesConditionsMech(node)) {
result = ruleDepsOfToutesConditionsMech(node)
} else if (ASTTypes.isSyncMech(node)) {
result = ruleDepsOfSyncMech(node)
} else if (ASTTypes.isGrilleMech(node)) {
result = ruleDepsOfGrilleMech(node)
} else if (ASTTypes.isTauxProgMech(node)) {
result = ruleDepsOfTauxProgMech(node)
} else if (ASTTypes.isDureeMech(node)) {
result = ruleDepsOfDureeMech(node)
}
if (result === undefined) {
throw new Error(
`This node doesn't have a visitor method defined: ${node.name}`
)
}
return result
}
function ruleDepsOfRuleNode<Names extends string>(
ruleNode: ASTTypes.RuleNode<Names>
): RuleDependencies<Names> {
return ruleNode.formule === undefined
? []
: ruleDepsOfNode(ruleNode.dottedName, ruleNode.formule)
}
export function buildRulesDependencies<Names extends string>(
parsedRules: ParsedRules<Names>
): RulesDependencies<Names> {
// This stringPairs thing is necessary because `toPairs` is strictly considering that
// object keys are strings (same for `Object.entries`). Maybe we should build our own
// `toPairs`?
const stringPairs: Array<[string, ASTTypes.RuleNode<Names>]> = Object.entries(
parsedRules
)
const pairs: Array<[Names, ASTTypes.RuleNode<Names>]> = stringPairs as Array<
[Names, ASTTypes.RuleNode<Names>]
>
return pairs.map(
([dottedName, ruleNode]: [Names, ASTTypes.RuleNode<Names>]): [
Names,
RuleDependencies<Names>
] => [dottedName, ruleDepsOfRuleNode<Names>(ruleNode)]
)
}

View File

@ -70,3 +70,18 @@ export function warning(
`
)
}
export class InternalError extends EngineError {
constructor(payload) {
super(
`
Erreur interne du moteur.
Cette erreur est le signe d'un bug dans publicodes. Pour nous aider à le résoudre, vous pouvez copier ce texte dans un nouveau ticket : https://github.com/betagouv/mon-entreprise/issues/new.
payload:
\t${JSON.stringify(payload, null, 2)}
`
)
}
}

View File

@ -1,152 +0,0 @@
import { map, pick, pipe } from 'ramda'
import { evaluationFunction } from '.'
import { typeWarning } from './error'
import { bonus, mergeAllMissing, mergeMissing } from './evaluation'
import { convertNodeToUnit } from './nodeUnits'
export const evaluateApplicability: evaluationFunction = function(node: any) {
const cacheKey = `${node.dottedName} [applicability]`
if (node.dottedName && this.cache[cacheKey]) {
return this.cache[cacheKey]
}
const evaluatedAttributes = pipe(
pick(['non applicable si', 'applicable si', 'rendu non applicable']) as (
x: any
) => any,
map(value => this.evaluateNode(value))
)(node) as any,
{
'non applicable si': notApplicable,
'applicable si': applicable,
'rendu non applicable': disabled
} = evaluatedAttributes,
parentDependencies = node.parentDependencies.map(parent =>
this.evaluateNode(parent)
)
const anyDisabledParent = parentDependencies.find(
parent => parent?.nodeValue === false
)
const { nodeValue, missingVariables = {} } = anyDisabledParent
? anyDisabledParent
: notApplicable?.nodeValue === true
? {
nodeValue: false,
missingVariables: notApplicable.missingVariables
}
: disabled?.nodeValue === true
? { nodeValue: false }
: applicable?.nodeValue === false
? { nodeValue: false, missingVariables: applicable.missingVariables }
: {
nodeValue: [notApplicable, applicable, ...parentDependencies].some(
n => n?.nodeValue === null
)
? null
: !notApplicable?.nodeValue &&
(applicable?.nodeValue == undefined || !!applicable?.nodeValue),
missingVariables: mergeAllMissing(
[...parentDependencies, notApplicable, disabled, applicable].filter(
Boolean
)
)
}
const res = {
...node,
nodeValue,
isApplicable: nodeValue,
missingVariables,
parentDependencies,
...evaluatedAttributes
}
if (node.dottedName) {
this.cache[cacheKey] = res
}
return res
}
export const evaluateFormula: evaluationFunction = function(node) {
const explanation = this.evaluateNode(node.explanation)
const { nodeValue, unit, missingVariables, temporalValue } = explanation
return {
...node,
nodeValue,
unit,
missingVariables,
explanation,
temporalValue
}
}
export const evaluateRule: evaluationFunction = function(node: any) {
this.cache._meta.contextRule.push(node.dottedName)
const applicabilityEvaluation = evaluateApplicability.call(this, node)
const {
missingVariables: condMissing,
nodeValue: isApplicable
} = applicabilityEvaluation
// evaluate the formula lazily, only if the applicability is known and true
let evaluatedFormula =
isApplicable && node.formule
? this.evaluateNode(node.formule)
: node.formule
if (node.unit) {
try {
evaluatedFormula = convertNodeToUnit(node.unit, evaluatedFormula)
} catch (e) {
typeWarning(
node.dottedName,
"L'unité de la règle est incompatible avec celle de sa formule",
e
)
}
}
const missingVariables = mergeMissing(
bonus(condMissing, !!Object.keys(condMissing).length),
evaluatedFormula.missingVariables
)
const temporalValue = evaluatedFormula.temporalValue
this.cache._meta.contextRule.pop()
return {
...node,
...applicabilityEvaluation,
...(node.formule && { formule: evaluatedFormula }),
nodeValue: evaluatedFormula.nodeValue,
unit: node.unit ?? evaluatedFormula.unit,
temporalValue,
isApplicable,
missingVariables
}
}
export const evaluateDisabledBy: evaluationFunction = function(node) {
const isDisabledBy = node.explanation.isDisabledBy.map(disablerNode =>
this.evaluateNode(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: evaluationFunction = function(node) {
const explanation = this.evaluateNode(node.explanation)
const nodeValue = explanation.nodeValue
const missingVariables = explanation.missingVariables
return { ...node, nodeValue, explanation, missingVariables }
}

View File

@ -1,18 +1,25 @@
import { add, evolve, fromPairs, keys, map, mergeWith, reduce } from 'ramda'
import {
add,
evolve,
fromPairs,
keys,
map,
mapObjIndexed,
mergeWith,
reduce
} from 'ramda'
import React from 'react'
import Engine, { evaluationFunction } from '.'
import {
ASTNode,
ConstantNode,
Evaluation,
EvaluationDecoration,
NodeKind
} from './AST/types'
import { typeWarning } from './error'
import {
evaluateReference,
evaluateReferenceTransforms
} from './evaluateReference'
import {
evaluateCondition,
evaluateDisabledBy,
evaluateFormula,
evaluateRule
} from './evaluateRule'
import { convertNodeToUnit, simplifyNodeUnit } from './nodeUnits'
import parse from './parse'
import {
concatTemporals,
liftTemporalNode,
@ -22,14 +29,13 @@ import {
temporalAverage,
zipTemporals
} from './temporal'
import { EvaluatedNode } from './types'
export const makeJsx = (node: EvaluatedNode): JSX.Element => {
export const makeJsx = (node: ASTNode): JSX.Element => {
const Component = node.jsx
return <Component {...node} />
}
export const collectNodeMissing = node => node.missingVariables || {}
export const collectNodeMissing = node => node?.missingVariables || {}
export const bonus = (missings, hasCondition = true) =>
hasCondition ? map(x => x + 0.0001, missings || {}) : missings
@ -59,10 +65,10 @@ function convertNodesToSameUnit(nodes, contextRule, mecanismName) {
})
}
export const evaluateArray: (
export const evaluateArray: <NodeName extends NodeKind>(
reducer: Parameters<typeof reduce>[0],
start: Parameters<typeof reduce>[1]
) => evaluationFunction = (reducer, start) =>
) => evaluationFunction<NodeName> = (reducer, start) =>
function(node: any) {
const evaluate = this.evaluateNode.bind(this)
const evaluatedNodes = convertNodesToSameUnit(
@ -96,6 +102,7 @@ export const evaluateArray: (
nodeValue: temporalValue[0].value
}
}
return {
...baseEvaluation,
temporalValue,
@ -103,28 +110,25 @@ export const evaluateArray: (
}
}
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: Evaluation) =>
({
nodeValue,
type: typeof nodeValue,
// eslint-disable-next-line
jsx: ({ nodeValue }: ASTNode & EvaluationDecoration) => (
<span className="value">{nodeValue}</span>
),
isDefault: true,
nodeKind: 'constant'
} as ConstantNode)
const evaluateDefaultNode: evaluationFunction = node => node
const evaluateExplanationNode: evaluationFunction = function(node) {
return this.evaluateNode(node.explanation)
}
export const parseObject = (recurse, objectShape, value) => {
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 ? recurse(value[key]) : defaultValue
return value[key] != null ? parse(value[key], context) : defaultValue
}
const transforms = fromPairs(
map(k => [k, recurseOne(k)], keys(objectShape)) as any
@ -132,22 +136,25 @@ export const parseObject = (recurse, objectShape, value) => {
return evolve(transforms as any, objectShape)
}
export const evaluateObject: (
effet: (this: Engine<string>, explanations: any) => any
) => evaluationFunction = effect =>
function(node: any) {
export function evaluateObject<NodeName extends NodeKind>(
effet: (this: Engine, explanations: any) => any
) {
return function(node) {
const evaluate = this.evaluateNode.bind(this)
const evaluations = map(evaluate, node.explanation)
const evaluations = mapObjIndexed(
evaluate as any,
(node as any).explanation
)
const temporalExplanations = mapTemporal(
Object.fromEntries,
concatTemporals(
Object.entries(evaluations).map(([key, node]) =>
zipTemporals(pureTemporal(key), liftTemporalNode(node))
zipTemporals(pureTemporal(key), liftTemporalNode(node as ASTNode))
)
)
)
const temporalExplanation = mapTemporal(explanations => {
const evaluation = effect.call(this, explanations)
const evaluation = effet.call(this, explanations)
return {
...evaluation,
explanation: {
@ -157,13 +164,11 @@ export const evaluateObject: (
}
}, temporalExplanations)
const sameUnitTemporalExplanation: Temporal<EvaluatedNode<
string,
number
>> = convertNodesToSameUnit(
const sameUnitTemporalExplanation: Temporal<ASTNode &
EvaluationDecoration & { nodeValue: number }> = convertNodesToSameUnit(
temporalExplanation.map(x => x.value),
this.cache._meta.contextRule,
node.name
node.nodeKind
).map((node, i) => ({
...temporalExplanation[i],
value: simplifyNodeUnit(node)
@ -184,7 +189,7 @@ export const evaluateObject: (
if (sameUnitTemporalExplanation.length === 1) {
return {
...baseEvaluation,
explanation: sameUnitTemporalExplanation[0].value.explanation
explanation: (sameUnitTemporalExplanation[0] as any).value
}
}
return {
@ -192,28 +197,5 @@ export const evaluateObject: (
temporalValue,
temporalExplanation
}
}
export const evaluationFunctions = {
rule: evaluateRule,
formula: evaluateFormula,
disabledBy: evaluateDisabledBy,
condition: evaluateCondition,
reference: evaluateReference,
referenceWithTransforms: evaluateReferenceTransforms,
parentDependencies: evaluateExplanationNode,
constant: evaluateDefaultNode,
defaultNode: evaluateDefaultNode
}
export function registerEvaluationFunction(
nodeKind: string,
evaluationFunction: evaluationFunction
) {
if (evaluationFunctions[nodeKind]) {
throw Error(
`Multiple evaluation functions registered for the nodeKind \x1b[4m${nodeKind}`
)
}
evaluationFunctions[nodeKind] = evaluationFunction
} as evaluationFunction<NodeName>
}

View File

@ -0,0 +1,19 @@
import { evaluationFunction } from '.';
import { ASTNode } from './AST/types';
export let evaluationFunctions = {
constant: node => node,
} as any;
export function registerEvaluationFunction<
NodeName extends ASTNode['nodeKind']
>(nodeKind: NodeName, evaluationFunction: evaluationFunction<NodeName>) {
evaluationFunctions ??= {};
if (evaluationFunctions[nodeKind]) {
throw Error(
`Multiple evaluation functions registered for the nodeKind \x1b[4m${nodeKind}`
);
}
evaluationFunctions[nodeKind] = evaluationFunction;
}

View File

@ -1,5 +1,5 @@
import { memoizeWith } from 'ramda'
import { Evaluation, Unit } from './types'
import { Evaluation, Unit } from './AST/types'
import { serializeUnit } from './units'
import { capitalise0 } from './utils'

View File

@ -7,9 +7,8 @@
@{%
const {
string, filteredVariable, date, variable, variableWithConversion,
temporalNumericValue, binaryOperation, unaryOperation, boolean, number,
numberWithUnit
string, date, variable, temporalNumericValue, binaryOperation,
unaryOperation, boolean, number, numberWithUnit
} = require('./grammarFunctions')
const moo = require("moo");
@ -67,8 +66,6 @@ TemporalNumericValue ->
NumericTerminal ->
Variable {% id %}
| VariableWithUnitConversion {% id %}
| FilteredVariable {% id %}
| number {% id %}
Negation ->
@ -99,14 +96,6 @@ UnitDenominator ->
UnitNumerator -> %words ("." %words):? {% flattenJoin %}
Unit -> UnitNumerator:? UnitDenominator:* {% flattenJoin %}
UnitConversion -> "[" Unit "]" {% ([,unit]) => unit %}
VariableWithUnitConversion ->
Variable %space UnitConversion {% variableWithConversion %}
# | FilteredVariable %space UnitConversion {% variableWithConversion %} TODO
Filter -> "." %words {% ([,filter]) => filter %}
FilteredVariable -> Variable %space Filter {% filteredVariable %}
AdditionSubstraction ->
AdditionSubstraction %space %additionSubstraction %space MultiplicationDivision {% binaryOperation('calculation') %}

View File

@ -18,13 +18,6 @@ export let unaryOperation = operationType => ([operator, , A]) => ({
}
})
export let filteredVariable = ([{ variable }, , { value: filter }]) => ({
filter: { filter, explanation: variable }
})
export let variableWithConversion = ([{ variable }, , unit]) => ({
unitConversion: { explanation: variable, unit: parseUnit(unit.value) }
})
export let temporalNumericValue = (variable, word, date) => ({
temporalValue: {
@ -45,15 +38,14 @@ export let variable = ([firstFragment, nextFragment], _, reject) => {
export let number = ([{ value }]) => ({
constant: {
type: 'number',
nodeValue: parseFloat(value)
}
})
export let numberWithUnit = ([number, , unit]) => ({
constant: {
nodeValue: parseFloat(number.value),
unit: parseUnit(unit.value)
}
export let numberWithUnit = (value) => ({
...number(value),
unité: value[2].value
})
export let date = ([{ value }]) => {

View File

@ -1,13 +1,11 @@
/* eslint-disable @typescript-eslint/ban-types */
import { map } from 'ramda'
import { evaluationError, warning } from './error'
import { evaluationFunctions } from './evaluation'
import { convertNodeToUnit, simplifyNodeUnit } from './nodeUnits'
import { parse } from './parse'
import parseRules from './parseRules'
import { ASTNode, EvaluationDecoration, NodeKind } from './AST/types'
import { evaluationFunctions } from './evaluationFunctions'
import parse from './parse'
import parsePublicodes, { disambiguateReference } from './parsePublicodes'
import { Rule, RuleNode } from './rule'
import * as utils from './ruleUtils'
import { EvaluatedNode, EvaluatedRule, ParsedRules, Rules } from './types'
import { parseUnit } from './units'
const emptyCache = () => ({
_meta: { contextRule: [] }
@ -16,6 +14,7 @@ const emptyCache = () => ({
type Cache = {
_meta: {
contextRule: Array<string>
parentEvaluationStack?: Array<string>
inversionFail?:
| {
given: string
@ -27,127 +26,97 @@ type Cache = {
}
}
type ParsedSituation<Names extends string> = Partial<ParsedRules<Names>>
export type EvaluationOptions = Partial<{
unit: string
}>
// export { default as cyclesLib } from './AST/index'
export * from './components'
export { default as cyclesLib } from './cyclesLib/index'
export { formatValue, serializeValue } from './format'
export { default as translateRules } from './translateRules'
export * from './types'
export { parseRules }
export { parsePublicodes }
export { utils }
export type evaluationFunction = (
this: Engine<string>,
node: EvaluatedNode
) => EvaluatedNode
export default class Engine<Names extends string> {
parsedRules: ParsedRules<Names>
parsedSituation: ParsedSituation<Names> = {}
export type evaluationFunction<Kind extends NodeKind = NodeKind> = (
this: Engine,
node: ASTNode & { nodeKind: Kind }
) => ASTNode & { nodeKind: Kind } & EvaluationDecoration
type ParsedRules<Name extends string> = Record<
Name,
RuleNode & { dottedName: Name }
>
export default class Engine<Name extends string = string> {
parsedRules: ParsedRules<Name>
parsedSituation: Record<string, ASTNode> = {}
cache: Cache
private warnings: Array<string> = []
constructor(rules: string | Rules<Names> | ParsedRules<Names>) {
constructor(rules: string | Record<string, Rule> | Record<string, RuleNode>) {
this.cache = emptyCache()
this.resetCache()
this.parsedRules =
typeof rules === 'string' || !(Object.values(rules)[0] as any)?.dottedName
? parseRules(rules)
: (rules as ParsedRules<Names>)
if (typeof rules === 'string') {
this.parsedRules = parsePublicodes(rules) as ParsedRules<Name>
}
const firstRuleObject = Object.values(rules)[0] as Rule | RuleNode
if (
typeof firstRuleObject === 'object' &&
firstRuleObject != null &&
'nodeKind' in firstRuleObject
) {
this.parsedRules = rules as ParsedRules<Name>
return
}
this.parsedRules = parsePublicodes(
rules as Record<string, Rule>
) as ParsedRules<Name>
}
private resetCache() {
this.cache = emptyCache()
}
private evaluateExpression(
expression: string,
context: string
): EvaluatedRule<Names> {
// EN ATTENDANT d'AVOIR une meilleure gestion d'erreur, on va mocker
// console.warn
setSituation(
situation: Partial<Record<Name, string | number | object>> = {}
) {
this.resetCache()
this.parsedSituation = map(value => {
return disambiguateReference(this.parsedRules)(
parse(value, {
dottedName: "'''situation",
parsedRules: {}
})
)
}, situation)
return this
}
evaluate(
expression: Name
): RuleNode & EvaluationDecoration & { dottedName: Name }
evaluate(expression: string): ASTNode & EvaluationDecoration {
/*
TODO
EN ATTENDANT d'AVOIR une meilleure gestion d'erreur, on va mocker console.warn
*/
const originalWarn = console.warn
console.warn = (warning: string) => {
this.warnings.push(warning)
originalWarn(warning)
}
const result = simplifyNodeUnit(
this.evaluateNode(
parse(
this.parsedRules,
{ dottedName: context },
this.parsedRules
)(expression)
if (this.parsedRules[expression]) {
// TODO : No replacement here. Is this what we want ?
return this.evaluateNode(this.parsedRules[expression])
}
const result = this.evaluateNode(
disambiguateReference(this.parsedRules)(
parse(expression, {
dottedName: "'''evaluation",
parsedRules: {}
})
)
)
console.warn = originalWarn
if (Object.keys(result.defaultValue?.missingVariable ?? {}).length) {
throw evaluationError(
context,
"Impossible d'évaluer l'expression car celle ci fait appel à des variables manquantes"
)
}
return result
}
setSituation(
situation: Partial<Record<Names, string | number | object>> = {}
) {
this.resetCache()
this.parsedSituation = map(
value =>
typeof value === 'object'
? value
: parse(
this.parsedRules,
{ dottedName: '' },
this.parsedRules
)(value),
situation
)
return this
}
evaluate(expression: Names, options?: EvaluationOptions): EvaluatedRule<Names>
evaluate(
expression: string,
options?: EvaluationOptions
): EvaluatedNode<Names> | EvaluatedRule<Names>
evaluate(expression: string, options?: EvaluationOptions) {
let result = this.evaluateExpression(
expression,
`[evaluation] ${expression}`
)
if (result.category === 'reference' && result.explanation) {
result = {
...result.explanation,
nodeValue: result.nodeValue,
missingVariables: result.missingVariables,
...('unit' in result && { unit: result.unit }),
...('temporalValue' in result && {
temporalValue: result.temporalValue
}),
dottedName: result.dottedName
} as EvaluatedRule<Names>
}
if (options?.unit) {
try {
return convertNodeToUnit(
parseUnit(options.unit),
result as EvaluatedNode<Names, number>
)
} catch (e) {
warning(
`[evaluation] ${expression}`,
"L'unité demandée est incompatible avec l'expression évaluée"
)
}
}
return result
}
@ -159,11 +128,11 @@ export default class Engine<Names extends string> {
return !!this.cache._meta.inversionFail
}
getParsedRules(): ParsedRules<Names> {
getParsedRules(): Record<string, RuleNode> {
return this.parsedRules
}
evaluateNode(node) {
evaluateNode<N extends ASTNode = ASTNode>(node: N): N & EvaluationDecoration {
if (!node.nodeKind) {
throw Error('The provided node must have a "nodeKind" attribute')
} else if (!evaluationFunctions[node.nodeKind]) {

View File

@ -1,27 +1,35 @@
import React from 'react'
import { evaluationFunction } from '..'
import parse from '../parse'
import { InfixMecanism } from '../components/mecanisms/common'
import {
bonus,
makeJsx,
mergeMissing,
registerEvaluationFunction
} from '../evaluation'
import { bonus, makeJsx, mergeMissing } from '../evaluation'
import { registerEvaluationFunction } from '../evaluationFunctions'
import { ASTNode } from '../AST/types'
export type ApplicableSiNode = {
explanation: {
condition: ASTNode
valeur: ASTNode
}
jsx: any
nodeKind: 'applicable si'
}
function MecanismApplicable({ explanation }) {
return (
<InfixMecanism prefixed value={explanation.valeur}>
<p>
<strong>Applicable si : </strong>
{makeJsx(explanation.applicable)}
{makeJsx(explanation.condition)}
</p>
</InfixMecanism>
)
}
const evaluate: evaluationFunction = function(node) {
const condition = this.evaluateNode(node.explanation.condition)
let valeur = node.explanation.valeur
const evaluate: evaluationFunction<'applicable si'> = function(node) {
const explanation = { ...node.explanation }
const condition = this.evaluateNode(explanation.condition)
let valeur = explanation.valeur
if (condition.nodeValue !== false) {
valeur = this.evaluateNode(valeur)
}
@ -30,32 +38,29 @@ const evaluate: evaluationFunction = function(node) {
nodeValue:
condition.nodeValue == null || condition.nodeValue === false
? condition.nodeValue
: valeur.nodeValue,
: 'nodeValue' in valeur
? valeur.nodeValue
: null,
explanation: { valeur, condition },
missingVariables: mergeMissing(
valeur.missingVariables,
'missingVariables' in valeur ? valeur.missingVariables : {},
bonus(condition.missingVariables)
),
unit: valeur.unit
...('unit' in valeur && { unit: valeur.unit })
}
}
parseApplicable.nom = 'applicable si' as const
export default function Applicable(recurse, v) {
export default function parseApplicable(v, context) {
const explanation = {
valeur: recurse(v.valeur),
condition: recurse(v['applicable si'])
valeur: parse(v.valeur, context),
condition: parse(v[parseApplicable.nom], context)
}
return {
// evaluate,
jsx: MecanismApplicable,
explanation,
category: 'mecanism',
name: Applicable.nom,
nodeKind: Applicable.nom,
unit: explanation.valeur.unit
nodeKind: parseApplicable.nom
}
}
Applicable.nom = 'applicable si'
registerEvaluationFunction(Applicable.nom, evaluate)
registerEvaluationFunction(parseApplicable.nom, evaluate)

View File

@ -1,16 +1,18 @@
import React from 'react'
import { evaluationFunction } from '..'
import { InfixMecanism } from '../components/mecanisms/common'
import {
makeJsx,
mergeAllMissing,
registerEvaluationFunction
} from '../evaluation'
import { EvaluatedNode } from '../types'
import { makeJsx, mergeAllMissing } from '../evaluation'
import { registerEvaluationFunction } from '../evaluationFunctions'
import parse from '../parse'
import { ASTNode } from '../AST/types'
export type ArrondiExplanation = {
valeur: EvaluatedNode<string, number>
arrondi: EvaluatedNode<string, number>
export type ArrondiNode = {
explanation: {
arrondi: ASTNode
valeur: ASTNode
}
jsx: any
nodeKind: 'arrondi'
}
function MecanismArrondi({ explanation }) {
@ -28,7 +30,7 @@ function roundWithPrecision(n: number, fractionDigits: number) {
return +n.toFixed(fractionDigits)
}
const evaluate: evaluationFunction = function(node) {
const evaluate: evaluationFunction<'arrondi'> = function(node) {
const valeur = this.evaluateNode(node.explanation.valeur)
const nodeValue = valeur.nodeValue
let arrondi = node.explanation.arrondi
@ -39,7 +41,7 @@ const evaluate: evaluationFunction = function(node) {
return {
...node,
nodeValue:
typeof valeur.nodeValue !== 'number'
typeof valeur.nodeValue !== 'number' || !('nodeValue' in arrondi)
? valeur.nodeValue
: typeof arrondi.nodeValue === 'number'
? roundWithPrecision(valeur.nodeValue, arrondi.nodeValue)
@ -54,22 +56,18 @@ const evaluate: evaluationFunction = function(node) {
}
}
export default function Arrondi(recurse, v) {
export default function Arrondi(v, context) {
const explanation = {
valeur: recurse(v.valeur),
arrondi: recurse(v.arrondi)
valeur: parse(v.valeur, context),
arrondi: parse(v.arrondi, context)
}
return {
jsx: MecanismArrondi,
explanation,
category: 'mecanism',
name: 'arrondi',
nodeKind: Arrondi.nom,
type: 'numeric',
unit: explanation.valeur.unit
nodeKind: Arrondi.nom
}
}
Arrondi.nom = 'arrondi'
Arrondi.nom = 'arrondi' as const
registerEvaluationFunction(Arrondi.nom, evaluate)

View File

@ -1,11 +1,10 @@
import { evaluationFunction } from '..'
import Barème from '../components/mecanisms/Barème'
import { evaluationError } from '../error'
import {
defaultNode,
mergeAllMissing,
registerEvaluationFunction
} from '../evaluation'
import parse from '../parse'
import { defaultNode, mergeAllMissing } from '../evaluation'
import { registerEvaluationFunction } from '../evaluationFunctions'
import {
liftTemporal2,
liftTemporalNode,
@ -15,24 +14,33 @@ import {
import { convertUnit, parseUnit } from '../units'
import {
evaluatePlafondUntilActiveTranche,
parseTranches
parseTranches,
TrancheNodes
} from './trancheUtils'
import { ASTNode } from '../AST/types'
// Barème en taux marginaux.
export default function parse(parse, v) {
export type BarèmeNode = {
explanation: {
tranches: TrancheNodes
multiplicateur: ASTNode
assiette: ASTNode
}
jsx
nodeKind: 'barème'
}
export default function parseBarème(v, context): BarèmeNode {
const explanation = {
assiette: parse(v.assiette),
multiplicateur: v.multiplicateur ? parse(v.multiplicateur) : defaultNode(1),
tranches: parseTranches(parse, v.tranches)
assiette: parse(v.assiette, context),
multiplicateur: v.multiplicateur
? parse(v.multiplicateur, context)
: defaultNode(1),
tranches: parseTranches(v.tranches, context)
}
return {
explanation,
jsx: Barème,
category: 'mecanism',
name: 'barème',
nodeKind: 'barème',
type: 'numeric',
unit: explanation.assiette.unit
nodeKind: 'barème'
}
}
@ -67,7 +75,7 @@ function evaluateBarème(tranches, assiette, evaluate, cache) {
return {
...tranche,
taux,
unit: assiette.unit,
...('unit' in assiette && { unit: assiette.unit }),
nodeValue:
(Math.min(assiette.nodeValue, tranche.plafondValue) -
tranche.plancherValue) *
@ -76,7 +84,7 @@ function evaluateBarème(tranches, assiette, evaluate, cache) {
}
})
}
const evaluate: evaluationFunction = function(node) {
const evaluate: evaluationFunction<'barème'> = function(node) {
const evaluateNode = this.evaluateNode.bind(this)
const assiette = this.evaluateNode(node.explanation.assiette)
const multiplicateur = this.evaluateNode(node.explanation.multiplicateur)
@ -91,14 +99,14 @@ const evaluate: evaluationFunction = function(node) {
},
this.cache
),
liftTemporalNode(assiette),
liftTemporalNode(multiplicateur)
liftTemporalNode(assiette as any),
liftTemporalNode(multiplicateur as any)
)
const temporalTranches = liftTemporal2(
(tranches, assiette) =>
evaluateBarème(tranches, assiette, evaluateNode, this.cache),
temporalTranchesPlafond,
liftTemporalNode(assiette)
liftTemporalNode(assiette as any)
)
const temporalValue = mapTemporal(
tranches =>
@ -125,7 +133,7 @@ const evaluate: evaluationFunction = function(node) {
: { tranches: temporalTranches[0].value })
},
unit: assiette.unit
}
} as any
}
registerEvaluationFunction('barème', evaluate)

View File

@ -1,51 +1,28 @@
import { add, dissoc, filter, objOf } from 'ramda'
import { evaluationFunction } from '..'
import { dissoc, omit, pick } from 'ramda'
import Composantes from '../components/mecanisms/Composantes'
import { evaluateArray, registerEvaluationFunction } from '../evaluation'
import { inferUnit } from '../units'
import { ASTNode } from '../AST/types'
import { registerEvaluationFunction } from '../evaluationFunctions'
import parse from '../parse'
export const evaluateComposantes: evaluationFunction = function(node) {
const evaluationFilter = c =>
!this.cache._meta.filter ||
!c.composante ||
((!c.composante['dû par'] ||
!['employeur', 'salarié'].includes(this.cache._meta.filter as any) ||
c.composante['dû par'] == this.cache._meta.filter) &&
(!c.composante['impôt sur le revenu'] ||
!['déductible', 'non déductible'].includes(
this.cache._meta.filter as any
) ||
c.composante['impôt sur le revenu'] == this.cache._meta.filter))
return evaluateArray(add as any, 0).call(this, {
...node,
explanation: filter(evaluationFilter, node.explanation)
})
}
export const decompose = (recurse, k, v) => {
const subProps = dissoc<Record<string, unknown>>('composantes', v)
const explanation = v.composantes.map(c => ({
...recurse(
objOf(k, {
...subProps,
...dissoc<Record<string, unknown>>('attributs', c)
export const decompose = (k, v, context): ASTNode => {
const { composantes, ...factoredKeys } = v
const explanation = parse(
{
somme: composantes.map(composante => {
const { attributs, ...otherKeys } = composante
return {
...attributs,
[k]: {
...factoredKeys,
...otherKeys
}
}
})
),
composante: c.nom ? { nom: c.nom } : c.attributs
}))
},
context
)
return {
explanation,
jsx: Composantes,
nodeKind: 'composantes',
category: 'mecanism',
name: 'composantes',
type: 'numeric',
unit: inferUnit(
'+',
explanation.map(e => e.unit)
)
...explanation,
jsx: Composantes
}
}
registerEvaluationFunction('composantes', evaluateComposantes)

View File

@ -2,23 +2,30 @@ import { is, map } from 'ramda'
import React from 'react'
import { evaluationFunction } from '..'
import { Mecanism } from '../components/mecanisms/common'
import {
makeJsx,
mergeAllMissing,
registerEvaluationFunction
} from '../evaluation'
import { ASTNode } from '../AST/types'
import { makeJsx, mergeAllMissing } from '../evaluation'
import { registerEvaluationFunction } from '../evaluationFunctions'
import parse from '../parse'
const evaluate: evaluationFunction = function(node) {
const [nodeValue, explanation] = node.explanation.reduce(
export type TouteCesConditionsNode = {
explanation: Array<ASTNode>
nodeKind: 'toutes ces conditions'
jsx: any
}
const evaluate: evaluationFunction<'toutes ces conditions'> = function(node) {
const [nodeValue, explanation] = node.explanation.reduce<
[boolean | null, Array<ASTNode>]
>(
([nodeValue, explanation], node) => {
if (nodeValue === false) {
return [nodeValue, [...explanation, node]]
}
const evaluatedNode = this.evaluateNode(node)
return [
nodeValue === false || nodeValue === null
? nodeValue
: evaluatedNode.nodeValue,
nodeValue === null || evaluatedNode.nodeValue === null
? null
: !!evaluatedNode.nodeValue,
[...explanation, evaluatedNode]
]
},
@ -33,9 +40,9 @@ const evaluate: evaluationFunction = function(node) {
}
}
export const mecanismAllOf = (recurse, v) => {
export const mecanismAllOf = (v, context) => {
if (!is(Array, v)) throw new Error('should be array')
const explanation = map(recurse, v)
const explanation = v.map(node => parse(node, context))
const jsx = ({ nodeValue, explanation, unit }) => (
<Mecanism name="toutes ces conditions" value={nodeValue} unit={unit}>
<ul>
@ -49,10 +56,7 @@ export const mecanismAllOf = (recurse, v) => {
return {
jsx,
explanation,
category: 'mecanism',
name: 'toutes ces conditions',
nodeKind: 'toutes ces conditions',
type: 'boolean'
nodeKind: 'toutes ces conditions'
}
}

View File

@ -2,13 +2,18 @@ import { is, map, max, mergeWith, reduce } from 'ramda'
import React from 'react'
import { evaluationFunction } from '..'
import { Mecanism } from '../components/mecanisms/common'
import {
collectNodeMissing,
makeJsx,
registerEvaluationFunction
} from '../evaluation'
import { ASTNode } from '../AST/types'
import { collectNodeMissing, makeJsx } from '../evaluation'
import { registerEvaluationFunction } from '../evaluationFunctions'
import parse from '../parse'
const evaluate: evaluationFunction = function(node) {
export type UneDeCesConditionsNode = {
explanation: Array<ASTNode>
nodeKind: 'une de ces conditions'
jsx: any
}
const evaluate: evaluationFunction<'une de ces conditions'> = function(node) {
const explanation = node.explanation.map(child => this.evaluateNode(child))
const anyTrue = explanation.find(e => e.nodeValue === true)
@ -29,9 +34,9 @@ const evaluate: evaluationFunction = function(node) {
return { ...node, nodeValue, explanation, missingVariables }
}
export const mecanismOneOf = (recurse, v) => {
export const mecanismOneOf = (v, context) => {
if (!is(Array, v)) throw new Error('should be array')
const explanation = map(recurse, v)
const explanation = v.map(node => parse(node, context))
const jsx = ({ nodeValue, explanation, unit }) => (
<Mecanism name="une de ces conditions" value={nodeValue} unit={unit}>
<ul>
@ -45,10 +50,7 @@ export const mecanismOneOf = (recurse, v) => {
return {
jsx,
explanation,
category: 'mecanism',
name: 'une de ces conditions',
nodeKind: 'une de ces conditions',
type: 'boolean'
nodeKind: 'une de ces conditions'
}
}

View File

@ -1,15 +1,24 @@
import React from 'react'
import { evaluationFunction } from '..'
import { ASTNode } from '../AST/types'
import { Mecanism } from '../components/mecanisms/common'
import { convertToDate, convertToString } from '../date'
import {
defaultNode,
makeJsx,
mergeAllMissing,
parseObject,
registerEvaluationFunction
parseObject
} from '../evaluation'
import { parseUnit } from '../units'
import { registerEvaluationFunction } from '../evaluationFunctions'
export type DuréeNode = {
explanation: {
depuis: ASTNode
"jusqu'à": ASTNode
}
jsx: any
nodeKind: 'durée'
}
function MecanismDurée({ nodeValue, explanation, unit }) {
return (
@ -28,20 +37,20 @@ function MecanismDurée({ nodeValue, explanation, unit }) {
)
}
const todayString = convertToString(new Date())
const objectShape = {
depuis: defaultNode(todayString),
"jusqu'à": defaultNode(todayString)
}
const evaluate: evaluationFunction = function(node) {
const evaluate: evaluationFunction<'durée'> = function(node) {
const from = this.evaluateNode(node.explanation.depuis)
const to = this.evaluateNode(node.explanation["jusqu'à"])
let nodeValue
if ([from, to].some(({ nodeValue }) => nodeValue === null)) {
nodeValue = null
} else {
const [fromDate, toDate] = [from.nodeValue, to.nodeValue].map(convertToDate)
const [fromDate, toDate] = [from.nodeValue, to.nodeValue].map(
convertToDate as any
)
nodeValue = Math.max(
0,
Math.round(
@ -61,18 +70,13 @@ const evaluate: evaluationFunction = function(node) {
}
}
export default (recurse, v) => {
const explanation = parseObject(recurse, objectShape, v)
export default (v, context) => {
const explanation = parseObject(objectShape, v, context)
return {
jsx: MecanismDurée,
explanation,
category: 'mecanism',
name: 'Durée',
nodeKind: 'durée',
type: 'numeric',
unit: parseUnit('jours')
}
nodeKind: 'durée'
} as DuréeNode
}
registerEvaluationFunction('durée', evaluate)

View File

@ -1,11 +1,8 @@
import { lensPath, over } from 'ramda'
import { evaluationFunction } from '..'
import grille from '../components/mecanisms/Grille'
import {
defaultNode,
mergeAllMissing,
registerEvaluationFunction
} from '../evaluation'
import { defaultNode, mergeAllMissing } from '../evaluation'
import { registerEvaluationFunction } from '../evaluationFunctions'
import {
liftTemporal2,
liftTemporalNode,
@ -15,26 +12,34 @@ import {
import { parseUnit } from '../units'
import {
evaluatePlafondUntilActiveTranche,
parseTranches
parseTranches,
TrancheNodes
} from './trancheUtils'
import parse from '../parse'
import { ASTNode } from '../AST/types'
export default function parse(parse, v) {
const defaultUnit = v['unité'] && parseUnit(v['unité'])
export type GrilleNode = {
explanation: {
assiette: ASTNode
multiplicateur: ASTNode
tranches: TrancheNodes
}
jsx: any
nodeKind: 'grille'
}
export default function parseGrille(v, context): GrilleNode {
const explanation = {
assiette: parse(v.assiette),
multiplicateur: v.multiplicateur ? parse(v.multiplicateur) : defaultNode(1),
tranches: parseTranches(parse, v.tranches).map(
over(lensPath(['montant', 'unit']), unit => unit ?? defaultUnit)
)
assiette: parse(v.assiette, context),
multiplicateur: v.multiplicateur
? parse(v.multiplicateur, context)
: defaultNode(1),
tranches: parseTranches(v.tranches, context)
}
return {
explanation,
jsx: grille,
category: 'mecanism',
name: 'grille',
nodeKind: 'grille',
type: 'numeric',
unit: explanation.tranches[0].montant.unit
nodeKind: 'grille'
}
}
const evaluateGrille = (tranches, evaluate) =>
@ -52,7 +57,7 @@ const evaluateGrille = (tranches, evaluate) =>
}
})
const evaluate: evaluationFunction = function(node: any) {
const evaluate: evaluationFunction<'grille'> = function(node) {
const evaluate = this.evaluateNode.bind(this)
const assiette = this.evaluateNode(node.explanation.assiette)
const multiplicateur = this.evaluateNode(node.explanation.multiplicateur)
@ -67,8 +72,8 @@ const evaluate: evaluationFunction = function(node: any) {
},
this.cache
),
liftTemporalNode(assiette),
liftTemporalNode(multiplicateur)
liftTemporalNode(assiette as any),
liftTemporalNode(multiplicateur as any)
)
const temporalTranches = mapTemporal(
tranches => evaluateGrille(tranches, evaluate),
@ -100,14 +105,15 @@ const evaluate: evaluationFunction = function(node: any) {
}
: { missingVariables: mergeAllMissing(activeTranches[0].value) }),
explanation: {
...node.explanation,
assiette,
multiplicateur,
...(temporalTranches.length > 1
? { temporalTranches }
: { tranches: temporalTranches[0].value })
},
unit: activeTranches[0].value[0]?.unit ?? node.unit
}
unit: activeTranches[0].value[0]?.unit ?? undefined
} as any
}
registerEvaluationFunction('grille', evaluate)

View File

@ -1,9 +1,26 @@
import parse from '../parse'
import { evaluationFunction } from '..'
import { ASTNode, ConstantNode, Unit } from '../AST/types'
import InversionNumérique from '../components/mecanisms/InversionNumérique'
import { mergeMissing, registerEvaluationFunction } from '../evaluation'
import { mergeMissing } from '../evaluation'
import { registerEvaluationFunction } from '../evaluationFunctions'
import { convertNodeToUnit } from '../nodeUnits'
import { Context } from '../parsePublicodes'
import { ReferenceNode } from '../reference'
import uniroot from '../uniroot'
import { parseUnit } from '../units'
import { InternalError } from '../error'
import { UnitéNode } from './unité'
export type InversionNode = {
explanation: {
ruleToInverse: string
inversionCandidates: Array<ReferenceNode>
unit?: Unit
}
jsx: any
nodeKind: 'inversion'
}
// The user of the inversion mechanism has to define a list of "inversion
// candidates". At runtime, the evaluation function of the mechanism will look
@ -14,46 +31,54 @@ import { parseUnit } from '../units'
// equal to its situation value, mathematically we search for the zero of the
// function x → f(x) - goal. The iteration logic between each test is
// implemented in the `uniroot` file.
export const evaluateInversion: evaluationFunction = function(node) {
let inversionGoal = node.explanation.inversionCandidates.find(
candidate => this.parsedSituation[candidate.dottedName] != undefined
export const evaluateInversion: evaluationFunction<'inversion'> = function(
node
) {
const inversionGoal = node.explanation.inversionCandidates.find(
candidate =>
this.parsedSituation[candidate.dottedName as string] != undefined
)
if (!inversionGoal) {
if (inversionGoal === undefined) {
return {
...node,
missingVariables: {
...Object.fromEntries(
node.explanation.inversionCandidates.map(n => [n.dottedName, 1])
node.explanation.inversionCandidates.map(name => [name, 1])
),
[node.explanation.ruleToInverse]: 1
},
nodeValue: null
}
}
inversionGoal = this.evaluateNode(inversionGoal)
const unit = this.parsedRules[node.explanation.ruleToInverse].unit
const evaluatedInversionGoal = this.evaluateNode(inversionGoal)
const unit = 'unit' in node ? node.unit : evaluatedInversionGoal.unit
const originalCache = { ...this.cache }
const originalSituation = { ...this.parsedSituation }
let inversionNumberOfIterations = 0
delete this.parsedSituation[inversionGoal.dottedName as string]
const evaluateWithValue = (n: number) => {
inversionNumberOfIterations++
this.cache = {
_meta: { ...originalCache._meta }
}
this.parsedSituation = {
...originalSituation,
[inversionGoal.dottedName]: undefined,
[node.explanation.ruleToInverse]: {
this.parsedSituation[node.explanation.ruleToInverse] = {
unit: unit,
jsx: null,
nodeKind: 'unité',
explanation: {
nodeKind: 'constant',
nodeValue: n,
unit
}
}
jsx: null,
type: 'number'
} as ConstantNode
} as UnitéNode
return convertNodeToUnit(unit, this.evaluateNode(inversionGoal))
}
const goal = convertNodeToUnit(unit, inversionGoal).nodeValue as number
const goal = convertNodeToUnit(unit, evaluatedInversionGoal)
.nodeValue as number
let nodeValue: number | null | undefined = null
// We do some blind attempts here to avoid using the default minimum and
@ -110,11 +135,6 @@ export const evaluateInversion: evaluationFunction = function(node) {
if (nodeValue === undefined) {
nodeValue = null
originalCache._meta.inversionFail = true
} else {
// For performance reason, we transfer the inversion cache
Object.entries(this.cache).forEach(([k, value]) => {
originalCache[k] = value
})
}
// // Uncomment to display the two attempts and their result
@ -123,9 +143,9 @@ export const evaluateInversion: evaluationFunction = function(node) {
this.cache = originalCache
this.parsedSituation = originalSituation
return {
...node,
unit,
nodeValue,
explanation: {
...node.explanation,
@ -136,24 +156,21 @@ export const evaluateInversion: evaluationFunction = function(node) {
}
}
export const mecanismInversion = (recurse, v, dottedName) => {
export const mecanismInversion = (v, context: Context) => {
if (!v.avec) {
throw new Error(
"Une formule d'inversion doit préciser _avec_ quoi on peut inverser la variable"
)
}
return {
unit: v.unité && parseUnit(v.unité),
explanation: {
ruleToInverse: dottedName,
inversionCandidates: v.avec.map(recurse)
ruleToInverse: context.dottedName,
inversionCandidates: v.avec.map(node => parse(node, context))
},
...('unité' in v && { unit: parseUnit(v.unité) }),
jsx: InversionNumérique,
category: 'mecanism',
name: 'inversion numérique',
nodeKind: 'inversion',
type: 'numeric'
}
nodeKind: 'inversion'
} as InversionNode
}
registerEvaluationFunction('inversion', evaluateInversion)

View File

@ -1,13 +1,18 @@
import React from 'react'
import { Mecanism } from '../components/mecanisms/common'
import {
evaluateArray,
makeJsx,
registerEvaluationFunction
} from '../evaluation'
import { ASTNode } from '../AST/types'
import { evaluateArray, makeJsx } from '../evaluation'
import { registerEvaluationFunction } from '../evaluationFunctions'
import parse from '../parse'
export const mecanismMax = (recurse, v) => {
const explanation = v.map(recurse)
export type MaxNode = {
explanation: Array<ASTNode>
nodeKind: 'maximum'
jsx: any
}
export const mecanismMax = (v, context) => {
const explanation = v.map(node => parse(node, context))
const jsx = ({ nodeValue, explanation, unit }) => (
<Mecanism name="le maximum de" value={nodeValue} unit={unit}>
@ -25,12 +30,8 @@ export const mecanismMax = (recurse, v) => {
return {
jsx,
explanation,
type: 'numeric',
category: 'mecanism',
name: 'le maximum de',
nodeKind: 'maximum',
unit: explanation[0].unit
}
nodeKind: 'maximum'
} as MaxNode
}
const max = (a, b) => {
@ -45,6 +46,5 @@ const max = (a, b) => {
}
return Math.max(a, b)
}
const evaluate = evaluateArray(max, false)
const evaluate = evaluateArray<'maximum'>(max, false)
registerEvaluationFunction('maximum', evaluate)

View File

@ -1,14 +1,18 @@
import { min } from 'ramda'
import React from 'react'
import { Mecanism } from '../components/mecanisms/common'
import {
evaluateArray,
makeJsx,
registerEvaluationFunction
} from '../evaluation'
import { ASTNode } from '../AST/types'
import { evaluateArray, makeJsx } from '../evaluation'
import { registerEvaluationFunction } from '../evaluationFunctions'
import parse from '../parse'
export const mecanismMin = (recurse, v) => {
const explanation = v.map(recurse)
export type MinNode = {
explanation: Array<ASTNode>
nodeKind: 'minimum'
jsx: any
}
export const mecanismMin = (v, context) => {
const explanation = v.map(node => parse(node, context))
const jsx = ({ nodeValue, explanation, unit }) => (
<Mecanism name="le minimum de" value={nodeValue} unit={unit}>
<ul>
@ -24,14 +28,10 @@ export const mecanismMin = (recurse, v) => {
return {
jsx,
explanation,
type: 'numeric',
category: 'mecanism',
name: 'le minimum de',
nodeKind: 'minimum',
unit: explanation[0].unit
}
nodeKind: 'minimum'
} as MinNode
}
const evaluate = evaluateArray(min, Infinity)
const evaluate = evaluateArray<'minimum'>(min, Infinity)
registerEvaluationFunction('minimum', evaluate)

View File

@ -1,13 +1,18 @@
import React from 'react'
import { evaluationFunction } from '..'
import { InfixMecanism } from '../components/mecanisms/common'
import {
bonus,
makeJsx,
mergeMissing,
registerEvaluationFunction
} from '../evaluation'
import { ASTNode } from '../AST/types'
import { bonus, makeJsx, mergeMissing } from '../evaluation'
import { registerEvaluationFunction } from '../evaluationFunctions'
import parse from '../parse'
export type NonApplicableSiNode = {
explanation: {
condition: ASTNode
valeur: ASTNode
}
jsx: any
nodeKind: 'non applicable si'
}
function MecanismNonApplicable({ explanation }) {
return (
<InfixMecanism prefixed value={explanation.valeur}>
@ -19,7 +24,7 @@ function MecanismNonApplicable({ explanation }) {
)
}
const evaluate: evaluationFunction = function(node) {
const evaluate: evaluationFunction<'non applicable si'> = function(node) {
const condition = this.evaluateNode(node.explanation.condition)
let valeur = node.explanation.valeur
if (condition.nodeValue !== true) {
@ -28,35 +33,34 @@ const evaluate: evaluationFunction = function(node) {
return {
...node,
nodeValue:
condition.nodeValue == null
? condition.nodeValue
condition.nodeValue === null
? null
: condition.nodeValue === true
? false
: valeur.nodeValue,
: 'nodeValue' in valeur
? valeur.nodeValue
: null,
explanation: { valeur, condition },
missingVariables: mergeMissing(
valeur.missingVariables,
'missingVariables' in valeur ? valeur.missingVariables : {},
bonus(condition.missingVariables)
),
unit: valeur.unit
...('unit' in valeur && { unit: valeur.unit })
}
}
export default function NonApplicable(recurse, v) {
export default function parseNonApplicable(v, context) {
const explanation = {
valeur: recurse(v.valeur),
condition: recurse(v['non applicable si'])
valeur: parse(v.valeur, context),
condition: parse(v[parseNonApplicable.nom], context)
}
return {
jsx: MecanismNonApplicable,
explanation,
category: 'mecanism',
name: 'non applicable',
nodeKind: 'non applicable',
unit: explanation.valeur.unit
}
nodeKind: parseNonApplicable.nom
} as NonApplicableSiNode
}
NonApplicable.nom = 'non applicable si'
parseNonApplicable.nom = 'non applicable si' as const
registerEvaluationFunction('non applicable', evaluate)
registerEvaluationFunction(parseNonApplicable.nom, evaluate)

View File

@ -1,17 +1,32 @@
import { registerEvaluationFunction } from '../evaluation'
import { ASTNode } from '../AST/types'
import { registerEvaluationFunction } from '../evaluationFunctions'
import parse from '../parse'
import { Context } from '../parsePublicodes'
// TODO : This isn't a real mecanism, cf. #963
export const mecanismOnePossibility = (recurse, v, dottedName) => ({
...v,
'une possibilité': 'oui',
context: dottedName,
export type PossibilityNode = {
explanation: Array<ASTNode>
'choix obligatoire'?: 'oui'
context: string
jsx: any
nodeKind: 'une possibilité'
})
registerEvaluationFunction(
'une possibilité',
(node: ReturnType<typeof mecanismOnePossibility>) => ({
...node,
missingVariables: { [node.context]: 1 }
})
)
}
// TODO : This isn't a real mecanism, cf. #963
export const mecanismOnePossibility = (v, context: Context) => {
if (Array.isArray(v)) {
v = {
possibilités: v
}
}
return {
...v,
explanation: v.possibilités.map(p => parse(p, context)),
nodeKind: 'une possibilité',
context: context.dottedName
} as PossibilityNode
}
registerEvaluationFunction<'une possibilité'>('une possibilité', node => ({
...node,
nodeValue: null,
jsx: null,
missingVariables: { [node.context]: 1 }
}))

View File

@ -1,34 +1,40 @@
import {
add,
divide,
equals,
fromPairs,
gt,
gte,
lt,
lte,
map,
multiply,
subtract
} from 'ramda'
import { equals, fromPairs, map } from 'ramda'
import React from 'react'
import { evaluationFunction } from '..'
import { Operation } from '../components/mecanisms/common'
import { ASTNode } from '../AST/types'
import { convertToDate } from '../date'
import { typeWarning } from '../error'
import {
makeJsx,
mergeAllMissing,
registerEvaluationFunction
} from '../evaluation'
import { makeJsx, mergeAllMissing } from '../evaluation'
import { registerEvaluationFunction } from '../evaluationFunctions'
import { convertNodeToUnit } from '../nodeUnits'
import parse from '../parse'
import { liftTemporal2, pureTemporal, temporalAverage } from '../temporal'
import { EvaluationDecoration } from '../AST/types'
import { inferUnit, serializeUnit } from '../units'
const parse = (k, symbol) => (recurse, v) => {
const explanation = v.explanation.map(recurse)
const [node1, node2] = explanation
const unit = inferUnit(k, [node1.unit, node2.unit])
const knownOperations = {
'*': [(a, b) => a * b, '×'],
'/': [(a, b) => a / b, ''],
'+': [(a, b) => a + b],
'-': [(a, b) => a - b, ''],
'<': [(a, b) => a < b],
'<=': [(a, b) => a <= b, '≤'],
'>': [(a, b) => a > b],
'>=': [(a, b) => a >= b, '≥'],
'=': [(a, b) => equals(a, b)],
'!=': [(a, b) => !equals(a, b), '≠']
} as const
export type OperationNode = {
nodeKind: 'operation'
explanation: [ASTNode, ASTNode]
operationKind: keyof typeof knownOperations
operator: string
jsx: any
}
const parseOperation = (k, symbol) => (v, context) => {
const explanation = v.explanation.map(node => parse(node, context))
const jsx = ({ nodeValue, explanation, unit }) => (
<Operation value={nodeValue} unit={unit}>
@ -45,13 +51,15 @@ const parse = (k, symbol) => (recurse, v) => {
nodeKind: 'operation',
operationKind: k,
operator: symbol || k,
explanation,
unit
}
explanation
} as OperationNode
}
const evaluate: evaluationFunction = function(node: any) {
const explanation = map(node => this.evaluateNode(node), node.explanation)
const evaluate: evaluationFunction<'operation'> = function(node) {
const explanation = node.explanation.map(node => this.evaluateNode(node)) as [
ASTNode & EvaluationDecoration,
ASTNode & EvaluationDecoration
]
let [node1, node2] = explanation
const missingVariables = mergeAllMissing([node1, node2])
@ -60,7 +68,7 @@ const evaluate: evaluationFunction = function(node: any) {
}
if (!['', '×'].includes(node.operator)) {
try {
if (node1.unit) {
if (node1.unit && 'unit' in node2) {
node2 = convertNodeToUnit(node1.unit, node2)
} else if (node2.unit) {
node1 = convertNodeToUnit(node2.unit, node1)
@ -82,7 +90,12 @@ const evaluate: evaluationFunction = function(node: any) {
const baseNode = {
...node,
explanation,
unit: inferUnit(node.operationKind, [node1.unit, node2.unit]),
...((node.operationKind === '*' ||
node.operationKind === '/' ||
node.operationKind === '-' ||
node.operationKind === '+') && {
unit: inferUnit(node.operationKind, [node1.unit, node2.unit])
}),
missingVariables
}
@ -109,8 +122,8 @@ const evaluate: evaluationFunction = function(node: any) {
}
return operatorFunction(a, b)
},
node1.temporalValue ?? pureTemporal(node1.nodeValue),
node2.temporalValue ?? pureTemporal(node2.nodeValue)
node1.temporalValue ?? (pureTemporal(node1.nodeValue) as any),
node2.temporalValue ?? (pureTemporal(node2.nodeValue) as any)
)
const nodeValue = temporalAverage(temporalValue, baseNode.unit)
@ -123,23 +136,10 @@ const evaluate: evaluationFunction = function(node: any) {
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)
parseOperation(k, symbol)
])
)

View File

@ -0,0 +1,66 @@
import React from 'react'
import { evaluationFunction } from '..'
import { InfixMecanism } from '../components/mecanisms/common'
import { ASTNode } from '../AST/types'
import { bonus, makeJsx, mergeMissing } from '../evaluation'
import { registerEvaluationFunction } from '../evaluationFunctions'
import parse from '../parse'
import { EvaluationDecoration } from '../AST/types'
export type ParDéfautNode = {
explanation: {
valeur: ASTNode
parDéfaut: ASTNode
}
jsx: any
nodeKind: 'par défaut'
}
function ParDéfautComponent({ explanation }) {
return (
<InfixMecanism prefixed value={explanation.valeur}>
<p>
<strong>Par défaut : </strong>
{makeJsx(explanation.parDéfaut)}
</p>
</InfixMecanism>
)
}
const evaluate: evaluationFunction<'par défaut'> = function(node) {
const explanation = { ...node.explanation }
let valeur = this.evaluateNode(explanation.valeur)
explanation.valeur = valeur
if (valeur.nodeValue === null) {
valeur = this.evaluateNode(explanation.parDéfaut)
explanation.parDéfaut = valeur
}
return {
...node,
nodeValue: valeur.nodeValue,
explanation,
missingVariables: mergeMissing(
(explanation.valeur as EvaluationDecoration).missingVariables,
'missingVariables' in explanation.parDéfaut
? bonus(explanation.parDéfaut.missingVariables)
: {}
),
...('unit' in valeur && { unit: valeur.unit })
}
}
export default function parseParDéfaut(v, context) {
const explanation = {
valeur: parse(v.valeur, context),
parDéfaut: parse(v['par défaut'], context)
}
return {
jsx: ParDéfautComponent,
explanation,
nodeKind: parseParDéfaut.nom
} as ParDéfautNode
}
parseParDéfaut.nom = 'par défaut' as const
registerEvaluationFunction(parseParDéfaut.nom, evaluate)

View File

@ -2,12 +2,13 @@ import React from 'react'
import { evaluationFunction } from '..'
import { InfixMecanism } from '../components/mecanisms/common'
import { typeWarning } from '../error'
import {
makeJsx,
mergeAllMissing,
registerEvaluationFunction
} from '../evaluation'
import parse from '../parse'
import { makeJsx, mergeAllMissing } from '../evaluation'
import { registerEvaluationFunction } from '../evaluationFunctions'
import { convertNodeToUnit } from '../nodeUnits'
import { ASTNode } from '../AST/types'
import { EvaluationDecoration } from '../AST/types'
function MecanismPlafond({ explanation }) {
return (
@ -25,8 +26,15 @@ function MecanismPlafond({ explanation }) {
</InfixMecanism>
)
}
const evaluate: evaluationFunction = function(node) {
export type PlafondNode = {
explanation: {
plafond: ASTNode
valeur: ASTNode
}
jsx: any
nodeKind: 'plafond'
}
const evaluate: evaluationFunction<'plafond'> = function(node) {
const valeur = this.evaluateNode(node.explanation.valeur)
let nodeValue = valeur.nodeValue
@ -35,7 +43,10 @@ const evaluate: evaluationFunction = function(node) {
plafond = this.evaluateNode(plafond)
if (valeur.unit) {
try {
plafond = convertNodeToUnit(valeur.unit, plafond)
plafond = convertNodeToUnit(
valeur.unit,
plafond as ASTNode & EvaluationDecoration
)
} catch (e) {
typeWarning(
this.cache._meta.contextRule,
@ -45,39 +56,37 @@ const evaluate: evaluationFunction = function(node) {
}
}
}
plafond
if (
typeof nodeValue === 'number' &&
'nodeValue' in plafond &&
typeof plafond.nodeValue === 'number' &&
nodeValue > plafond.nodeValue
) {
nodeValue = plafond.nodeValue
plafond.isActive = true
;(plafond as any).isActive = true
}
return {
...node,
nodeValue,
unit: valeur.unit,
...('unit' in valeur && { unit: valeur.unit }),
explanation: { valeur, plafond },
missingVariables: mergeAllMissing([valeur, plafond])
}
}
export default function Plafond(recurse, v) {
export default function parsePlafond(v, context) {
const explanation = {
valeur: recurse(v.valeur),
plafond: recurse(v.plafond)
valeur: parse(v.valeur, context),
plafond: parse(v.plafond, context)
}
return {
jsx: MecanismPlafond,
explanation,
category: 'mecanism',
name: 'plafond',
nodeKind: 'plafond',
type: 'numeric',
unit: explanation.valeur.unit
}
nodeKind: 'plafond'
} as PlafondNode
}
Plafond.nom = 'plafond'
parsePlafond.nom = 'plafond'
registerEvaluationFunction('plafond', evaluate)

View File

@ -1,13 +1,13 @@
import React from 'react'
import { evaluationFunction } from '..'
import { InfixMecanism } from '../components/mecanisms/common'
import { ASTNode } from '../AST/types'
import { typeWarning } from '../error'
import {
makeJsx,
mergeAllMissing,
registerEvaluationFunction
} from '../evaluation'
import { makeJsx, mergeAllMissing } from '../evaluation'
import { registerEvaluationFunction } from '../evaluationFunctions'
import { convertNodeToUnit } from '../nodeUnits'
import parse from '../parse'
import { EvaluationDecoration } from '../AST/types'
function MecanismPlancher({ explanation }) {
return (
@ -25,8 +25,15 @@ function MecanismPlancher({ explanation }) {
</InfixMecanism>
)
}
const evaluate: evaluationFunction = function(node) {
export type PlancherNode = {
explanation: {
plancher: ASTNode
valeur: ASTNode
}
jsx: any
nodeKind: 'plancher'
}
const evaluate: evaluationFunction<'plancher'> = function(node) {
const valeur = this.evaluateNode(node.explanation.valeur)
let nodeValue = valeur.nodeValue
let plancher = node.explanation.plancher
@ -34,7 +41,10 @@ const evaluate: evaluationFunction = function(node) {
plancher = this.evaluateNode(plancher)
if (valeur.unit) {
try {
plancher = convertNodeToUnit(valeur.unit, plancher)
plancher = convertNodeToUnit(
valeur.unit,
plancher as ASTNode & EvaluationDecoration
)
} catch (e) {
typeWarning(
this.cache._meta.contextRule,
@ -46,36 +56,32 @@ const evaluate: evaluationFunction = function(node) {
}
if (
typeof nodeValue === 'number' &&
'nodeValue' in plancher &&
typeof plancher.nodeValue === 'number' &&
nodeValue < plancher.nodeValue
) {
nodeValue = plancher.nodeValue
plancher.isActive = true
;(plancher as any).isActive = true
}
return {
...node,
nodeValue,
...('unit' in valeur && { unit: valeur.unit }),
explanation: { valeur, plancher },
missingVariables: mergeAllMissing([valeur, plancher]),
unit: valeur.unit
missingVariables: mergeAllMissing([valeur, plancher])
}
}
export default function Plancher(recurse, v) {
export default function Plancher(v, context) {
const explanation = {
valeur: recurse(v.valeur),
plancher: recurse(v.plancher)
valeur: parse(v.valeur, context),
plancher: parse(v.plancher, context)
}
return {
evaluate,
jsx: MecanismPlancher,
explanation,
category: 'mecanism',
name: 'plancher',
nodeKind: 'plancher',
type: 'numeric',
unit: explanation.valeur.unit
}
nodeKind: 'plancher'
} as PlancherNode
}
Plancher.nom = 'plancher'

View File

@ -1,20 +0,0 @@
import { registerEvaluationFunction } from '../evaluation'
const evaluate = (cache, situation, parsedRules, node) => {
return { ...node }
}
export const mecanismPossibility = (recurse, k, v) => {
return {
explanation: {},
jsx: function Synchronisation({ explanation }) {
return null
},
category: 'mecanism',
name: 'possibilité',
nodeKind: 'possibilité',
type: 'possibilité'
}
}
registerEvaluationFunction('possibilité', evaluate)

View File

@ -1,15 +1,23 @@
import { evaluationFunction } from '..'
import Product from '../components/mecanisms/Product'
import { ASTNode } from '../AST/types'
import { typeWarning } from '../error'
import {
defaultNode,
evaluateObject,
parseObject,
registerEvaluationFunction
} from '../evaluation'
import { defaultNode, evaluateObject, parseObject } from '../evaluation'
import { registerEvaluationFunction } from '../evaluationFunctions'
import { convertNodeToUnit, simplifyNodeUnit } from '../nodeUnits'
import { areUnitConvertible, convertUnit, inferUnit } from '../units'
export type ProductNode = {
explanation: {
assiette: ASTNode
facteur: ASTNode
plafond: ASTNode
taux: ASTNode
}
jsx: any
nodeKind: 'produit'
}
const objectShape = {
assiette: false,
taux: defaultNode(1),
@ -17,23 +25,14 @@ const objectShape = {
plafond: defaultNode(Infinity)
}
export const mecanismProduct = (recurse, v) => {
const explanation = parseObject(recurse, objectShape, v)
export const mecanismProduct = (v, context) => {
const explanation = parseObject(objectShape, v, context)
return {
jsx: Product,
explanation,
category: 'mecanism',
name: 'produit',
nodeKind: 'produit',
type: 'numeric',
unit: inferUnit(
'*',
[explanation.assiette, explanation.taux, explanation.facteur].map(
el => el.unit
)
)
}
nodeKind: 'produit'
} as ProductNode
}
const productEffect: evaluationFunction = function({
@ -78,12 +77,13 @@ const productEffect: evaluationFunction = function({
return simplifyNodeUnit({
nodeValue,
unit,
explanation: {
plafondActif: assiette.nodeValue > plafond.nodeValue
}
})
}
const evaluate = evaluateObject(productEffect)
const evaluate = evaluateObject<'produit'>(productEffect)
registerEvaluationFunction('produit', evaluate)

View File

@ -1,12 +1,26 @@
import { evaluationFunction } from '..'
import { ASTNode } from '../AST/types'
import Recalcul from '../components/mecanisms/Recalcul'
import { defaultNode, registerEvaluationFunction } from '../evaluation'
import { EvaluatedNode } from '../types'
import { defaultNode } from '../evaluation'
import { registerEvaluationFunction } from '../evaluationFunctions'
import parse from '../parse'
import { ReferenceNode } from '../reference'
import { disambiguateRuleReference } from '../ruleUtils'
import { EvaluationDecoration } from '../AST/types'
import { serializeUnit } from '../units'
const evaluateRecalcul: evaluationFunction = function(node) {
export type RecalculNode = {
explanation: {
recalcul: ASTNode
amendedSituation: Array<[ReferenceNode, ASTNode]>
}
jsx: any
nodeKind: 'recalcul'
}
const evaluateRecalcul: evaluationFunction<'recalcul'> = function(node) {
if (this.cache._meta.inRecalcul) {
return (defaultNode(false) as any) as EvaluatedNode
return (defaultNode(false) as any) as RecalculNode & EvaluationDecoration
}
const amendedSituation = node.explanation.amendedSituation
@ -18,21 +32,27 @@ const evaluateRecalcul: evaluationFunction = function(node) {
([originRule, replacement]) =>
originRule.nodeValue !== replacement.nodeValue ||
serializeUnit(originRule.unit) !== serializeUnit(replacement.unit)
)
) as Array<
[ReferenceNode & EvaluationDecoration, ASTNode & EvaluationDecoration]
>
const originalCache = this.cache
const originalSituation = this.parsedSituation
const originalCache = { ...this.cache }
const originalSituation = { ...this.parsedSituation }
// Optimisation : no need for recalcul if situation is the same
this.cache = Object.keys(amendedSituation).length
? { _meta: { ...this.cache._meta, inRecalcul: true } } // Create an empty cache
: this.cache
: { ...this.cache }
this.parsedSituation = {
...this.parsedSituation,
...Object.fromEntries(
amendedSituation.map(([originRule, replacement]) => [
originRule.dottedName,
amendedSituation.map(([reference, replacement]) => [
disambiguateRuleReference(
this.parsedRules,
reference.contextDottedName,
reference.name
),
replacement
])
]) as any
)
}
@ -42,24 +62,25 @@ const evaluateRecalcul: evaluationFunction = function(node) {
return {
...node,
nodeValue: evaluatedNode.nodeValue,
...(evaluatedNode.temporalValue && {
temporalValue: evaluatedNode.temporalValue
}),
unit: evaluatedNode.unit,
explanation: {
recalcul: evaluatedNode,
amendedSituation
}
},
missingVariables: evaluatedNode.missingVariables,
...('unit' in evaluatedNode && { unit: evaluatedNode.unit }),
...(evaluatedNode.temporalValue && {
temporalValue: evaluatedNode.temporalValue
})
}
}
export const mecanismRecalcul = (recurse, v, dottedNameContext) => {
export const mecanismRecalcul = (v, context) => {
const amendedSituation = Object.keys(v.avec).map(dottedName => [
recurse(dottedName),
recurse(v.avec[dottedName])
parse(dottedName, context),
parse(v.avec[dottedName], context)
])
const defaultRuleToEvaluate = dottedNameContext
const nodeToEvaluate = recurse(v.règle ?? defaultRuleToEvaluate)
const defaultRuleToEvaluate = context.dottedName
const nodeToEvaluate = parse(v.règle ?? defaultRuleToEvaluate, context)
return {
explanation: {
recalcul: nodeToEvaluate,
@ -67,7 +88,7 @@ export const mecanismRecalcul = (recurse, v, dottedNameContext) => {
},
jsx: Recalcul,
nodeKind: 'recalcul'
}
} as RecalculNode
}
registerEvaluationFunction('recalcul', evaluateRecalcul)

View File

@ -1,14 +1,22 @@
import { max, min } from 'ramda'
import Allègement from '../components/mecanisms/Allègement'
import { typeWarning } from '../error'
import {
defaultNode,
evaluateObject,
parseObject,
registerEvaluationFunction
} from '../evaluation'
import { defaultNode, evaluateObject, parseObject } from '../evaluation'
import { registerEvaluationFunction } from '../evaluationFunctions'
import { convertNodeToUnit } from '../nodeUnits'
import { serializeUnit } from '../units'
import parse from '../parse'
import { ASTNode } from '../AST/types'
export type ReductionNode = {
explanation: {
assiette: ASTNode
abattement: ASTNode
plafond: ASTNode
}
jsx: any
nodeKind: 'allègement'
}
const objectShape = {
assiette: false,
@ -16,11 +24,11 @@ const objectShape = {
plafond: defaultNode(Infinity)
}
const evaluate = evaluateObject(function({
const evaluate = evaluateObject<'allègement'>(function({
assiette,
abattement,
plafond
}: any) {
}) {
const assietteValue = assiette.nodeValue
if (assietteValue == null) return { nodeValue: null }
if (assiette.unit) {
@ -52,7 +60,7 @@ const evaluate = evaluateObject(function({
: assietteValue
return {
nodeValue,
unit: assiette.unit,
...('unit' in assiette && { unit: assiette.unit }),
explanation: {
plafond,
abattement
@ -60,18 +68,14 @@ const evaluate = evaluateObject(function({
}
})
export const mecanismReduction = (recurse, v) => {
const explanation = parseObject(recurse, objectShape, v)
export const mecanismReduction = (v, context) => {
const explanation = parseObject(objectShape, v, context)
return {
jsx: Allègement,
explanation,
category: 'mecanism',
name: 'allègement',
nodeKind: 'allègement',
type: 'numeric',
unit: explanation?.assiette?.unit
}
nodeKind: 'allègement'
} as ReductionNode
}
registerEvaluationFunction('allègement', evaluate)

View File

@ -0,0 +1,65 @@
import { isEmpty } from 'ramda'
import { ASTNode, EvaluationDecoration } from '../AST/types'
import { makeJsx, mergeAllMissing } from '../evaluation'
import { registerEvaluationFunction } from '../evaluationFunctions'
import parse from '../parse'
function MecanismSituation({ explanation, nodeValue, unit }) {
// TODO : vue différente selon si valeur depuis la situation ou calculée
return makeJsx({ ...explanation.valeur, nodeValue, unit })
}
export type SituationNode = {
explanation: {
situationKey: string
valeur: ASTNode
situationValeur?: ASTNode
}
jsx: any
nodeKind: 'nom dans la situation'
}
export default function parseSituation(v, context) {
const explanation = {
situationKey: v[parseSituation.nom],
valeur: parse(v.valeur, context)
}
return {
jsx: MecanismSituation,
nodeKind: parseSituation.nom,
explanation
} as SituationNode
}
parseSituation.nom = 'nom dans la situation' as const
registerEvaluationFunction(parseSituation.nom, function evaluate(node) {
const explanation = { ...node.explanation }
const situationKey = explanation.situationKey
let valeur: ASTNode & EvaluationDecoration
if (situationKey in this.parsedSituation) {
valeur = this.evaluateNode(this.parsedSituation[situationKey])
explanation.situationValeur = valeur
} else {
valeur = this.evaluateNode(explanation.valeur)
explanation.valeur = valeur
delete explanation.situationValeur
}
const unit =
valeur.unit ??
('unit' in explanation.valeur ? explanation.valeur.unit : undefined)
const missingVariables = mergeAllMissing(
[explanation.situationValeur, explanation.valeur].filter(Boolean)
)
return {
...node,
nodeValue: valeur.nodeValue,
missingVariables:
isEmpty(missingVariables) && valeur.nodeValue === null
? { [situationKey]: 1 }
: missingVariables,
...(unit !== undefined && { unit }),
explanation
}
})

View File

@ -1,26 +1,27 @@
import { ASTNode } from '../AST/types'
import Somme from '../components/mecanisms/Somme'
import { evaluateArray, registerEvaluationFunction } from '../evaluation'
import { inferUnit } from '../units'
import { evaluateArray } from '../evaluation'
import { registerEvaluationFunction } from '../evaluationFunctions'
import parse from '../parse'
const evaluate = evaluateArray(
const evaluate = evaluateArray<'somme'>(
(x: any, y: any) => (x === false && y === false ? false : x + y),
false
)
export const mecanismSum = (recurse, v) => {
const explanation = v.map(recurse)
export type SommeNode = {
explanation: Array<ASTNode>
nodeKind: 'somme'
jsx: any
}
export const mecanismSum = (v, context) => {
const explanation = v.map(node => parse(node, context))
return {
jsx: Somme,
explanation,
category: 'mecanism',
name: 'somme',
nodeKind: 'somme',
type: 'numeric',
unit: inferUnit(
'+',
explanation.map(r => r.unit)
)
}
nodeKind: 'somme'
} as SommeNode
}
registerEvaluationFunction('somme', evaluate)

View File

@ -2,48 +2,54 @@ import { path } from 'ramda'
import React from 'react'
import { evaluationFunction } from '..'
import { RuleLinkWithContext } from '../components/RuleLink'
import { registerEvaluationFunction } from '../evaluation'
import { ASTNode } from '../AST/types'
import { registerEvaluationFunction } from '../evaluationFunctions'
import parse from '../parse'
const evaluate: evaluationFunction = function(node: any) {
const APIExplanation = this.evaluateNode(node.explanation.API)
export type SynchronisationNode = {
explanation: {
chemin: string
data: ASTNode
}
jsx: any
nodeKind: 'synchronisation'
}
const evaluate: evaluationFunction<'synchronisation'> = function(node: any) {
const data = this.evaluateNode(node.explanation.data)
const valuePath = node.explanation.chemin.split(' . ')
const nodeValue =
APIExplanation.nodeValue == null
? null
: path(valuePath, APIExplanation.nodeValue)
data.nodeValue == null ? null : path(valuePath, 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 && APIExplanation.nodeValue != null
? path(valuePath, APIExplanation.explanation.defaultValue)
nodeValue == null && data.nodeValue != null
? path(valuePath, data.explanation.defaultValue)
: nodeValue
const missingVariables = {
...APIExplanation.missingVariables,
...(APIExplanation.nodeValue === null
? { [APIExplanation.dottedName]: 1 }
: {})
...data.missingVariables,
...(data.nodeValue === null ? { [data.dottedName]: 1 } : {})
}
const explanation = { ...node.explanation, API: APIExplanation }
const explanation = { ...node.explanation, data }
return { ...node, nodeValue: safeNodeValue, explanation, missingVariables }
}
export const mecanismSynchronisation = (recurse, v) => {
export const mecanismSynchronisation = (v, context) => {
return {
explanation: { ...v, API: recurse(v.API) },
// TODO : expect API exists ?
explanation: { ...v, data: parse(v.data, context) },
jsx: function Synchronisation({ explanation }) {
return (
<p>
Obtenu à partir de la saisie{' '}
<RuleLinkWithContext dottedName={explanation.API.dottedName} />
<RuleLinkWithContext dottedName={explanation.data.dottedName} />
</p>
)
},
category: 'mecanism',
name: 'synchronisation',
nodeKind: 'synchronisation'
}
} as SynchronisationNode
}
registerEvaluationFunction('synchronisation', evaluate)

View File

@ -1,35 +1,42 @@
import { evaluationFunction } from '..'
import tauxProgressif from '../components/mecanisms/TauxProgressif'
import {
defaultNode,
mergeAllMissing,
registerEvaluationFunction
} from '../evaluation'
import { defaultNode, mergeAllMissing } from '../evaluation'
import { registerEvaluationFunction } from '../evaluationFunctions'
import parse from '../parse'
import { convertNodeToUnit } from '../nodeUnits'
import { parseUnit } from '../units'
import {
evaluatePlafondUntilActiveTranche,
parseTranches
parseTranches,
TrancheNodes
} from './trancheUtils'
export default function parse(parse, v) {
const explanation = {
assiette: parse(v.assiette),
multiplicateur: v.multiplicateur ? parse(v.multiplicateur) : defaultNode(1),
tranches: parseTranches(parse, v.tranches)
import { ASTNode } from '../AST/types'
export type TauxProgressifNode = {
explanation: {
tranches: TrancheNodes
multiplicateur: ASTNode
assiette: ASTNode
}
jsx
nodeKind: 'taux progressif'
}
export default function parseTauxProgressif(v, context): TauxProgressifNode {
const explanation = {
assiette: parse(v.assiette, context),
multiplicateur: v.multiplicateur
? parse(v.multiplicateur, context)
: defaultNode(1),
tranches: parseTranches(v.tranches, context)
} as TauxProgressifNode['explanation']
return {
jsx: tauxProgressif,
explanation,
category: 'mecanism',
name: 'taux progressif',
nodeKind: 'taux progressif',
type: 'numeric',
unit: parseUnit('%')
nodeKind: 'taux progressif'
}
}
const evaluate: evaluationFunction = function(node: any) {
const evaluate: evaluationFunction<'taux progressif'> = function(node) {
const evaluate = this.evaluateNode.bind(this)
const assiette = this.evaluateNode(node.explanation.assiette)
const multiplicateur = this.evaluateNode(node.explanation.multiplicateur)
@ -70,7 +77,10 @@ const evaluate: evaluationFunction = function(node: any) {
}
}
if (tranches.every(({ isActive }) => isActive !== true)) {
if (
tranches.every(({ isActive }) => isActive !== true) ||
typeof assiette.nodeValue !== 'number'
) {
return {
...evaluatedNode,
nodeValue: null,
@ -105,7 +115,7 @@ const evaluate: evaluationFunction = function(node: any) {
return {
...evaluatedNode,
nodeValue: null,
activeTranche: activeTranche.missingVariables
missingVariables: activeTranche.missingVariables
}
}
@ -118,7 +128,8 @@ const evaluate: evaluationFunction = function(node: any) {
activeTranche.nodeValue = nodeValue
return {
...evaluatedNode,
nodeValue
nodeValue,
missingVariables: {}
}
}

View File

@ -1,10 +1,16 @@
import { evolve } from 'ramda'
import { ASTNode, Evaluation } from '../AST/types'
import { evaluationError, typeWarning } from '../error'
import { mergeAllMissing } from '../evaluation'
import { Evaluation } from '../types'
import parse from '../parse'
import { convertUnit, inferUnit } from '../units'
export const parseTranches = (parse, tranches) => {
type TrancheNode = { taux: ASTNode } | { montant: ASTNode }
export type TrancheNodes = [
...Array<TrancheNode & { plafond: ASTNode }>,
TrancheNode & { plafond?: ASTNode }
]
export const parseTranches = (tranches, context): TrancheNodes => {
return tranches
.map((t, i) => {
if (!t.plafond && i > tranches.length) {
@ -14,7 +20,13 @@ export const parseTranches = (parse, tranches) => {
}
return { ...t, plafond: t.plafond ?? Infinity }
})
.map(evolve({ taux: parse, montant: parse, plafond: parse }))
.map(
evolve({
taux: node => parse(node, context),
montant: node => parse(node, context),
plafond: node => parse(node, context)
})
)
}
export function evaluatePlafondUntilActiveTranche(

View File

@ -0,0 +1,58 @@
import { ASTNode, Unit } from '../AST/types'
import { typeWarning } from '../error'
import { makeJsx } from '../evaluation'
import { registerEvaluationFunction } from '../evaluationFunctions'
import parse from '../parse'
import { convertUnit, parseUnit } from '../units'
export type UnitéNode = {
unit: Unit
explanation: ASTNode
jsx: any
nodeKind: 'unité'
}
function MecanismUnité({ explanation, nodeValue, unit }) {
return makeJsx({ ...explanation, nodeValue, unit })
}
export default function parseUnité(v, context): UnitéNode {
const explanation = parse(v.valeur, context)
const unit = parseUnit(v.unité)
return {
jsx: MecanismUnité,
explanation,
unit,
nodeKind: parseUnité.nom
}
}
parseUnité.nom = 'unité' as const
registerEvaluationFunction(parseUnité.nom, function evaluate(node) {
const valeur = this.evaluateNode(node.explanation)
let nodeValue = valeur.nodeValue
if (nodeValue !== false && 'unit' in node) {
try {
nodeValue = convertUnit(
valeur.unit,
node.unit,
valeur.nodeValue as number
)
} catch (e) {
typeWarning(
this.cache._meta.contextRule,
`Erreur lors de la conversion d'unité explicite`,
e
)
}
}
return {
...node,
nodeValue,
explanation: valeur,
missingVariables: valeur.missingVariables
}
})

View File

@ -1,5 +1,7 @@
import { evaluationFunction } from '..'
import { registerEvaluationFunction } from '../evaluation'
import { ASTNode } from '../AST/types'
import { registerEvaluationFunction } from '../evaluationFunctions'
import parse from '../parse'
import {
createTemporalEvaluation,
narrowTemporalValue,
@ -7,7 +9,22 @@ import {
temporalAverage
} from '../temporal'
const evaluate: evaluationFunction = function(node: any) {
export type VariableTemporelleNode = {
explanation: {
period: {
start: ASTNode | undefined
end: ASTNode | undefined
}
value: ASTNode
}
jsx: any
nodeKind: 'variable temporelle'
}
const evaluate: evaluationFunction<'variable temporelle'> = function(
node: any
) {
const start =
node.explanation.period.start &&
this.evaluateNode(node.explanation.period.start)
@ -31,22 +48,25 @@ const evaluate: evaluationFunction = function(node: any) {
period: { start, end },
value
},
unit: value.unit
...('unit' in value && { unit: value.unit })
}
}
export default function parseVariableTemporelle(parse, v) {
const explanation = parse(v.explanation)
export default function parseVariableTemporelle(
v,
context
): VariableTemporelleNode {
const explanation = parse(v.explanation, context)
return {
nodeKind: 'variable temporelle',
jsx: null,
explanation: {
period: {
start: v.period.start && parse(v.period.start),
end: v.period.end && parse(v.period.end)
start: v.period.start && parse(v.period.start, context),
end: v.period.end && parse(v.period.end, context)
},
value: explanation
},
unit: explanation.unit
}
}
}

View File

@ -1,85 +1,80 @@
import { or } from 'ramda'
import { evaluationFunction } from '..'
import Variations from '../components/mecanisms/Variations'
import { ASTNode, Unit } from '../AST/types'
import { typeWarning } from '../error'
import { bonus, defaultNode, registerEvaluationFunction } from '../evaluation'
import { convertNodeToUnit } from '../nodeUnits'
import { bonus, defaultNode } from '../evaluation'
import { registerEvaluationFunction } from '../evaluationFunctions'
import { convertNodeToUnit, simplifyNodeUnit } from '../nodeUnits'
import parse from '../parse'
import {
liftTemporal2,
pureTemporal,
sometime,
Temporal,
temporalAverage
} from '../temporal'
import { inferUnit } from '../units'
import { mergeAllMissing } from './../evaluation'
export default function parse(recurse, v) {
export type VariationNode = {
explanation: Array<{
condition: ASTNode
consequence: ASTNode
}>
nodeKind: 'variations'
jsx: any
}
export const devariate = (k, v, context): ASTNode => {
if (k === 'valeur') {
return parse(v, context)
}
const { variations, ...factoredKeys } = v
const explanation = parse(
{
variations: variations.map(({ alors, sinon, si }) => {
const { attributs, ...otherKeys } = alors ?? sinon
return {
[alors !== undefined ? 'alors' : 'sinon']: {
...attributs,
[k]: {
...factoredKeys,
...otherKeys
}
},
...(si !== undefined && { si })
}
})
},
context
)
return explanation
}
export default function parseVariations(v, context) {
const explanation = v.map(({ si, alors, sinon }) =>
sinon !== undefined
? { consequence: recurse(sinon), condition: defaultNode(true) }
: { consequence: recurse(alors), condition: recurse(si) }
? { consequence: parse(sinon, context), condition: defaultNode(true) }
: { consequence: parse(alors, context), condition: parse(si, context) }
)
// TODO - find an appropriate representation
return {
explanation,
jsx: Variations,
category: 'mecanism',
name: 'variations',
nodeKind: 'variations',
type: 'numeric',
unit: inferUnit(
'+',
explanation.map(r => r.consequence.unit)
)
nodeKind: 'variations'
}
}
export function devariate(recurse, k, v) {
const explanation = devariateExplanation(recurse, k, v)
return {
explanation,
jsx: Variations,
category: 'mecanism',
name: 'variations',
nodeKind: 'variations',
type: 'numeric',
unit: inferUnit(
'+',
explanation.map(r => r.consequence.unit)
)
}
}
type Variation =
| {
si: any
alors: Record<string, unknown>
}
| {
sinon: Record<string, unknown>
}
const devariateExplanation = (
recurse,
mecanismKey,
v: { variations: Array<Variation> }
) => {
const { variations, ...fixedProps } = v
const explanation = variations.map(variation => ({
condition: 'sinon' in variation ? defaultNode(true) : recurse(variation.si),
consequence: recurse({
[mecanismKey]: {
...fixedProps,
...('sinon' in variation ? variation.sinon : variation.alors)
}
})
}))
return explanation
}
const evaluate: evaluationFunction = function(node: any) {
const [temporalValue, explanation, unit] = node.explanation.reduce(
const evaluate: evaluationFunction<'variations'> = function(node) {
const [temporalValue, explanation, unit] = node.explanation.reduce<
[
Temporal<any>,
VariationNode['explanation'],
Unit | undefined,
Temporal<any>
]
>(
(
[evaluation, explanations, unit, previousConditions],
{ condition, consequence },
@ -122,16 +117,17 @@ const evaluate: evaluationFunction = function(node: any) {
]
}
let evaluatedConsequence = this.evaluateNode(consequence)
try {
evaluatedConsequence = convertNodeToUnit(unit, evaluatedConsequence)
} catch (e) {
typeWarning(
this.cache._meta.contextRule,
`L'unité de la branche n° ${i +
1} du mécanisme 'variations' n'est pas compatible avec celle d'une branche précédente`,
e
)
if (unit) {
try {
evaluatedConsequence = convertNodeToUnit(unit, evaluatedConsequence)
} catch (e) {
typeWarning(
this.cache._meta.contextRule,
`L'unité de la branche n° ${i +
1} du mécanisme 'variations' n'est pas compatible avec celle d'une branche précédente`,
e
)
}
}
const currentValue = liftTemporal2(
(cond, value) => cond && value,
@ -153,12 +149,12 @@ const evaluate: evaluationFunction = function(node: any) {
liftTemporal2(or, previousConditions, currentCondition)
]
},
[pureTemporal(false), [], node.unit, pureTemporal(false)]
[pureTemporal(false), [], undefined, pureTemporal(false)]
)
const nodeValue = temporalAverage(temporalValue, unit)
const missingVariables = mergeAllMissing(
explanation.reduce(
explanation.reduce<ASTNode[]>(
(values, { condition, consequence }) => [
...values,
condition,
@ -167,14 +163,15 @@ const evaluate: evaluationFunction = function(node: any) {
[]
)
)
return {
return simplifyNodeUnit({
...node,
nodeValue,
unit,
...(unit !== undefined && { unit }),
explanation,
missingVariables,
...(temporalValue.length > 1 && { temporalValue })
}
})
}
registerEvaluationFunction('variations', evaluate)

View File

@ -1,6 +1,6 @@
import { mapTemporal } from './temporal'
import { convertUnit, simplifyUnit } from './units'
import { EvaluatedNode, Unit } from './types'
import { ASTNode, EvaluationDecoration, Unit } from './AST/types'
export function simplifyNodeUnit(node) {
if (!node.unit) {
@ -11,22 +11,23 @@ export function simplifyNodeUnit(node) {
return convertNodeToUnit(unit, node)
}
export function convertNodeToUnit<Names extends string>(
export function convertNodeToUnit(
to: Unit | undefined,
node: EvaluatedNode<Names, number>
node: ASTNode & EvaluationDecoration
) {
const temporalValue =
node.temporalValue && node.unit
? mapTemporal(
value => convertUnit(node.unit, to, value),
value => convertUnit(node.unit, to, value as number),
node.temporalValue
)
: node.temporalValue
return {
...node,
nodeValue: node.unit
? convertUnit(node.unit, to, node.nodeValue)
: node.nodeValue,
nodeValue:
node.unit && typeof node.nodeValue === 'number'
? convertUnit(node.unit, to, node.nodeValue)
: node.nodeValue,
...(temporalValue && { temporalValue }),
unit: to
}

View File

@ -1,6 +1,7 @@
import { Grammar, Parser } from 'nearley'
import { omit } from 'ramda'
import { isEmpty } from 'ramda'
import React from 'react'
import { ASTNode, ConstantNode } from './AST/types'
import { EngineError, syntaxError } from './error'
import { formatValue } from './format'
import grammar from './grammar.ne'
@ -18,23 +19,27 @@ import { mecanismMin } from './mecanisms/min'
import nonApplicable from './mecanisms/nonApplicable'
import { mecanismOnePossibility } from './mecanisms/one-possibility'
import operations from './mecanisms/operation'
import parDéfaut from './mecanisms/parDéfaut'
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'
import situation from './mecanisms/situation'
import { mecanismSum } from './mecanisms/sum'
import { mecanismSynchronisation } from './mecanisms/synchronisation'
import tauxProgressif from './mecanisms/tauxProgressif'
import unité from './mecanisms/unité'
import variableTemporelle from './mecanisms/variableTemporelle'
import variations, { devariate } from './mecanisms/variations'
import { parseReference, parseReferenceTransforms } from './parseReference'
import { EvaluatedRule } from './types'
import { Context } from './parsePublicodes'
import parseReference from './reference'
import parseRule from './rule'
export const parse = (rules, rule, parsedRules) => rawNode => {
export default function parse(rawNode, context: Context): ASTNode {
if (rawNode == null) {
syntaxError(
rule.dottedName,
context.dottedName,
`
Une des valeurs de la formule est vide.
Vérifiez que tous les champs à droite des deux points sont remplis`
@ -42,39 +47,43 @@ Vérifiez que tous les champs à droite des deux points sont remplis`
}
if (typeof rawNode === 'boolean') {
syntaxError(
rule.dottedName,
context.dottedName,
`
Les valeurs booléennes true / false ne sont acceptées.
Utilisez leur contrepartie française : 'oui' / 'non'`
)
}
const node =
typeof rawNode === 'object' ? rawNode : parseExpression(rule, '' + rawNode)
typeof rawNode === 'object' ? rawNode : parseExpression(rawNode, context)
if ('nom' in node) {
return parseRule(node, context)
}
return parseMecanism(rules, rule, parsedRules)(node)
return parseChainedMecanisms(node, context)
}
const compiledGrammar = Grammar.fromCompiled(grammar)
const parseExpression = (rule, rawNode) => {
function parseExpression(rawNode, context: Context): Object | undefined {
/* Strings correspond to infix expressions.
* Indeed, a subset of expressions like simple arithmetic operations `3 + (quantity * 2)` or like `salary [month]` are more explicit that their prefixed counterparts.
* This function makes them prefixed operations. */
try {
const [parseResult] = new Parser(compiledGrammar).feed(rawNode).results
const [parseResult] = new Parser(compiledGrammar).feed(rawNode + '').results
return parseResult
} catch (e) {
syntaxError(
rule.dottedName,
context.dottedName,
`\`${rawNode}\` n'est pas une expression valide`,
e
)
}
}
const parseMecanism = (rules, rule, parsedRules) => rawNode => {
function parseMecanism(rawNode, context: Context) {
if (Array.isArray(rawNode)) {
syntaxError(
rule.dottedName,
context.dottedName,
`
Il manque le nom du mécanisme pour le tableau : [${rawNode
.map(x => `'${x}'`)
@ -84,11 +93,10 @@ Les mécanisme possibles sont : 'somme', 'le maximum de', 'le minimum de', 'tout
)
}
rawNode = unfoldChainedMecanisms(rawNode)
const keys = Object.keys(rawNode)
if (keys.length > 1) {
syntaxError(
rule.dottedName,
context.dottedName,
`
Les mécanismes suivants se situent au même niveau : ${keys
.map(x => `'${x}'`)
@ -97,95 +105,77 @@ Cela vient probablement d'une erreur dans l'indentation
`
)
}
const mecanismName = Object.keys(rawNode)[0]
const values = rawNode[mecanismName]
// TODO: All parse functions should be "stateless" (ie be simple functions
// with the same list of parameters and not curried fantasy):
const parseFunctions = {
...statelessParseFunction,
filter: () =>
parseReferenceTransforms(
rules,
rule,
parsedRules
)({
filter: values.filter,
variable: values.explanation
}),
variable: () => parseReference(rules, rule, parsedRules, '')(values),
unitConversion: () =>
parseReferenceTransforms(
rules,
rule,
parsedRules
)({
variable: values.explanation,
unit: values.unit
})
if (isEmpty(rawNode)) {
return { nodeKind: 'constant', nodeValue: null }
}
const mecanismName = Object.keys(rawNode)[0]
const values = rawNode[mecanismName]
const parseFn = parseFunctions[mecanismName]
if (!parseFn) {
syntaxError(
rule.dottedName,
context.dottedName,
`
Le mécanisme ${mecanismName} est inconnu.
Vérifiez qu'il n'y ait pas d'erreur dans l'orthographe du nom.`
)
}
try {
const recurse = parse(rules, rule, parsedRules)
// Mécanisme de composantes. Voir mécanismes.md/composantes
if (values?.composantes) {
return decompose(recurse, mecanismName, values)
return decompose(mecanismName, values, context)
}
if (values?.variations) {
return devariate(recurse, mecanismName, values)
if (values?.variations && Object.values(values).length > 1) {
return devariate(mecanismName, values, context)
}
return parseFn(recurse, values, rule.dottedName)
return parseFn(values, context)
} catch (e) {
if (e instanceof EngineError) {
throw e
}
syntaxError(rule.dottedName, e.message)
syntaxError(
context.dottedName,
`➡️ Dans le mécanisme ${mecanismName}
${e.message}`
)
}
}
const chainableMecanisms = [
applicable,
nonApplicable,
parDéfaut,
situation,
plancher,
plafond,
unité,
arrondi
]
function unfoldChainedMecanisms(rawNode) {
if (Object.keys(rawNode).length === 1) {
return rawNode
function parseChainedMecanisms(rawNode, context: Context): ASTNode {
const parseFn = chainableMecanisms.find(fn => fn.nom in rawNode)
if (!parseFn) {
return parseMecanism(rawNode, context)
}
return chainableMecanisms.reduceRight(
(node, parseFn) => {
if (!(parseFn.nom in rawNode)) {
return node
}
return {
[parseFn.nom]: {
[parseFn.nom]: rawNode[parseFn.nom],
valeur: node
}
const { [parseFn.nom]: param, ...valeur } = rawNode
return parseMecanism(
{
[parseFn.nom]: {
valeur,
[parseFn.nom]: param
}
},
omit(
chainableMecanisms.map(fn => fn.nom),
rawNode
)
context
)
}
const statelessParseFunction = {
const parseFunctions = {
...operations,
...chainableMecanisms.reduce((acc, fn) => ({ [fn.nom]: fn, ...acc }), {}),
'une possibilité': mecanismOnePossibility,
'inversion numérique': mecanismInversion,
recalcul: mecanismRecalcul,
variable: parseReference,
'une de ces conditions': mecanismOneOf,
'toutes ces conditions': mecanismAllOf,
somme: mecanismSum,
@ -201,18 +191,18 @@ const statelessParseFunction = {
allègement: mecanismReduction,
variations,
synchronisation: mecanismSynchronisation,
'une possibilité': mecanismOnePossibility,
'inversion numérique': mecanismInversion,
recalcul: mecanismRecalcul,
valeur: (recurse, v) => recurse(v),
constant: (_, v) => ({
valeur: parse,
objet: v => ({
type: 'objet',
nodeValue: v,
nodeKind: 'constant'
}),
constant: v => ({
type: v.type,
constant: true,
nodeValue: v.nodeValue,
nodeKind: 'constant',
unit: v.unit,
// eslint-disable-next-line
jsx: (node: EvaluatedRule) => (
jsx: (node: ConstantNode) => (
<span className={v.type}>
{formatValue(node, {
// We want to display constants with full precision,
@ -223,3 +213,5 @@ const statelessParseFunction = {
)
})
}
export const mecanismKeys = Object.keys(parseFunctions)

View File

@ -0,0 +1,109 @@
import yaml from 'yaml'
import { traverseParsedRules, updateAST } from './AST'
import parse from './parse'
import { inlineReplacements } from './replacement'
import { Rule, RuleNode } from './rule'
import { disambiguateRuleReference } from './ruleUtils'
export type Context = {
dottedName: string
parsedRules: Record<string, RuleNode>
}
type RawRule = Omit<Rule, 'nom'> | string | undefined | number
export type RawPublicodes = Record<string, RawRule> | string
export default function parsePublicodes<Names extends string>(
rawRules: RawPublicodes,
partialContext: Partial<Context> = {}
) {
// STEP 1: parse Yaml
let rules =
typeof rawRules === 'string'
? (yaml.parse(('' + rawRules).replace(/\t/g, ' ')) as Record<
string,
RawRule
>)
: { ...rawRules }
// STEP 2: transpile [ref] writing
rules = transpileRef(rules)
// STEP 3: Rules parsing
const context: Context = {
dottedName: partialContext.dottedName ?? '',
parsedRules: partialContext.parsedRules ?? {}
}
Object.entries(rules).forEach(([dottedName, rule]) => {
if (rule == null) {
rule = {}
}
if (typeof rule !== 'object') {
rule = {
formule: rule
}
}
parse({ nom: dottedName, ...rule }, context)
})
let parsedRules = context.parsedRules
// STEP 4: Disambiguate reference
parsedRules = traverseParsedRules(
disambiguateReference(parsedRules),
parsedRules
) as Record<string, RuleNode>
// STEP 5: Inline replacements
parsedRules = inlineReplacements(parsedRules)
// TODO STEP 6: check for cycle
// TODO STEP 7: type check
return parsedRules
}
// We recursively traverse the YAML tree in order to transform named parameters
// into rules.
function transpileRef(object: Record<string, any> | string | Array<any>) {
if (Array.isArray(object)) {
return object.map(transpileRef)
}
if (!object || typeof object !== 'object') {
return object
}
object as Record<string, any>
return Object.entries(object).reduce((obj, [key, value]) => {
const match = /\[ref( (.+))?\]$/.exec(key)
if (!match) {
return { ...obj, [key]: transpileRef(value) }
}
const argumentType = key.replace(match[0], '').trim()
const argumentName = match[2]?.trim() || argumentType
return {
...obj,
[argumentType]: {
nom: argumentName,
valeur: transpileRef(value)
}
}
}, {})
}
export const disambiguateReference = (parsedRules: Record<string, RuleNode>) =>
updateAST(node => {
if (node.nodeKind === 'reference') {
return {
...node,
dottedName: disambiguateRuleReference(
parsedRules,
node.contextDottedName,
node.name
)
}
}
})

View File

@ -1,89 +0,0 @@
import { Leaf } from './components/mecanisms/common'
import parseRule from './parseRule'
import { disambiguateRuleReference } from './ruleUtils'
export const parseReference = (
rules,
rule,
parsedRules,
filter
) => partialReference => {
const dottedName = disambiguateRuleReference(
rules,
rule.dottedName,
partialReference
)
const inInversionFormula = rule.formule?.['inversion numérique']
const parsedRule =
parsedRules[dottedName] ||
// TODO: The 'inversion numérique' formula should not exist. The instructions to
// the evaluation should be enough to infer that an inversion is necessary
// (assuming it is possible, the client decides this) #767
(!inInversionFormula && parseRule(rules, dottedName, parsedRules))
const contextRuleName = rule.dottedName
if (
// TODO: At this point in the code, the parsedRule value should never be the
// string "being parsed", this is a ordering problem.
parsedRule !== 'being parsed' &&
parsedRule !== false &&
rule.dottedName &&
!contextRuleName.startsWith('[evaluation]')
) {
rule.dependencies?.add(dottedName)
}
const unit = parsedRule.unit
return {
nodeKind: 'reference',
jsx: Leaf,
name: partialReference,
category: 'reference',
partialReference,
dottedName,
explanation: { ...parsedRule, filter, contextRuleName },
unit
}
}
type parseReferenceTransformsParameters = {
variable: { fragments: Array<string> }
filter?: string
unit?: string
}
export const parseReferenceTransforms = (rules, rule, parsedRules) => ({
variable,
filter,
unit
}: parseReferenceTransformsParameters) => {
const originalNode = parseReference(
rules,
rule,
parsedRules,
filter
)(variable)
return {
...originalNode,
nodeKind: 'referenceWithTransforms',
explanation: {
originalNode,
filter,
unit
},
// Decorate node with the composante filter (either who is paying, either tax free)
...(filter
? {
cotisation: {
...(originalNode as any).cotisation,
'dû par': filter,
'impôt sur le revenu': filter
}
}
: {}),
unit: unit || originalNode.unit
}
}

View File

@ -1,272 +0,0 @@
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 { makeJsx } from './evaluation'
import { parse } from './parse'
import {
disambiguateRuleReference,
findParentDependencies,
nameLeaf
} from './ruleUtils'
import { ParsedRule, Rule, Rules } from './types'
import {
areUnitConvertible,
parseUnit,
serializeUnit,
simplifyUnit
} from './units'
import { capitalise0, coerceArray } from './utils'
export default function<Names extends string>(
rules: Rules<Names>,
dottedName,
parsedRules
): ParsedRule<Names> {
if (parsedRules[dottedName]) return parsedRules[dottedName]
parsedRules[dottedName] = 'being parsed'
/*
The parseRule function will traverse the tree of the `rule` and produce an
AST, an object containing other objects containing other objects... Some of
the attributes of the rule are dynamic, they need to be parsed. It is the
case of `non applicable si`, `applicable si`, `formule`. These attributes'
values themselves may have mechanism properties (e. g. `barème`) or inline
expressions (e. g. `maVariable + 3`). These mechanisms or variables are in
turn traversed by `parse()`. During this processing, 'evaluate' and'jsx'
functions are attached to the objects of the AST. They will be evaluated
during the evaluation phase, called "analyse".
*/
const parentDependencies = findParentDependencies(rules, dottedName)
let rawRule = rules[dottedName]
if (rawRule == null) {
rawRule = {}
}
if (typeof rawRule === 'string') {
rawRule = {
formule: rawRule
}
}
rawRule as Rule
if (
rawRule['par défaut'] &&
rawRule['formule'] &&
!rawRule.formule['une possibilité']
) {
throw new warning(
dottedName,
'Une règle ne peut pas avoir à la fois une formule ET une valeur par défaut.'
)
}
const name = nameLeaf(dottedName)
const unit = rawRule.unité != null ? parseUnit(rawRule.unité) : undefined
const rule = {
...rawRule,
rawRule,
name,
dottedName,
type: rawRule.type,
title: capitalise0(rawRule['titre'] || name),
examples: rawRule['exemples'],
icons: rawRule['icônes'],
summary: rawRule['résumé'],
unit,
parentDependencies,
dependencies: new Set(),
defaultValue: rawRule['par défaut']
}
const parsedRule = evolve({
// Voilà les attributs d'une règle qui sont aujourd'hui dynamiques, donc à traiter
// Les métadonnées d'une règle n'en font pas aujourd'hui partie
// condition d'applicabilité de la règle
parentDependencies: parents =>
parents.map(parent => {
const node = parse(rules, rule, parsedRules)(parent)
const jsx = ({ nodeValue, explanation }) =>
nodeValue === null ? (
<div>Active seulement si {makeJsx(explanation)}</div>
) : nodeValue === true ? (
<div>Active car {makeJsx(explanation)}</div>
) : nodeValue === false ? (
<div>Non active car {makeJsx(explanation)}</div>
) : null
return {
jsx,
category: 'ruleProp',
rulePropType: 'cond',
name: 'parentDependencies',
nodeKind: 'parentDependencies',
type: 'numeric',
explanation: node
}
}),
'non applicable si': evolveCond(
'non applicable si',
rule,
rules,
parsedRules
),
'applicable si': evolveCond('applicable si', rule, rules, parsedRules),
'rend non applicable': nonApplicableRules =>
coerceArray(nonApplicableRules).map(referenceName => {
return disambiguateRuleReference(rules, dottedName, referenceName)
}),
remplace: evolveReplacement(rules, rule, parsedRules),
defaultValue: value =>
typeof value === 'string' || typeof value === 'number'
? parse(rules, rule, parsedRules)(value)
: // TODO : An "object" default value is only used in the
// "synchronisation" mecanism. This should be refactored to not use the
// attribute "defaultValue"
typeof value === 'object'
? { ...value, nodeKind: 'defaultNode' }
: value,
formule: value => {
const child = parse(rules, rule, parsedRules)(value)
const jsx = ({ explanation }) => makeJsx(explanation)
return {
nodeKind: 'formula',
jsx,
category: 'ruleProp',
rulePropType: 'formula',
name: 'formule',
unit: child.unit,
explanation: child
}
}
})(rule)
parsedRules[dottedName] = {
// Pas de propriété explanation et jsx ici car on est parti du (mauvais)
// principe que 'non applicable si' et 'formule' sont particuliers, alors
// qu'ils pourraient être rangé avec les autres mécanismes
...parsedRule,
nodeKind: 'rule',
parsed: true,
unit:
parsedRule.unit ??
(parsedRule.formule?.unit && simplifyUnit(parsedRule.formule.unit)) ??
parsedRule.defaultValue?.unit,
isDisabledBy: [],
replacedBy: []
}
parsedRules[dottedName]['rendu non applicable'] = {
nodeKind: 'disabledBy',
jsx: ({ explanation: { isDisabledBy } }) => {
return (
isDisabledBy.length > 0 && (
<>
<h3>Exception{isDisabledBy.length > 1 && 's'}</h3>
<p>
<Trans>Cette règle ne s'applique pas pour</Trans> :{' '}
{isDisabledBy.map((rule, i) => (
<React.Fragment key={i}>
{i > 0 && ', '}
<RuleLinkWithContext dottedName={dottedName} />
</React.Fragment>
))}
</p>
</>
)
)
},
category: 'ruleProp',
rulePropType: 'cond',
name: 'rendu non applicable',
type: 'boolean',
explanation: parsedRules[dottedName]
}
if (process.env.NODE_ENV === 'development') {
Object.values(parsedRules[dottedName]['suggestions'] ?? {}).forEach(
suggestion => {
const parsedSuggestion = parse(rules, rule, parsedRules)(suggestion)
if (
!areUnitConvertible(
parsedRules[dottedName].unit,
parsedSuggestion.unit
) &&
parsedSuggestion.category !== 'reference'
) {
compilationError(
dottedName,
`La suggestion "${suggestion}" n'a pas une unité compatible avec la règle :
"${serializeUnit(parsedRules[dottedName].unit)}" et "${serializeUnit(
parsedSuggestion.unit
)}"`
)
}
}
)
}
return parsedRules[dottedName]
}
const evolveCond = (dottedName, rule, rules, parsedRules) => value => {
const child = parse(rules, rule, parsedRules)(value)
const jsx = ({ nodeValue, explanation, unit }) => (
<Mecanism name={dottedName} value={nodeValue} unit={unit}>
{explanation.category === 'variable' ? (
<div className="node">{makeJsx(explanation)}</div>
) : (
makeJsx(explanation)
)}
</Mecanism>
)
return {
jsx,
nodeKind: 'condition',
category: 'ruleProp',
rulePropType: 'cond',
dottedName,
type: 'boolean',
explanation: child
}
}
const evolveReplacement = (rules, rule, parsedRules) => replacements =>
coerceArray(replacements).map(reference => {
const referenceName =
typeof reference === 'string' ? reference : reference.règle
let replacementNode = reference.par
if (replacementNode != null) {
replacementNode = parse(rules, rule, parsedRules)(replacementNode)
}
const [whiteListedNames, blackListedNames] = [
reference.dans,
reference['sauf dans']
]
.map(dottedName => dottedName && coerceArray(dottedName))
.map(
names =>
names &&
names.map(dottedName =>
disambiguateRuleReference(rules, rule.dottedName, dottedName)
)
)
return {
referenceName: disambiguateRuleReference(
rules,
rule.dottedName,
referenceName
),
replacementNode,
whiteListedNames,
blackListedNames
}
})

View File

@ -1,122 +0,0 @@
import parseRule from './parseRule'
import yaml from 'yaml'
import { compose, dissoc, lensPath, over, set } from 'ramda'
import { compilationError } from './error'
import { parseReference } from './parseReference'
import { ParsedRules, Rules } from './types'
export default function parseRules<Names extends string>(
rawRules: Rules<Names> | string
): ParsedRules<Names> {
const rules =
typeof rawRules === 'string'
? (yaml.parse(rawRules.replace(/\t/g, ' ')) as Rules<Names>)
: { ...rawRules }
extractInlinedNames(rules)
/* First we parse each rule one by one. When a mechanism is encountered, it is
recursively parsed. When a reference to a variable is encountered, a
'variable' node is created, we don't parse variables recursively. */
const parsedRules = {}
/* A rule `A` can disable a rule `B` using the rule `rend non applicable: B`
in the definition of `A`. We need to map these exonerations to be able to
retreive them from `B` */
const nonApplicableMapping: Record<string, any> = {}
const replacedByMapping: Record<string, any> = {}
;(Object.keys(rules) as Names[]).map(dottedName => {
const parsedRule = parseRule(rules, dottedName, parsedRules)
if (parsedRule['rend non applicable']) {
nonApplicableMapping[parsedRule.dottedName] =
parsedRule['rend non applicable']
}
const replaceDescriptors = parsedRule['remplace']
if (replaceDescriptors) {
replaceDescriptors.forEach(
descriptor =>
(replacedByMapping[descriptor.referenceName] = [
...(replacedByMapping[descriptor.referenceName] ?? []),
{ ...descriptor, referenceName: parsedRule.dottedName }
])
)
}
})
Object.entries(nonApplicableMapping).forEach(([a, b]) => {
b.forEach(ruleName => {
parsedRules[ruleName].isDisabledBy.push(
parseReference(rules, parsedRules[ruleName], parsedRules, undefined)(a)
)
})
})
Object.entries(replacedByMapping).forEach(([a, b]) => {
parsedRules[a].replacedBy = b.map(({ referenceName, ...other }) => ({
referenceNode: parseReference(
rules,
parsedRules[referenceName],
parsedRules,
undefined
)(referenceName),
...other
}))
})
return parsedRules as ParsedRules<Names>
}
// We recursively traverse the YAML tree in order to extract named parameters
// into their own dedicated rules, and replace the inline definition with a
// reference to the newly created rule.
function extractInlinedNames(rules: Record<string, Record<string, any>>) {
const extractNamesInRule = (dottedName: string) => {
rules[dottedName] !== null &&
Object.entries(rules[dottedName]).forEach(
extractNamesInObject(dottedName)
)
}
const extractNamesInObject = (
dottedName: string,
context: Array<string | number> = []
) => ([key, value]: [string, Record<string, any>]) => {
const match = /\[ref( (.+))?\]$/.exec(key)
if (match) {
const argumentType = key.replace(match[0], '').trim()
const argumentName = match[2]?.trim() || argumentType
const extractedReferenceName = `${dottedName} . ${argumentName}`
if (typeof rules[extractedReferenceName] !== 'undefined') {
compilationError(
dottedName,
`Le paramètre [ref] ${argumentName} entre en conflit avec la règle déjà existante ${extractedReferenceName}`
)
}
rules[extractedReferenceName] = {
formule: value,
// The `virtualRule` parameter is used to avoid creating a
// dedicated documentation page.
virtualRule: true
}
rules[dottedName] = compose(
over(lensPath(context), dissoc(key)) as any,
set(lensPath([...context, argumentType]), extractedReferenceName)
)(rules[dottedName]) as any
extractNamesInRule(extractedReferenceName)
} else if (Array.isArray(value)) {
value.forEach((content: Record<string, any>, i) =>
Object.entries(content).forEach(
extractNamesInObject(dottedName, [...context, key, i])
)
)
} else if (value && typeof value === 'object') {
Object.entries(value).forEach(
extractNamesInObject(dottedName, [...context, key])
)
}
}
Object.keys(rules).forEach(extractNamesInRule)
}

View File

@ -0,0 +1,41 @@
import { EvaluationDecoration } from './AST/types'
import { Leaf } from './components/mecanisms/common'
import { InternalError } from './error'
import { registerEvaluationFunction } from './evaluationFunctions'
import { Context } from './parsePublicodes'
import { RuleNode } from './rule'
export type ReferenceNode = {
nodeKind: 'reference'
name: string
explanation?: RuleNode & EvaluationDecoration
contextDottedName: string
dottedName?: string
jsx: any
}
export default function parseReference(
v: string,
context: Context
): ReferenceNode {
return {
nodeKind: 'reference',
jsx: Leaf,
name: v,
contextDottedName: context.dottedName
}
}
registerEvaluationFunction('reference', function evaluateReference(node) {
if (!node.dottedName) {
throw new InternalError(node)
}
const explanation = this.evaluateNode(this.parsedRules[node.dottedName])
return {
...node,
explanation,
missingVariables: explanation.missingVariables,
nodeValue: explanation.nodeValue,
...('unit' in explanation && { unit: explanation.unit })
}
})

View File

@ -0,0 +1,164 @@
import { groupBy } from 'ramda'
import { AST } from 'yaml'
import { traverseParsedRules, updateAST } from './AST'
import { ASTNode } from './AST/types'
import { InternalError, warning } from './error'
import { defaultNode } from './evaluation'
import parse from './parse'
import { Context } from './parsePublicodes'
import { RuleNode } from './rule'
import { Rule } from './rule'
import { coerceArray } from './utils'
export type ReplacementNode = {
nodeKind: 'replacement'
definitionRule: ASTNode & { nodeKind: 'reference' }
replacedReference: ASTNode & { nodeKind: 'reference' }
replacementNode: ASTNode
whiteListedNames: Array<ASTNode & { nodeKind: 'reference' }>
jsx: any
blackListedNames: Array<ASTNode & { nodeKind: 'reference' }>
}
export function parseReplacements(
replacements: Rule['remplace'],
context: Context
): Array<ReplacementNode> {
if (!replacements) {
return []
}
return coerceArray(replacements).map(reference => {
if (typeof reference === 'string') {
reference = { règle: reference }
}
const replacedReference = parse(reference.règle, context)
let replacementNode = parse(reference.par ?? context.dottedName, context)
const [whiteListedNames, blackListedNames] = [
reference.dans ?? [],
reference['sauf dans'] ?? []
]
.map(dottedName => coerceArray(dottedName))
.map(refs => refs.map(ref => parse(ref, context)))
return {
nodeKind: 'replacement',
definitionRule: parse(context.dottedName, context),
replacedReference,
replacementNode,
jsx: null,
whiteListedNames,
blackListedNames
} as ReplacementNode
})
}
export function parseRendNonApplicable(
rules: Rule['rend non applicable'],
context: Context
): Array<ReplacementNode> {
return parseReplacements(rules, context).map(replacement => ({
...replacement,
replacementNode: defaultNode(false)
}))
}
export function inlineReplacements(
parsedRules: Record<string, RuleNode>
): Record<string, RuleNode> {
const replacements: Record<string, Array<ReplacementNode>> = groupBy(
(r: ReplacementNode) => {
if (!r.replacedReference.dottedName) {
throw new InternalError(r)
}
return r.replacedReference.dottedName
},
Object.values(parsedRules).flatMap(rule => rule.replacements)
)
return traverseParsedRules(
updateAST(node => {
if (node.nodeKind === 'replacement') {
// We don't want to replace references in replacements...
// Nor in ammended situation of recalcul and inversion (TODO)
return false
}
if (node.nodeKind === 'reference') {
if (!node.dottedName) {
throw new InternalError(node)
}
return replace(node, replacements[node.dottedName] ?? [])
}
}),
parsedRules
) as Record<string, RuleNode>
}
function replace(
node: ASTNode & { nodeKind: 'reference' }, //& { dottedName: string },
replacements: Array<ReplacementNode>
): ASTNode {
// TODO : handle transitivité
const applicableReplacements = replacements
.filter(
({ definitionRule }) =>
definitionRule.dottedName !== node.contextDottedName
)
.filter(
({ whiteListedNames }) =>
!whiteListedNames.length ||
whiteListedNames.some(name =>
node.contextDottedName.startsWith(name.dottedName as string)
)
)
.filter(
({ blackListedNames }) =>
!blackListedNames.length ||
blackListedNames.every(
name => !node.contextDottedName.startsWith(name.dottedName as string)
)
)
.sort((r1, r2) => {
// Replacement with whitelist conditions have precedence over the others
const criterion1 =
(+!!r2.whiteListedNames.length as number) -
+!!r1.whiteListedNames.length
// Replacement with blacklist condition have precedence over the others
const criterion2 =
+!!r2.blackListedNames.length - +!!r1.blackListedNames.length
return criterion1 || criterion2
})
if (applicableReplacements.length > 1) {
warning(
node.contextDottedName,
`
Il existe plusieurs remplacements pour la référence '${node.dottedName}'.
Lors de l'execution, ils seront résolus dans l'odre suivant :
${applicableReplacements.map(
replacement =>
`\n\t- Celui définit dans la règle '${replacement.definitionRule.dottedName}'`
)}
`
)
}
return applicableReplacements.reduceRight<ASTNode>(
(replacedNode, replacement) => {
return {
nodeKind: 'variations',
explanation: [
{
condition: replacement.definitionRule,
consequence: replacement.replacementNode
},
{
condition: defaultNode(true),
consequence: replacedNode
}
]
} as ASTNode & { nodeKind: 'variations' }
},
node
)
}

129
publicodes/source/rule.ts Normal file
View File

@ -0,0 +1,129 @@
import { filter, map, mapObjIndexed, pick } from 'ramda'
import { ASTNode, EvaluationDecoration } from './AST/types'
import RuleComponent from './components/rule/Rule'
import { bonus, mergeMissing } from './evaluation'
import { registerEvaluationFunction } from "./evaluationFunctions"
import parseNonApplicable from './mecanisms/nonApplicable'
import parse, { mecanismKeys } from './parse'
import { Context } from './parsePublicodes'
import { parseRendNonApplicable, parseReplacements, ReplacementNode } from './replacement'
import { nameLeaf, ruleParents } from './ruleUtils'
import { capitalise0 } from './utils'
export type Rule = {
formule?: Object | string
question?: string
description?: string
unité?: string
acronyme?: string
exemples?: any
nom: string
résumé?: string
'icônes'?: string
titre?: string
type?: string
note?: string
remplace?: RendNonApplicable | Array<RendNonApplicable>
'rend non applicable'?: Remplace | Array<string>
suggestions?: Record<string, string | number | object>
références?: { [source: string]: string }
}
type Remplace = {
règle: string
par?: Object | string | number
dans?: Array<string> | string
'sauf dans'?: Array<string> | string
} | string
type RendNonApplicable = Exclude<Remplace, {par: any}>
export type RuleNode = {
dottedName: string
title: string
nodeKind: "rule"
jsx: any
rawNode: Rule,
replacements: Array<ReplacementNode>
explanation: {
parent: ASTNode | false
valeur: ASTNode
}
suggestions: Record<string, ASTNode>
dependencies: Array<string>
}
export default function parseRule(
rawRule: Rule,
context: Context
): RuleNode {
const dottedName = [context.dottedName, rawRule.nom]
.filter(Boolean)
.join(' . ')
if (context.parsedRules[dottedName]) {
throw new Error(`La référence '${dottedName}' a déjà été définie`)
}
const ruleValue = {
...pick(mecanismKeys, rawRule),
...('formule' in rawRule && { valeur: rawRule.formule }),
'nom dans la situation': dottedName
}
const ruleContext = { ...context, dottedName }
const name = nameLeaf(dottedName)
const [parent] = ruleParents(dottedName)
const explanation = {
valeur: parse(ruleValue, ruleContext),
parent: !!parent && parse(parent, context),
}
context.parsedRules[dottedName] = filter(Boolean, {
dottedName,
replacements: [
...parseRendNonApplicable(rawRule["rend non applicable"], ruleContext),
...parseReplacements(rawRule.remplace, ruleContext),
],
title: capitalise0(rawRule['titre'] || name),
suggestions: mapObjIndexed(node => parse(node, ruleContext), rawRule.suggestions ?? {}),
nodeKind: "rule",
jsx: RuleComponent,
explanation,
rawNode: rawRule,
dependencies: [] as Array<string> // TODO
}) as RuleNode
return context.parsedRules[dottedName]
}
registerEvaluationFunction('rule', function evaluate(node) {
if (this.cache[node.dottedName]) {
return this.cache[node.dottedName]
}
const explanation = { ...node.explanation }
this.cache._meta.parentEvaluationStack ??= []
let parent: ASTNode & EvaluationDecoration | null = null
if (explanation.parent && !this.cache._meta.parentEvaluationStack.includes(node.dottedName)) {
this.cache._meta.parentEvaluationStack.push(node.dottedName)
parent = this.evaluateNode(explanation.parent) as ASTNode & EvaluationDecoration
explanation.parent = parent
this.cache._meta.parentEvaluationStack.pop()
}
let valeur: ASTNode & EvaluationDecoration | null = null
if (!parent || parent.nodeValue !== false) {
valeur = this.evaluateNode(explanation.valeur) as ASTNode & EvaluationDecoration
explanation.valeur = valeur
}
const evaluation = {
...node,
explanation,
nodeValue: valeur && 'nodeValue' in valeur ? valeur.nodeValue : false,
missingVariables: mergeMissing(valeur?.missingVariables, bonus(parent?.missingVariables)),
...(valeur && 'unit' in valeur && { unit: valeur.unit }),
}
this.cache[node.dottedName] = evaluation;
return evaluation;
})

View File

@ -1,5 +1,6 @@
import { last, pipe, range, take } from 'ramda'
import { Rule, Rules } from './types'
import { syntaxError } from './error'
import { RuleNode } from './rule'
const splitName = (str: string) => str.split(' . ')
const joinName = strs => strs.join(' . ')
@ -24,11 +25,11 @@ export function ruleParents<Names extends string>(
.reverse()
}
export function disambiguateRuleReference<Names extends string>(
rules: Rules<Names>,
contextName: Names,
export function disambiguateRuleReference<R extends Record<string, RuleNode>>(
rules: R,
contextName: string = '',
partialName: string
) {
): keyof R {
const possibleDottedName = [
contextName,
...ruleParents(contextName),
@ -36,37 +37,16 @@ export function disambiguateRuleReference<Names extends string>(
].map(x => (x ? x + ' . ' + partialName : partialName))
const dottedName = possibleDottedName.find(name => name in rules)
if (!dottedName) {
throw new Error(`La référence '${partialName}' est introuvable.
Vérifiez que l'orthographe et l'espace de nom sont corrects`)
syntaxError(
contextName,
`La référence '${partialName}' est introuvable.
Vérifiez que l'orthographe et l'espace de nom sont corrects`
)
throw new Error()
}
return dottedName
}
export function findParentDependencies<Names extends string>(
rules: Rules<Names>,
name: Names
): Array<Names> {
// A parent dependency means that one of a rule's parents is not just a namespace holder, it is a boolean question. E.g. is it a fixed-term contract, yes / no
// When it is resolved to false, then the whole branch under it is disactivated (non applicable)
// It lets those children omit obvious and repetitive parent applicability tests
return ruleParents(name)
.map(parent => [parent, rules[parent]] as [Names, Rule])
.filter(([_, rule]) => !!rule)
.filter(
([_, { question, unité, formule, type }]) =>
//Find the first "calculable" parent
(question && !unité && !formule) ||
type === 'groupe' ||
(question && formule?.['une possibilité'] !== undefined) ||
(typeof formule === 'string' && formule.includes(' = ')) ||
formule === 'oui' ||
formule === 'non' ||
formule?.['une de ces conditions'] ||
formule?.['toutes ces conditions']
)
.map(([name, _]) => name)
}
export function ruleWithDedicatedDocumentationPage(rule) {
return (
rule.virtualRule !== true &&

View File

@ -6,7 +6,13 @@ import {
getRelativeDate,
getYear
} from './date'
import { EvaluatedNode, Unit, Evaluation, Types } from './types'
import {
Unit,
Evaluation,
Types,
ASTNode,
EvaluationDecoration
} from './AST/types'
export type Period<T> = {
start: T | null
@ -63,8 +69,8 @@ export function parsePeriod<Date>(word: string, date: Date): Period<Date> {
throw new Error('Non implémenté')
}
export type TemporalNode<Names extends string> = Temporal<
EvaluatedNode<Names, number>
export type TemporalNode = Temporal<
ASTNode & EvaluationDecoration & { nodeValue: number }
>
export type Temporal<T> = Array<Period<string> & { value: T }>
@ -143,11 +149,9 @@ export function concatTemporals<T, U>(
)
}
export function liftTemporalNode<
T extends Types,
Names extends string,
N extends EvaluatedNode<Names, T>
>(node: N): Temporal<Pick<N, Exclude<keyof N, 'temporalValue'>>> {
export function liftTemporalNode<N extends ASTNode>(
node: N
): Temporal<Pick<N, Exclude<keyof N, 'temporalValue'>>> {
if (!('temporalValue' in node)) {
return pureTemporal(node)
}

View File

@ -1,13 +1,13 @@
import { assoc, mapObjIndexed } from 'ramda'
import { Rule, Rules } from './types'
import { RuleNode } from './rule'
type Translation = Record<string, string>
type translateAttribute = (
prop: string,
rule: Rule,
rule: RuleNode,
translation: Translation,
lang: string
) => Rule
) => RuleNode
/* Traduction */
const translateSuggestion: translateAttribute = (
@ -41,7 +41,7 @@ export const attributesToTranslate = [
]
const translateProp = (lang: string, translation: Translation) => (
rule: Rule,
rule: RuleNode,
prop: string
) => {
if (prop === 'suggestions' && rule?.suggestions) {
@ -56,8 +56,8 @@ function translateRule<Names extends string>(
lang: string,
translations: { [Name in Names]: Translation },
name: Names,
rule: Rule
): Rule {
rule: RuleNode
): RuleNode {
const ruleTrans = translations[name]
if (!ruleTrans) {
return rule
@ -68,13 +68,14 @@ function translateRule<Names extends string>(
)
}
export default function translateRules<Names extends string>(
export default function translateRules(
lang: string,
translations: { [Name in Names]: Translation },
rules: Rules<Names>
): Rules<Names> {
translations: Record<string, Translation>,
rules: Record<string, RuleNode>
): Record<string, RuleNode> {
const translatedRules = mapObjIndexed(
(rule: Rule, name: Names) => translateRule(lang, translations, name, rule),
(rule: RuleNode, name: string) =>
translateRule(lang, translations, name, rule),
rules
)

8
publicodes/source/types/dagres.d.ts vendored Normal file
View File

@ -0,0 +1,8 @@
declare module '@dagrejs/graphlib' {
export interface Graph {
setEdge(n1: string, n2: string): void
}
export type alg = {
findCycles: (g: Graph) => Array<Array<string>>
}
}

View File

@ -1,106 +0,0 @@
import { Temporal } from '../temporal'
type BaseUnit = string
export type Unit = {
numerators: Array<BaseUnit>
denominators: Array<BaseUnit>
}
export type Rule = {
formule?: string
question?: string
description?: string
unité?: string
acronyme?: string
exemples?: any
résumé?: string
titre?: string
type?: string
note?: string
suggestions?: { [description: string]: number }
références?: { [source: string]: string }
}
export type Rules<Names extends string = string> = Record<Names, Rule>
export type ParsedRule<Name extends string = string> = Rule & {
dottedName: Name
name: string
title: string
nodeKind: string
parentDependencies: Array<any>
rawRule: Rule
unit?: Unit
summary?: string
defaultValue?: any
defaultUnit?: Unit
examples?: any
API?: string
icons?: string
formule?: any
explanation?: any
isDisabledBy: Array<any>
dependencies: Set<Name>
replacedBy: Array<{
whiteListedNames: Array<Name>
blackListedNames: Array<Name>
referenceNode: ParsedRule<Name>
replacementNode: ParsedRule<Name>
}>
rulePropType?: string
jsx?: () => React.Component
cotisation?: Partial<{
'dû par': string
branche: string
destinataire: string
responsable: string
}>
taxe?: {
'dû par': string
}
}
export type ParsedRules<Names extends string = string> = {
[name in Names]: ParsedRule<name>
}
export type Types = number | boolean | string
// Idée : une évaluation est un n-uple : (value, unit, missingVariable, isApplicable)
// Une temporalEvaluation est une liste d'evaluation sur chaque période. : [(Evaluation, Period)]
export type Evaluation<T extends Types = Types> = T | false | null
export type EvaluatedNode<
Names extends string = string,
T extends Types = Types
> = {
nodeValue: Evaluation<T>
explanation: Record<string, any>
isDefault?: boolean
jsx: React.FunctionComponent<EvaluatedNode>
category?: string
dottedName: Names
missingVariables: Partial<Record<Names, number>>
} & (T extends number
? {
unit: Unit
temporalValue?: Temporal<Evaluation<number>>
} // eslint-disable-next-line @typescript-eslint/ban-types
: {})
// This type should be defined inline by the function evaluating the rule (and
// probably infered as its return type). This is only a partial definition but
// it type-checks.
export type EvaluatedRule<
Names extends string = string,
Explanation = ParsedRule<Names>,
Type extends Types = Types
> = ParsedRule<Names> &
EvaluatedNode<Names, Type> & {
isApplicable: boolean
explanation: Explanation
// eslint-disable-next-line @typescript-eslint/ban-types
'rendu non applicable': EvaluatedRule<Names, {}, Type>
'applicable si': EvaluatedNode<Names, Type>
'non applicable si': EvaluatedNode<Names, Type>
}

View File

@ -12,7 +12,7 @@ import {
without
} from 'ramda'
import i18n from './i18n'
import { Evaluation, Unit } from './types'
import { Evaluation, Unit } from './AST/types'
export const parseUnit = (string: string, lng = 'fr'): Unit => {
const [a, ...b] = string.split('/'),
@ -80,23 +80,26 @@ export const inferUnit = (
operator: SupportedOperators,
rawUnits: Array<Unit | undefined>
): Unit | undefined => {
const units = rawUnits.map(u => u || noUnit)
if (operator === '*')
return simplify({
numerators: unnest(units.map(u => u.numerators)),
denominators: unnest(units.map(u => u.denominators))
})
if (operator === '/') {
if (units.length !== 2)
if (rawUnits.length !== 2)
throw new Error('Infer units of a division with units.length !== 2)')
return inferUnit('*', [
units[0],
rawUnits[0] || noUnit,
{
numerators: units[1].denominators,
denominators: units[1].numerators
numerators: (rawUnits[1] || noUnit).denominators,
denominators: (rawUnits[1] || noUnit).numerators
}
])
}
const units = rawUnits.filter(Boolean)
if (units.length <= 1) {
return units[0]
}
if (operator === '*')
return simplify({
numerators: unnest(units.map(u => u?.numerators ?? [])),
denominators: unnest(units.map(u => u?.denominators ?? []))
})
if (operator === '-' || operator === '+') {
return rawUnits.find(u => u)
@ -198,6 +201,9 @@ export function convertUnit(
if (!value) {
return value
}
if (from === undefined) {
return value
}
const [fromSimplified, factorTo] = simplifyUnitWithValue(from || noUnit)
const [toSimplified, factorFrom] = simplifyUnitWithValue(to || noUnit)
return round(

View File

@ -1,6 +1,6 @@
import { expect } from 'chai'
import dedent from 'dedent-js'
import { cyclesInDependenciesGraph } from '../source/cyclesLib/graph'
import { cyclesInDependenciesGraph } from '../source/AST/graph'
describe('Cyclic dependencies detectron 3000 ™', () => {
it('should detect the trivial formule cycle', () => {
@ -26,4 +26,31 @@ describe('Cyclic dependencies detectron 3000 ™', () => {
const cycles = cyclesInDependenciesGraph(rules)
expect(cycles).to.deep.equal([['d', 'c', 'b', 'a']])
})
it('should not detect formule cycles due to parent dependancy', () => {
const rules = dedent`
a:
formule: b + 1
a . b:
formule: 3
`
const cycles = cyclesInDependenciesGraph(rules)
expect(cycles).to.deep.equal([])
})
it('should not detect cycles due to replacement', () => {
const rules = dedent`
a:
formule: b + 1
a . b:
formule: 3
a . c:
remplace: b
formule: a
`
const cycles = cyclesInDependenciesGraph(rules)
expect(cycles).to.deep.equal([["a . c", "a"]])
})
})

View File

@ -30,15 +30,15 @@ describe('inversions', () => {
taux: 77%
brut:
unité:
formule:
inversion numérique:
unité:
avec:
- net
`
const result = new Engine(rules)
.setSituation({ net: 2000 })
.evaluate('brut')
.setSituation({ net: '2000 €' })
.evaluate('brut')
expect(result.nodeValue).to.be.closeTo(2000 / (77 / 100), 0.0001 * 2000)
})
@ -51,15 +51,14 @@ describe('inversions', () => {
assiette: brut
taux: 77%
brut:
unité:
brut:
formule:
inversion numérique:
unité:
avec:
- net
`
const result = new Engine(rules).setSituation({ net: 0 }).evaluate('brut')
const result = new Engine(rules).setSituation({ net: '0 €' }).evaluate('brut')
expect(result.nodeValue).to.be.closeTo(0, 0.0001)
})
@ -77,9 +76,9 @@ describe('inversions', () => {
taux: 70%
brut:
unité:
formule:
inversion numérique:
unité:
avec:
- net
cadre:
@ -107,9 +106,9 @@ describe('inversions', () => {
taux: 70%
brut:
unité:
formule:
inversion numérique:
unité:
avec:
- net
cadre:
@ -134,7 +133,7 @@ describe('inversions', () => {
taux: 70%
`
const result = new Engine(rules)
.setSituation({ net: 2000 })
.setSituation({ net: '2000 €' })
.evaluate('brut')
expect(result.nodeValue).to.be.null
expect(Object.keys(result.missingVariables)).to.include('cadre')
@ -146,13 +145,11 @@ describe('inversions', () => {
formule:
produit:
assiette: assiette
variations:
- si: cadre
alors:
taux: 80%
- si: cadre != oui
alors:
taux: 70%
taux:
variations:
- si: cadre
alors: 80%
- sinon: 70%
total:
formule:
@ -161,9 +158,9 @@ describe('inversions', () => {
taux: 150%
brut:
unité:
formule:
inversion numérique:
unité:
avec:
- net
- total
@ -175,7 +172,7 @@ describe('inversions', () => {
`
const result = new Engine(rules)
.setSituation({ net: 2000, cadre: 'oui' })
.setSituation({ net: '2000 €', cadre: 'oui' })
.evaluate('total')
expect(result.nodeValue).to.be.closeTo(3750, 1)
expect(Object.keys(result.missingVariables)).to.be.empty
@ -195,25 +192,25 @@ describe('inversions', () => {
assiette: 67 + brut
composantes:
- attributs:
par: employeur
nom: employeur
taux: 100%
- attributs:
par: salarié
nom: salarié
taux: 50%
total:
formule: cotisation .employeur + cotisation .salarié
formule: cotisation . employeur + cotisation . salarié
brut:
unité:
formule:
inversion numérique:
unité:
avec:
- net
- total
`
const result = new Engine(rules)
.setSituation({ net: 2000 })
.setSituation({ net: '2000 €' })
.evaluate('total')
expect(result.nodeValue).to.be.closeTo(3750, 1)
expect(Object.keys(result.missingVariables)).to.be.empty

View File

@ -78,7 +78,7 @@ impôt sur le revenu à payer:
referenced in situation:
formule: 200
overwrited in situation:
formule: 100
formule: 100
result:
formule: overwrited in situation + 22
`

View File

@ -42,6 +42,7 @@ testSuites.forEach(([suiteName, suite]) => {
.evaluate(name, {
unit: defaultUnit
})
if (typeof valeur === 'number') {
expect(result.nodeValue).to.be.closeTo(valeur, 0.001)
} else if (valeur !== undefined) {

View File

@ -4,7 +4,8 @@ import Engine from '../source/index'
describe('Missing variables', function() {
it('should identify missing variables', function() {
const rawRules = {
sum: {},
ko: 'oui',
sum: 'oui',
'sum . startHere': {
formule: 2,
'non applicable si': 'sum . evt . ko'
@ -20,13 +21,13 @@ describe('Missing variables', function() {
new Engine(rawRules).evaluate('sum . startHere').missingVariables
)
expect(result).to.include('sum . evt . ko')
expect(result).to.include('sum . evt')
})
it('should identify missing variables mentioned in expressions', function() {
const rawRules = {
sum: {},
'sum . evt': {},
sum: 'oui',
'sum . evt': 'oui',
'sum . startHere': {
formule: 2,
'non applicable si': 'evt . nyet > evt . nope'
@ -44,7 +45,7 @@ describe('Missing variables', function() {
it('should ignore missing variables in the formula if not applicable', function() {
const rawRules = {
sum: {},
sum: 'oui',
'sum . startHere': {
formule: 'trois',
'non applicable si': '3 > 2'
@ -60,7 +61,7 @@ describe('Missing variables', function() {
it('should not report missing variables when "one of these" short-circuits', function() {
const rawRules = {
sum: {},
sum: 'oui',
'sum . startHere': {
formule: 'trois',
'non applicable si': {
@ -78,7 +79,8 @@ describe('Missing variables', function() {
it('should report "une possibilité" as a missing variable even though it has a formula', function() {
const rawRules = {
top: {},
top: 'oui',
ko: 'oui',
'top . startHere': { formule: 'trois' },
'top . trois': {
formule: { 'une possibilité': ['ko'] }
@ -93,11 +95,12 @@ describe('Missing variables', function() {
it('should not report missing variables when "une possibilité" is inapplicable', function() {
const rawRules = {
top: {},
top: 'oui',
ko: 'oui',
'top . startHere': { formule: 'trois' },
'top . trois': {
formule: { 'une possibilité': ['ko'] },
'non applicable si': 1
'non applicable si': 'oui'
}
}
const result = Object.keys(
@ -110,7 +113,8 @@ describe('Missing variables', function() {
it('should not report missing variables when "une possibilité" was answered', function() {
const rawRules = {
top: {},
top: 'oui',
ko: 'oui',
'top . startHere': { formule: 'trois' },
'top . trois': {
formule: { 'une possibilité': ['ko'] }
@ -128,29 +132,31 @@ describe('Missing variables', function() {
// TODO : réparer ce test
it.skip('should report missing variables in variations', function() {
const rawRules = {
top: {},
top: 'oui',
'top . startHere': {
formule: { somme: ['variations'] }
},
'top . variations': {
formule: {
barème: {
assiette: 2008,
variations: [
{
si: 'dix',
alors: {
multiplicateur: 'deux',
tranches: [
{ plafond: 1, taux: 0.1 },
{ plafond: 2, taux: 'trois' },
{ taux: 10 }
]
variations: [
{
si: 'dix',
alors: {
barème: {
assiette: 2008,
multiplicateur: 'deux',
tranches: [
{ plafond: 1, taux: 0.1 },
{ plafond: 2, taux: 'trois' },
{ taux: 10 }
]
}
},
}},
{
si: '3 > 4',
alors: {
barème: {
assiette: 2008,
multiplicateur: 'quatre',
tranches: [
{ plafond: 1, taux: 0.1 },
@ -158,11 +164,11 @@ describe('Missing variables', function() {
{ 'au-dessus de': 2, taux: 10 }
]
}
}
}}
]
}
}
},
,
'top . dix': {},
'top . deux': {},
'top . trois': {},
@ -182,11 +188,11 @@ describe('Missing variables', function() {
describe('nextSteps', function() {
it('should generate questions for simple situations', function() {
const rawRules = {
top: {},
top: 'oui',
'top . sum': { formule: 'deux' },
'top . deux': {
'non applicable si':'top . sum . evt',
formule: 2,
'non applicable si': 'top . sum . evt'
},
'top . sum . evt': {
titre: 'Truc',
@ -203,7 +209,7 @@ describe('nextSteps', function() {
})
it('should generate questions', function() {
const rawRules = {
top: {},
top: 'oui',
'top . sum': { formule: 'deux' },
'top . deux': {
formule: 'sum . evt'
@ -223,7 +229,7 @@ describe('nextSteps', function() {
it('should generate questions with more intricate situation', function() {
const rawRules = {
top: {},
top: 'oui',
'top . sum': { formule: { somme: [2, 'deux'] } },
'top . deux': {
formule: 2,

View File

@ -47,14 +47,13 @@ Barème à composantes:
- taux: 9%
plafond: 2
- taux: 29%
unité attendue: €/mois
exemples:
- nom:
situation:
assiette: 12000
base: 5000
valeur attendue: 1580
unité attendue: €/mois
ma condition:
@ -69,6 +68,7 @@ taux variable:
deuxième barème:
titre: Barème à taux variable
unité: €/mois
formule:
barème:
assiette: assiette
@ -77,6 +77,7 @@ deuxième barème:
- taux: taux variable
plafond: 1
- taux: 90%
unité attendue: '€/mois'
exemples:

View File

@ -5,7 +5,8 @@ douches par mois:
unité: douche/mois
Conversion de reference:
formule: douches par mois [douche/an]
formule: douches par mois
unité: douche/an
exemples:
- situation:
douches par mois: 30
@ -18,11 +19,6 @@ Conversion de reference 2:
- situation:
douches par mois: 30
valeur attendue: 360
- nom: unités par défaut prioritaire devant unité de variable
situation:
douches par mois: 30
unité par défaut: douche/mois
valeur attendue: 30
Conversion de variable:
formule: 1.5 kCo2/douche * douches par mois
@ -31,12 +27,7 @@ Conversion de variable:
douches par mois: 30
valeur attendue: 45
unité attendue: kCo2/mois
- nom: Unité cible de simulation
situation:
douches par mois: 20
unité par défaut: kCo2/an
unité attendue: kCo2/an
valeur attendue: 360
Conversion de variable et expressions:
unité: kCo2/an
@ -98,11 +89,11 @@ Conversion de mécanisme 2:
- taux: 3%
plafond: 7500 €/mois
- taux: 1%
unité: €/mois
exemples:
- situation:
assiette annuelle: 36000
valeur attendue: 131.25
unité par défaut: €/mois
Conversion dans une expression:
unité: €/an
@ -117,6 +108,7 @@ Conversion dans une comparaison:
mutuelle:
formule: 30 €/mois
retraite:
formule:
produit:
@ -129,10 +121,10 @@ Conversion dans une somme compliquée:
somme:
- mutuelle
- retraite
unité: €/mois
exemples:
- situation:
assiette annuelle: 20000
unité par défaut: €/mois
valeur attendue: 130
maladie:
@ -141,10 +133,10 @@ maladie:
assiette: assiette annuelle
composantes:
- attributs:
dû par: employeur
nom: employeur
taux: 15%
- attributs:
dû par: salarié
nom: salarié
taux: 5%
plafond: 1000 €/mois
@ -152,7 +144,7 @@ Conversion avec composantes:
unité: €/mois
formule:
somme:
- maladie .salarié
- maladie . salarié
- retraite
- mutuelle
exemples:
@ -165,8 +157,8 @@ Conversion dans un allègement:
allègement:
assiette: 1000€/an
abattement: 10€/mois
unité: €/an
exemples:
unité par défaut: €/an
valeur attendue: 880
Conversion dans avec un abattement en %:
@ -195,8 +187,8 @@ Conversion avec plusieurs échelons:
somme:
- prévoyance cadre
- 35€/mois
unité: €/an
exemples:
unité par défaut: €/an
situation:
assiette mensuelle: 1100
valeur attendue: 600
@ -209,5 +201,5 @@ Conversion de situation:
exemples:
unité par défaut: €/an
situation:
retraite: 4000
retraite: 4000 €/an
valeur attendue: 4360

View File

@ -31,7 +31,7 @@ parenthèses:
salaire de base:
unité: $
contrat:
contrat: oui
contrat . salaire de base:
produit:
@ -179,11 +179,12 @@ pourcentage:
multiplication et pourcentage:
formule: 38.1% * salaire de base
unité attendue: $
unité: $
exemples:
- situation:
salaire de base: 1000
valeur attendue: 381
unité attendue: $
litéral avec unité:
formule: 1 jour
@ -205,8 +206,9 @@ inférence d'unité littéraux:
catégorie d'activité:
formule:
une possibilité:
- commerciale
- artisanale
possibilités:
- commerciale
- artisanale
catégorie d'activité . artisanale:
catégorie d'activité . commerciale:
@ -225,7 +227,8 @@ revenu:
unité: €/mois
unité de variable modifiée:
formule: revenu [k€/an]
formule: revenu
unité: k€/an
exemples:
- situation:
revenu: 1000
@ -233,6 +236,7 @@ unité de variable modifiée:
opérations multiples:
formule: 4 * plafond sécurité sociale * 10%
unité: $
exemples:
- situation:
plafond sécurité sociale: 1000
@ -265,6 +269,7 @@ négation d'expressions:
variables négatives dans expression:
formule: 10% * (- salaire de base)
unité: $
exemples:
- situation:
salaire de base: 3000
@ -272,6 +277,7 @@ variables négatives dans expression:
expression dans situation:
formule: 10% * salaire de base
unité: $
exemples:
- situation:
salaire de base: 12 * 100
@ -282,6 +288,7 @@ salaire:
unité: €/mois
expression dans situation 2:
formule: 10% * salaire
unité: €/mois
exemples:
- situation:
salaire: 48k€/an

View File

@ -3,9 +3,9 @@ assiette:
Grille:
formule:
unité:
grille:
assiette: assiette
unité:
tranches:
- montant: 50
plafond: 1000
@ -36,9 +36,9 @@ plafond:
unité:
Grille avec valeur manquante:
formule:
unité:
grille:
assiette: assiette
unité:
tranches:
- montant: 100
plafond: plafond

View File

@ -53,7 +53,6 @@ mon facteur:
unité: patates
Multiplication à facteur:
unité attendue: patates
formule:
produit:
assiette: 100
@ -63,6 +62,7 @@ Multiplication à facteur:
- nom:
situation:
mon facteur: 3
unité attendue: patates
valeur attendue: 300
Multiplication complète:

View File

@ -3,30 +3,32 @@ cotisation:
multiplication:
assiette [ref]: 1000
taux [ref taux employeur]: 4%
test:
paramètre nommés:
formule: test
exemples:
- valeur attendue: 40
- situation:
test: cotisation . assiette
valeur attendue: 1000
- situation:
cotisation . assiette: 2000
valeur attendue: 80
- situation:
cotisation . assiette: 3000
cotisation . taux employeur: 3
valeur attendue: 90
paramètre nommés imbriqués:
test: cotisation . taux employeur
valeur attendue: 4
cotisation 2:
formule:
multiplication:
assiette [ref]:
valeur: 1000
plafond [ref]: 100
taux: 5%
paramètre nommés imbriqués:
formule: cotisation 2 . assiette . plafond
exemples:
- valeur attendue: 5
- situation:
paramètre nommés imbriqués . assiette . plafond: 200
valeur attendue: 10
- valeur attendue: 100
paramètre nommé utilisé dans la règle:
formule:
@ -36,4 +38,4 @@ paramètre nommé utilisé dans la règle:
valeur: assiette * 10%
plancher: 100
exemples:
- valeur attendue: 400
- valeur attendue: 400

View File

@ -14,6 +14,7 @@ SMIC net:
- valeur attendue: 500
Recalcule règle courante:
unité:
formule:
valeur: 10% * salaire brut
plafond:

View File

@ -1,7 +1,6 @@
restaurant . prix du repas:
formule: 10 €/repas
restaurant . client gourmand:
formule: oui
restaurant: oui
restaurant . prix du repas: 10 €/repas
restaurant . client gourmand: oui
restaurant . client enfant:
rend non applicable:
- client gourmand
@ -13,6 +12,7 @@ restaurant . prix du repas gourmand:
formule: 15 €/repas
restaurant . menu enfant:
formule: oui
applicable si: client enfant
remplace:
règle: prix du repas
@ -34,11 +34,12 @@ modifie une règle:
cotisations . assiette:
formule: 1000
cotisations:
formule:
somme:
- retraite .salarié
- retraite .employeur
- retraite . salarié
- retraite . employeur
- chômage
- maladie
@ -47,10 +48,10 @@ cotisations . retraite:
produit:
composantes:
- attributs:
dû par: employeur
nom: employeur
taux: 8%
- attributs:
dû par: salarié
nom: salarié
taux: 2%
assiette: assiette
@ -66,64 +67,76 @@ cotisations . maladie:
taux: 10%
assiette: assiette
remplacement sans boucle infinie si il n'y a pas de dépendances cycliques:
applicable si: exemple1
formule: cotisations
exemple1:
par défaut: non
remplace:
règle: cotisations . assiette
par: 100
exemples:
- situation:
exemple1: oui
valeur attendue: 30
remplacement contextuel par inclusion:
formule: cotisations
exemple2:
remplace:
règle: cotisations . assiette
par: 500
dans: cotisations . retraite
exemples:
- situation:
exemple2: oui
valeur attendue: 250
- nom: avec remplacement existant
situation:
exemple1: oui
exemple2: oui
valeur attendue: 70
applicable si: exemple2
par défaut: non
remplacement contextuel par exclusion:
formule: cotisations
exemple3:
par défaut: non
remplace:
règle: cotisations . assiette
par: 100
sauf dans:
- cotisations . chômage
- cotisations . maladie
exemple4:
par défaut: non
exemple4 . cotisations retraite:
remplace: cotisations . retraite
formule:
produit:
assiette: cotisations . assiette
composantes:
- attributs:
remplace: cotisations . retraite . employeur
nom: employeur
taux: 12%
- attributs:
remplace: cotisations . retraite . salarié
nom: salarié
taux: 8%
exemple5:
par défaut: non
remplace:
- règle: cotisations . chômage
par: 10
- règle: cotisations . maladie
par: 0
remplacements :
formule: cotisations
exemples:
- situation:
- nom: sans boucle infinie si il n'y a pas de dépendances cycliques
situation:
exemple1: oui
valeur attendue: 30
- nom: contextuel par inclusion
situation:
exemple2: oui
valeur attendue: 250
- nom: avec plusieurs remplacements existant pour une même variables
# ici, le remplacement de l'exemple 2 doit être effectué car plus précis que celui de l'exemple 1
situation:
exemple1: oui
exemple2: oui
valeur attendue: 70
- nom: contextuel par exclusion
situation:
exemple3: oui
valeur attendue: 210
applicable si: exemple3
remplacement d'une variable avec composante:
formule: cotisations
remplace:
règle: cotisations . retraite
par:
produit:
assiette: cotisations . assiette
composantes:
- attributs:
dû par: employeur
taux: 12%
- attributs:
dû par: salarié
taux: 8%
exemples:
- situation:
- nom: variable avec composante
situation:
exemple4: oui
valeur attendue: 400
- nom: avec remplacement dans un remplacement
@ -131,26 +144,12 @@ remplacement d'une variable avec composante:
exemple4: oui
exemple1: oui
valeur attendue: 40
applicable si: exemple4
remplacement de plusieurs variables d'un coup:
formule: cotisations
remplace:
- règle: cotisations . chômage
par: 10
- règle: cotisations . maladie
par: 0
exemples:
- situation:
- nom: plusieurs variables d'un coup
situation:
exemple5: oui
valeur attendue: 110
applicable si: exemple5
exemple1:
exemple2:
exemple3:
exemple4:
exemple5:
A:
formule: 1
@ -161,11 +160,11 @@ B:
C:
remplace: B
formule: 3
remplacement associatif:
formule: A
exemples:
- valeur attendue: 3
# TODO
# remplacement associatif:
# formule: A
# exemples:
# - valeur attendue: 3
x:
formule: non
@ -183,6 +182,7 @@ remplacement non applicable car branche desactivée:
- valeur attendue: 1
# Remplacement non effectué dans la formule du remplacement
espace: oui
espace . valeur:
formule: 20
espace . remplacement:

View File

@ -3,24 +3,26 @@ localisation:
code commune:
formule:
synchronisation:
API: localisation
data: localisation
chemin: code
exemples:
- nom: Base
situation:
localisation:
code: 29200
objet:
code: 29200
valeur attendue: 29200
région:
formule:
synchronisation:
API: localisation
data: localisation
chemin: région . nom
exemples:
- nom: Base
situation:
localisation:
région:
nom: Bretagne
objet:
région:
nom: Bretagne
valeur attendue: Bretagne

View File

@ -0,0 +1,27 @@
cotisation retraite:
valeur sans unité:
formule:
valeur: 100
unité:
exemples:
- unité attendue:
conversion d'unité:
formule:
valeur: 12 mois
unité: an
exemples:
- unité attendue: an
valeur attendue: 1
unité chainée:
formule:
produit:
assiette: 10 €/mois
taux: 50%
unité: €/an
exemples:
- unité attendue: €/an
valeur attendue: 60

View File

@ -1,260 +0,0 @@
variable temporelle numérique . le . valeur:
formule: 40 €/mois | le 02/04/2019
variable temporelle numérique . le . test date applicable:
formule: valeur | le 02/04/2019
exemples:
- valeur attendue: 40
variable temporelle numérique . le . test date non applicable:
formule: valeur | le 02/03/2021
exemples:
- valeur attendue: false
variable temporelle numérique . depuis . valeur:
formule: 40 €/mois | depuis le 02/04/2019
variable temporelle numérique . depuis . test date applicable:
formule: valeur | depuis le 06/04/2019
exemples:
- valeur attendue: 40
variable temporelle numérique . depuis . test date non applicable:
formule: valeur | le 08/03/2019
exemples:
- valeur attendue: false
variable temporelle numérique . intervalle . valeur:
formule: 40 €/mois | du 02/04/2019 | au 04/05/2020
variable temporelle numérique . intervalle . test date applicable:
formule: valeur | le 06/04/2019
exemples:
- valeur attendue: 40
variable temporelle numérique . intervalle . test date applicable 2:
formule: valeur | depuis le 05/06/2019 | jusqu'au 19/04/2020
exemples:
- valeur attendue: 40
variable temporelle numérique . intervalle . test date non applicable:
formule: valeur | le 08/03/2021
exemples:
- valeur attendue: false
variable temporelle numérique . intervalle . test date non applicable 2:
formule: valeur | le 28/01/2019
exemples:
- valeur attendue: false
variable temporelle numérique . variable . date limite de paiement:
formule: 03/09/2020
variable temporelle numérique . variable . majorations de retard:
formule: '40 €/jour | à partir de : date limite de paiement'
variable temporelle numérique . variable . test date non applicable:
formule: "majorations de retard | jusqu'au : 02/09/2020"
exemples:
- valeur attendue: false
variable temporelle numérique . variable . test date non applicable 2:
formule: majorations de retard | du 01/02/2020 | au 03/08/2020
exemples:
- valeur attendue: false
variable temporelle numérique . variable . test date applicable:
formule: 'majorations de retard | depuis la : date limite de paiement'
exemples:
- valeur attendue: 40
variable temporelle numérique . variable . test date applicable 2:
formule: majorations de retard | le 03/09/2020
exemples:
- valeur attendue: 40
prix:
formule: (20 €/mois | à partir du 15/11/2019) + (10 €/mois | à partir du 01/02/2020)
date:
variable temporelle numérique . test addition:
formule: 'prix | le : date'
exemples:
- situation:
date: 01/01/2019
valeur attendue: non
- situation:
date: 15/12/2019
valeur attendue: 20
- situation:
date: 12/09/2020
valeur attendue: 30
prix avec variations:
formule: prix - (prix * 50% | du 01/01/2020 | au 31/01/2020)
début:
fin:
variable temporelle numérique . expression . multiplication:
formule: "prix avec variations | depuis : début | jusqu'à : fin"
# 20 [avant janvier] / 10 [pendant janvier] | 30 [pendant et après février]
exemples:
- situation:
début: 01/01/2020
fin: 31/01/2020
valeur attendue: 10
unité attendue: €/mois
- situation:
début: 01/01/2020
fin: 29/02/2020
valeur attendue: 20
unité attendue: €/mois
- situation:
début: 01/02/2020
fin: 31/03/2020
valeur attendue: 30
unité attendue: €/mois
taux associé:
formule:
variations:
- si: prix avec variations >= 20 €/mois
alors: 10%/mois
- si: prix avec variations < 20 €/mois
alors: 60%/mois
# Cette formule peut paraître bizarre, mais lorsque le prix est non
# applicable, c'est bien le sinon qui s'applique
- sinon: 5%/mois
variable temporelle numérique . variation:
formule: "taux associé | depuis : début | jusqu'à : fin"
exemples:
- situation:
début: 01/01/2020
fin: 31/01/2020
valeur attendue: 60
- situation:
début: 01/01/2020
fin: 29/02/2020
valeur attendue: 35
- situation:
début: 01/02/2020
fin: 31/03/2020
valeur attendue: 10
- situation:
début: 01/10/2019
fin: 30/10/2019
valeur attendue: 5
contrat salarié . date d'embauche:
formule: 12/09/2018
contrat salarié . salaire:
formule:
somme:
- brut de base
- primes
contrat salarié . salaire . brut de base:
formule:
somme:
- "2000€/mois | depuis : date d'embauche | jusqu'au 08/08/2019"
- 2200€/mois | depuis le 09/08/2019
contrat salarié . salaire . primes:
formule: 2000€/mois | du 01/12/2019 | au 31/12/2019
plafond sécurité sociale:
formule:
somme:
- 3377 €/mois | du 01/01/2019 | au 31/12/2019
- 3424 €/mois | du 01/01/2020 | au 31/12/2020
contrat salarié . cotisations . retraite:
formule:
multiplication:
assiette: salaire
plafond: plafond sécurité sociale
taux: 10%
variable temporelle numérique . somme:
formule: contrat salarié . salaire | du 01/12/2019 | au 31/12/2019
exemples:
- valeur attendue: 4200 # 2000 + 2200
variable temporelle numérique . somme avec valeur changeant au cours du mois:
formule: contrat salarié . salaire | du 01/08/2019 | au 31/08/2019
exemples:
- valeur attendue: 2148.387 # (2000 * 8 + 2200 * 23)/31
variable temporelle numérique . multiplication:
formule: contrat salarié . cotisations . retraite | du 01/05/2019 | au 31/05/2019
exemples:
- valeur attendue: 200 # 2000 * 10%
variable temporelle numérique . multiplication avec valeur changeant au cours du mois:
formule: contrat salarié . cotisations . retraite | du 01/08/2019 | au 31/08/2019
exemples:
- valeur attendue: 214.839 # (2000 * 8 + 2200 * 23)/31
variable temporelle numérique . multiplication avec valeur au dessus du plafond:
formule: contrat salarié . cotisations . retraite | du 01/12/2019 | au 31/12/2019
exemples:
- valeur attendue: 337.7 # (2000 * 8 + 2200 * 23)/31
variable temporelle numérique . multiplication avec valeur sur l'année:
formule: contrat salarié . cotisations . retraite | du 01/01/2019 | au 31/12/2019
exemples:
# 200 * 7 [janvier-juin]
# + 214.839 [juillet]
# + 220 * 3 [aout-novembre]
# + 337.7 [décembre]
# /12 mois
- valeur attendue: 217.7115
# test . proratisation du salaire avec entrée en cours de mois:
# formule: salaire brut [avril 2019]
# exemples:
# - valeur attendue: 400 # (2000 * 6)/30
cotisation spéciale:
formule:
barème:
assiette: contrat salarié . salaire
multiplicateur: plafond sécurité sociale
tranches:
- taux: 0%
plafond: 10%
- taux: 10%
plafond: 20%
- taux: 30%
plafond: 50%
- taux: 40%
plafond: 100%
- taux: 50%
variable temporelle numérique . barème:
formule: cotisation spéciale | du 01/01/2019 | au 31/12/2019
exemples:
- valeur attendue: 567.438
grille:
formule:
barème:
assiette: contrat salarié . salaire
tranches:
- montant: 5 heures
plafond: 1000
- montant: 10 heures
plafond: 2000
- montant: 30 heures
plafond: 4000
- montant: 40 heures
variable temporelle numérique . grille:
formule: cotisation spéciale | du 01/01/2019 | au 31/12/2019
exemples:
- valeur attendue: 567.438
condition:
date applicable:
applicable si: condition
formule: 10/01/2019
variable temporelle . date non applicable:
formule: '(30 | à partir de : date applicable) | le 09/01/2019'
exemples:
- situation:
condition: oui
valeur attendue: false
- situation:
condition: non
valeur attendue: 30

View File

@ -166,7 +166,7 @@ variations sans unité:
exemples:
- valeur attendue: 7
unité attendue: '%'
taux réduit:
variations dans un produit:
formule:
@ -184,4 +184,4 @@ variations dans un produit:
valeur attendue: 79.35
- situation:
taux réduit: non
valeur attendue: 120.75
valeur attendue: 120.75

View File

@ -2,6 +2,7 @@
"extends": "../tsconfig.json",
"compilerOptions": {
"outDir": "./dist/types",
"jsx": "react",
"declaration": true,
"emitDeclarationOnly": true
},