diff --git a/source/engine/known-mecanisms.yaml b/source/engine/known-mecanisms.yaml index 2bacde10f..51f8ee2bc 100644 --- a/source/engine/known-mecanisms.yaml +++ b/source/engine/known-mecanisms.yaml @@ -136,6 +136,13 @@ barème linéaire: Il est composé de tranches qui se suivent. Il suffit de trouver l'assiette qui correspond à la tranche, et de multiplier le taux associé avec l'assiette. Un montant fixe pour chaque tranche peut aussi remplacer le taux, rendant le barème encore plus simple, mais moins "juste", car moins continu. +barème continu: + type: numeric + description: | + Ce barème définit des points A(x1, y1) B(x2, y2) C(x3, y3) etc. On trace alors une droite entre ces points. x correspond à l'assiette du barème, et y au taux qui va être appliqué à l'assiette pour déterminer le montant du calcul. + Les x1, x2, peuvent éventuellement être des unités d'un multiplicateur. + + complément: type: numeric description: | diff --git a/source/engine/mecanisms.js b/source/engine/mecanisms.js index e55a871cb..8736a3c65 100644 --- a/source/engine/mecanisms.js +++ b/source/engine/mecanisms.js @@ -3,7 +3,6 @@ import { path, mergeWith, objOf, - toPairs, dissoc, add, find, @@ -27,7 +26,12 @@ import { subtract, sum, isNil, - reject + reject, + aperture, + sort, + toPairs, + reduced, + last } from 'ramda' import React from 'react' import { Trans } from 'react-i18next' @@ -779,6 +783,57 @@ export let mecanismScale = (recurse, k, v) => { } } +export let mecanismContinuousScale = (recurse, k, v) => { + let objectShape = { + assiette: false, + multiplicateur: constantNode(1) + } + let effect = ({ assiette, multiplicateur, points }) => { + if (anyNull([assiette, multiplicateur])) return null + //We'll build a linear function given the two constraints that must be respected + return pipe( + toPairs, + // we don't rely on the sorting of objects + sort(([k1], [k2]) => k1 - k2), + points => [...points, [Infinity, last(points)[1]]], + aperture(2), + reduce((_, [[lowerLimit, lowerRate], [upperLimit, upperRate]]) => { + let x1 = val(multiplicateur) * lowerLimit, + x2 = val(multiplicateur) * upperLimit, + y1 = val(assiette) * val(recurse(lowerRate)), + y2 = val(assiette) * val(recurse(upperRate)) + if (val(assiette) > x1 && val(assiette) <= x2) { + // Outside of these 2 limits, it's a linear function a * x + b + let a = (y2 - y1) / (x2 - x1), + b = y1 - x1 * a + return reduced(a * val(assiette) + b) + } + }, 0) + )(points) + } + let explanation = { + ...parseObject(recurse, objectShape, v), + points: v.points + }, + evaluate = evaluateObject(objectShape, effect) + let jsx = (nodeValue, explanation) => ( + Réduction linéaire } + /> + ) + return { + evaluate, + jsx, + explanation, + category: 'mecanism', + name: 'réduction linéaire', + type: 'numeric' + } +} + export let mecanismMax = (recurse, k, v) => { let explanation = v.map(recurse) diff --git a/source/engine/treat.js b/source/engine/treat.js index 699c9e4e2..190f8b261 100644 --- a/source/engine/treat.js +++ b/source/engine/treat.js @@ -39,6 +39,7 @@ import { mecanismProduct, mecanismScale, mecanismLinearScale, + mecanismContinuousScale, mecanismMax, mecanismMin, mecanismError, @@ -252,6 +253,7 @@ export let treatObject = (rules, rule, treatOptions) => rawNode => { multiplication: mecanismProduct, barème: mecanismScale, 'barème linéaire': mecanismLinearScale, + 'barème continu': mecanismContinuousScale, 'le maximum de': mecanismMax, 'le minimum de': mecanismMin, complément: mecanismComplement, diff --git a/test/mécanismes/barème-continu.yaml b/test/mécanismes/barème-continu.yaml new file mode 100644 index 000000000..b72126f90 --- /dev/null +++ b/test/mécanismes/barème-continu.yaml @@ -0,0 +1,40 @@ +- nom: base + format: nombre + formule: 300 + +- nom: assiette + format: nombre + +- test: Simple + formule: + barème continu: + assiette: assiette + multiplicateur: base + points: + 0: 0% + 0.4: 3.16% + 1.1: 6.35% + + exemples: + - nom: Premier intervale + situation: + assiette: 10 + valeur attendue: 0.026 + - nom: Deuxième intervale + situation: + assiette: 120 + valeur attendue: 3.792 + - nom: Premier point + situation: + assiette: 150 + valeur attendue: 5.423 + - nom: Deuxième point + situation: + assiette: 330 + valeur attendue: 20.955 + - nom: Au-delà + situation: + assiette: 1000 + valeur attendue: 63.5 + +