⚙️🔥 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 #1191pull/1275/head
parent
9bc80e4158
commit
ba01ae2d4f
|
@ -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:
|
||||
|
|
|
@ -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 l’article 5 de l’Accord 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:
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -44,7 +44,7 @@ const rules: Rules = {
|
|||
...professionLibérale,
|
||||
...entrepriseEtablissement,
|
||||
...protectionSociale,
|
||||
...salarié,
|
||||
... salarié,
|
||||
...CCBatiment,
|
||||
...CCHotels,
|
||||
...CCOptique,
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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) => [
|
||||
|
|
|
@ -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 :**
|
||||
|
|
|
@ -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]])
|
||||
)
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
})
|
|
@ -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>
|
||||
}
|
|
@ -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 {
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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))
|
||||
])
|
||||
)
|
||||
|
||||
|
|
|
@ -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)
|
||||
)
|
||||
}
|
|
@ -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]])
|
||||
)
|
||||
}
|
|
@ -1,3 +0,0 @@
|
|||
import { cyclicDependencies } from './graph'
|
||||
|
||||
export default { cyclicDependencies }
|
|
@ -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)]
|
||||
)
|
||||
}
|
|
@ -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)}
|
||||
`
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 }
|
||||
}
|
|
@ -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>
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
|
@ -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'
|
||||
|
||||
|
|
|
@ -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') %}
|
||||
|
|
|
@ -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 }]) => {
|
||||
|
|
|
@ -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]) {
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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'
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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'
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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 }
|
||||
}))
|
||||
|
|
|
@ -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)
|
||||
])
|
||||
)
|
||||
|
||||
|
|
|
@ -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)
|
|
@ -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)
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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)
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
}
|
||||
})
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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: {}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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
|
||||
}
|
||||
})
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
})
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
})
|
|
@ -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)
|
||||
}
|
|
@ -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 })
|
||||
}
|
||||
})
|
|
@ -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
|
||||
)
|
||||
}
|
|
@ -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;
|
||||
})
|
||||
|
|
@ -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 &&
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
)
|
||||
|
||||
|
|
|
@ -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>>
|
||||
}
|
||||
}
|
|
@ -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>
|
||||
}
|
|
@ -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(
|
||||
|
|
|
@ -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"]])
|
||||
})
|
||||
})
|
||||
|
|
|
@ -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:
|
||||
dû par: employeur
|
||||
nom: employeur
|
||||
taux: 100%
|
||||
- attributs:
|
||||
dû 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
|
||||
|
|
|
@ -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
|
||||
`
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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
|
|
@ -14,6 +14,7 @@ SMIC net:
|
|||
- valeur attendue: 500
|
||||
|
||||
Recalcule règle courante:
|
||||
unité: €
|
||||
formule:
|
||||
valeur: 10% * salaire brut
|
||||
plafond:
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -2,6 +2,7 @@
|
|||
"extends": "../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "./dist/types",
|
||||
"jsx": "react",
|
||||
"declaration": true,
|
||||
"emitDeclarationOnly": true
|
||||
},
|
||||
|
|
Loading…
Reference in New Issue