⚙️ Ajoute le mécanisme régularisation
- améliore la gestion des unités pour les variables temporellespull/866/head
parent
0a5aba9078
commit
665943288a
|
@ -1876,11 +1876,6 @@ contrat salarié . statut JEI . exonération de cotisations:
|
|||
- vieillesse .employeur
|
||||
|
||||
contrat salarié . réduction générale:
|
||||
aide:
|
||||
type: réduction de cotisations
|
||||
thème: aide bas salaires
|
||||
démarches: non
|
||||
alias: réduction fillon
|
||||
description: |
|
||||
Dans le cadre du pacte de responsabilité et de solidarité, le dispositif zéro cotisation Urssaf permet à l'employeur d'un salarié au Smic de ne plus payer aucune cotisation. Le montant de l'allègement est égal au produit de la rémunération annuelle brute par un coefficient. Il n'y a pas de formalité particulière à effectuer.
|
||||
références:
|
||||
|
|
|
@ -21,7 +21,11 @@ export function normalizeDate(
|
|||
const dateRegexp = /[\d]{2}\/[\d]{2}\/[\d]{4}/
|
||||
export function convertToDate(value: string): Date {
|
||||
const [day, month, year] = normalizeDateString(value).split('/')
|
||||
return new Date(+year, +month - 1, +day)
|
||||
var result = new Date(+year, +month - 1, +day)
|
||||
// Reset date to utc midnight for exact calculation of day difference (no
|
||||
// daylight saving effect)
|
||||
result.setMinutes(result.getMinutes() - result.getTimezoneOffset())
|
||||
return result
|
||||
}
|
||||
export function convertToDateIfNeeded(...values: string[]) {
|
||||
const dateStrings = values.map(dateString => '' + dateString)
|
||||
|
@ -47,3 +51,35 @@ export function getRelativeDate(date: string, dayDifferential: number): string {
|
|||
relativeDate.setDate(relativeDate.getDate() + dayDifferential)
|
||||
return convertToString(relativeDate)
|
||||
}
|
||||
|
||||
export function getYear(date: string): number {
|
||||
return +date.slice(-4)
|
||||
}
|
||||
|
||||
export function getDifferenceInDays(from: string, to: string): number {
|
||||
const millisecondsPerDay = 1000 * 60 * 60 * 24
|
||||
return (
|
||||
1 +
|
||||
(convertToDate(from).getTime() - convertToDate(to).getTime()) /
|
||||
millisecondsPerDay
|
||||
)
|
||||
}
|
||||
|
||||
export function getDifferenceInMonths(from: string, to: string): number {
|
||||
// We want to compute the difference in actual month between the two dates
|
||||
// For date that start during a month, a pro-rata will be done depending on
|
||||
// the duration of the month in days
|
||||
const [dayFrom, monthFrom, yearFrom] = from.split('/').map(x => +x)
|
||||
const [dayTo, monthTo, yearTo] = to.split('/').map(x => +x)
|
||||
const numberOfFullMonth = monthTo - monthFrom + 12 * (yearTo - yearFrom)
|
||||
const numDayMonthFrom = new Date(yearFrom, monthFrom, 0).getDate()
|
||||
const numDayMonthTo = new Date(yearTo, monthTo, 0).getDate()
|
||||
const prorataMonthFrom = (dayFrom - 1) / numDayMonthFrom
|
||||
const prorataMonthTo = dayTo / numDayMonthTo
|
||||
return numberOfFullMonth - prorataMonthFrom + prorataMonthTo
|
||||
}
|
||||
|
||||
export function getDifferenceInYears(from: string, to: string): number {
|
||||
// Todo : take leap year into account
|
||||
return getDifferenceInDays(from, to) / 365.25
|
||||
}
|
||||
|
|
|
@ -1,7 +1,5 @@
|
|||
import {
|
||||
add,
|
||||
any,
|
||||
equals,
|
||||
evolve,
|
||||
filter,
|
||||
fromPairs,
|
||||
|
@ -16,8 +14,8 @@ import {
|
|||
concatTemporals,
|
||||
liftTemporalNode,
|
||||
mapTemporal,
|
||||
periodAverage,
|
||||
pure,
|
||||
pureTemporal,
|
||||
temporalAverage,
|
||||
zipTemporals
|
||||
} from './period'
|
||||
|
||||
|
@ -47,14 +45,15 @@ export let evaluateNode = (cache, situationGate, parsedRules, node) => {
|
|||
: simplifyNodeUnit(evaluatedNode)
|
||||
return evaluatedNode
|
||||
}
|
||||
const sameUnitValues = (explanation, contextRule, mecanismName) => {
|
||||
const firstNodeWithUnit = explanation.find(node => !!node.unit)
|
||||
|
||||
function convertNodesToSameUnit(nodes, contextRule, mecanismName) {
|
||||
const firstNodeWithUnit = nodes.find(node => !!node.unit)
|
||||
if (!firstNodeWithUnit) {
|
||||
return [undefined, explanation.map(({ nodeValue }) => nodeValue)]
|
||||
return nodes
|
||||
}
|
||||
const values = explanation.map(node => {
|
||||
return nodes.map(node => {
|
||||
try {
|
||||
return convertNodeToUnit(firstNodeWithUnit?.unit, node).nodeValue
|
||||
return convertNodeToUnit(firstNodeWithUnit.unit, node)
|
||||
} catch (e) {
|
||||
typeWarning(
|
||||
contextRule,
|
||||
|
@ -63,77 +62,66 @@ const sameUnitValues = (explanation, contextRule, mecanismName) => {
|
|||
firstNodeWithUnit?.rawNode}'`,
|
||||
e
|
||||
)
|
||||
return node.nodeValue
|
||||
return node
|
||||
}
|
||||
})
|
||||
return [firstNodeWithUnit.unit, values]
|
||||
}
|
||||
|
||||
export let evaluateArray = (reducer, start) => (
|
||||
export const evaluateArray = (reducer, start) => (
|
||||
cache,
|
||||
situationGate,
|
||||
parsedRules,
|
||||
node
|
||||
) => {
|
||||
const evaluate = evaluateNode.bind(null, cache, situationGate, parsedRules)
|
||||
const temporalExplanation = concatTemporals(
|
||||
node.explanation.map(evaluate).map(liftTemporalNode)
|
||||
const evaluatedNodes = convertNodesToSameUnit(
|
||||
node.explanation.map(evaluate),
|
||||
cache._meta.contextRule,
|
||||
node.name
|
||||
)
|
||||
const temporalEvaluations = mapTemporal(explanation => {
|
||||
explanation
|
||||
const [unit, values] = sameUnitValues(
|
||||
explanation,
|
||||
cache._meta.contextRule,
|
||||
node.name
|
||||
)
|
||||
const nodeValue = values.some(value => value === null)
|
||||
? null
|
||||
: reduce(reducer, start, values)
|
||||
const missingVariables =
|
||||
node.nodeValue == null ? mergeAllMissing(explanation) : {}
|
||||
return {
|
||||
...node,
|
||||
nodeValue,
|
||||
explanation,
|
||||
missingVariables,
|
||||
unit
|
||||
}
|
||||
}, temporalExplanation)
|
||||
if (temporalEvaluations.length === 1) {
|
||||
return temporalEvaluations[0]
|
||||
if (!evaluatedNodes.every(Boolean)) {
|
||||
console.log(node.explanation)
|
||||
}
|
||||
const temporalValue = mapTemporal(node => node.nodeValue, temporalEvaluations)
|
||||
return {
|
||||
const temporalValues = concatTemporals(
|
||||
evaluatedNodes.map(
|
||||
({ temporalValue, nodeValue }) => temporalValue ?? pureTemporal(nodeValue)
|
||||
)
|
||||
)
|
||||
const temporalValue = mapTemporal(values => {
|
||||
if (values.some(value => value === null)) {
|
||||
return null
|
||||
}
|
||||
return reduce(reducer, start, values)
|
||||
}, temporalValues)
|
||||
|
||||
const baseEvaluation = {
|
||||
...node,
|
||||
explanation: evaluatedNodes,
|
||||
unit: evaluatedNodes[0].unit
|
||||
}
|
||||
if (temporalValue.length === 1) {
|
||||
return {
|
||||
...baseEvaluation,
|
||||
nodeValue: temporalValue[0].value
|
||||
}
|
||||
}
|
||||
return {
|
||||
...baseEvaluation,
|
||||
temporalValue,
|
||||
nodeValue: periodAverage(temporalValue)
|
||||
nodeValue: temporalAverage(temporalValue)
|
||||
}
|
||||
}
|
||||
|
||||
export let evaluateArrayWithFilter = (evaluationFilter, reducer, start) => (
|
||||
export const evaluateArrayWithFilter = (evaluationFilter, reducer, start) => (
|
||||
cache,
|
||||
situationGate,
|
||||
parsedRules,
|
||||
node
|
||||
) => {
|
||||
let evaluateOne = child =>
|
||||
evaluateNode(cache, situationGate, parsedRules, child),
|
||||
explanation = map(
|
||||
evaluateOne,
|
||||
filter(evaluationFilter(situationGate), node.explanation)
|
||||
),
|
||||
[unit, values] = sameUnitValues(
|
||||
explanation,
|
||||
cache._meta.contextRule,
|
||||
node.name
|
||||
),
|
||||
nodeValue = any(equals(null), values)
|
||||
? null
|
||||
: reduce(reducer, start, values),
|
||||
missingVariables =
|
||||
node.nodeValue == null ? mergeAllMissing(explanation) : {}
|
||||
|
||||
return { ...node, nodeValue, explanation, missingVariables, unit }
|
||||
return evaluateArray(reducer, start)(cache, situationGate, parsedRules, {
|
||||
...node,
|
||||
explanation: filter(evaluationFilter(situationGate), node.explanation)
|
||||
})
|
||||
}
|
||||
|
||||
export let defaultNode = nodeValue => ({
|
||||
|
@ -167,36 +155,53 @@ export let evaluateObject = (objectShape, effect) => (
|
|||
Object.fromEntries,
|
||||
concatTemporals(
|
||||
Object.entries(evaluations).map(([key, node]) =>
|
||||
zipTemporals(pure(key), liftTemporalNode(node))
|
||||
zipTemporals(pureTemporal(key), liftTemporalNode(node))
|
||||
)
|
||||
)
|
||||
)
|
||||
const temporalEvaluations = mapTemporal(
|
||||
explanations => effect(explanations, cache, situationGate, parsedRules),
|
||||
temporalExplanations
|
||||
)
|
||||
const temporalExplanation = mapTemporal(explanations => {
|
||||
const evaluation = effect(explanations, cache, situationGate, parsedRules)
|
||||
return {
|
||||
...evaluation,
|
||||
explanation: {
|
||||
...explanations,
|
||||
...evaluation.explanation
|
||||
}
|
||||
}
|
||||
}, temporalExplanations)
|
||||
|
||||
const sameUnitTemporalExplanation = convertNodesToSameUnit(
|
||||
temporalExplanation.map(x => x.value),
|
||||
cache._meta.contextRule,
|
||||
node.name
|
||||
).map((node, i) => ({
|
||||
...temporalExplanation[i],
|
||||
value: simplifyNodeUnit(node)
|
||||
}))
|
||||
|
||||
const temporalValue = mapTemporal(
|
||||
evaluation =>
|
||||
evaluation !== null && typeof evaluation === 'object'
|
||||
? evaluation.nodeValue
|
||||
: evaluation,
|
||||
temporalEvaluations
|
||||
({ nodeValue }) => nodeValue,
|
||||
sameUnitTemporalExplanation
|
||||
)
|
||||
const nodeValue = periodAverage(temporalValue)
|
||||
|
||||
return simplifyNodeUnit({
|
||||
const nodeValue = temporalAverage(temporalValue)
|
||||
if (nodeValue === 495) {
|
||||
console.log(temporalValue)
|
||||
}
|
||||
const baseEvaluation = {
|
||||
...node,
|
||||
nodeValue,
|
||||
...(temporalEvaluations.length > 1
|
||||
? { temporalValue }
|
||||
: {
|
||||
missingVariables: mergeAllMissing(Object.values(evaluations)),
|
||||
explanation: {
|
||||
...evaluations,
|
||||
...temporalEvaluations[0].additionalExplanation
|
||||
},
|
||||
unit: temporalEvaluations[0].unit
|
||||
})
|
||||
})
|
||||
unit: sameUnitTemporalExplanation[0].value.unit,
|
||||
explanation: evaluations
|
||||
}
|
||||
if (sameUnitTemporalExplanation.length === 1) {
|
||||
return {
|
||||
...baseEvaluation,
|
||||
explanation: sameUnitTemporalExplanation[0].value.explanation
|
||||
}
|
||||
}
|
||||
return {
|
||||
...baseEvaluation,
|
||||
temporalValue,
|
||||
temporalExplanation
|
||||
}
|
||||
}
|
||||
|
|
|
@ -15,11 +15,9 @@ import {
|
|||
path,
|
||||
pluck,
|
||||
reduce,
|
||||
subtract,
|
||||
toPairs
|
||||
} from 'ramda'
|
||||
import React from 'react'
|
||||
import { Trans } from 'react-i18next'
|
||||
import 'react-virtualized/styles.css'
|
||||
import { typeWarning } from './error'
|
||||
import {
|
||||
|
@ -281,7 +279,7 @@ export let mecanismRecalcul = dottedNameContext => (recurse, k, v) => {
|
|||
let evaluate = (currentCache, situationGate, parsedRules, node) => {
|
||||
let defaultRuleToEvaluate = dottedNameContext
|
||||
let nodeToEvaluate = recurse(node?.règle ?? defaultRuleToEvaluate)
|
||||
let cache = { _meta: currentCache._meta, _metaInRecalcul: true } // Create an empty cache
|
||||
let cache = { _meta: { ...currentCache._meta, inRecalcul: true } } // Create an empty cache
|
||||
let amendedSituation = Object.fromEntries(
|
||||
Object.keys(node.avec).map(dottedName => [
|
||||
disambiguateRuleReference(
|
||||
|
@ -293,7 +291,7 @@ export let mecanismRecalcul = dottedNameContext => (recurse, k, v) => {
|
|||
])
|
||||
)
|
||||
|
||||
if (currentCache._metaInRecalcul) {
|
||||
if (currentCache._meta.inRecalcul) {
|
||||
return defaultNode(false)
|
||||
}
|
||||
|
||||
|
@ -376,7 +374,7 @@ export let mecanismReduction = (recurse, k, v) => {
|
|||
cache
|
||||
) => {
|
||||
let v_assiette = assiette.nodeValue
|
||||
if (v_assiette == null) return null
|
||||
if (v_assiette == null) return { nodeValue: null }
|
||||
if (assiette.unit) {
|
||||
try {
|
||||
franchise = convertNodeToUnit(assiette.unit, franchise)
|
||||
|
@ -431,8 +429,8 @@ export let mecanismReduction = (recurse, k, v) => {
|
|||
: montantFranchiséDécoté
|
||||
return {
|
||||
nodeValue,
|
||||
additionalExplanation: {
|
||||
unit: assiette.unit,
|
||||
unit: assiette.unit,
|
||||
explanation: {
|
||||
franchise,
|
||||
plafond,
|
||||
abattement
|
||||
|
@ -509,9 +507,10 @@ export let mecanismProduct = (recurse, k, v) => {
|
|||
)
|
||||
return {
|
||||
nodeValue,
|
||||
additionalExplanation: {
|
||||
plafondActif: assiette.nodeValue > plafond.nodeValue,
|
||||
unit
|
||||
|
||||
unit,
|
||||
explanation: {
|
||||
plafondActif: assiette.nodeValue > plafond.nodeValue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -601,53 +600,6 @@ export let mecanismMin = (recurse, k, v) => {
|
|||
}
|
||||
}
|
||||
|
||||
export let mecanismComplement = (recurse, k, v) => {
|
||||
if (v.composantes) {
|
||||
//mécanisme de composantes. Voir known-mecanisms.md/composantes
|
||||
return decompose(recurse, k, v)
|
||||
}
|
||||
|
||||
let objectShape = { cible: false, montant: false }
|
||||
let effect = ({ cible, montant }) => {
|
||||
let nulled = cible.nodeValue == null
|
||||
return nulled
|
||||
? null
|
||||
: subtract(montant.nodeValue, min(cible.nodeValue, montant.nodeValue))
|
||||
}
|
||||
let explanation = parseObject(recurse, objectShape, v)
|
||||
|
||||
return {
|
||||
evaluate: evaluateObject(objectShape, effect),
|
||||
explanation,
|
||||
type: 'numeric',
|
||||
category: 'mecanism',
|
||||
name: 'complément pour atteindre',
|
||||
// eslint-disable-next-line
|
||||
jsx: (nodeValue, explanation) => (
|
||||
<Node
|
||||
classes="mecanism list complement"
|
||||
name="complément"
|
||||
value={nodeValue}
|
||||
>
|
||||
<ul className="properties">
|
||||
<li key="cible">
|
||||
<span className="key">
|
||||
<Trans>cible</Trans>:{' '}
|
||||
</span>
|
||||
<span className="value">{makeJsx(explanation.cible)}</span>
|
||||
</li>
|
||||
<li key="mini">
|
||||
<span className="key">
|
||||
<Trans>montant à atteindre</Trans>:{' '}
|
||||
</span>
|
||||
<span className="value">{makeJsx(explanation.montant)}</span>
|
||||
</li>
|
||||
</ul>
|
||||
</Node>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export let mecanismSynchronisation = (recurse, k, v) => {
|
||||
let evaluate = (cache, situationGate, parsedRules, node) => {
|
||||
let APIExplanation = evaluateNode(
|
||||
|
|
|
@ -0,0 +1,150 @@
|
|||
import { convertToString, getYear } from 'Engine/date'
|
||||
import { evaluationError } from 'Engine/error'
|
||||
import { evaluateNode } from 'Engine/evaluation'
|
||||
import {
|
||||
createTemporalEvaluation,
|
||||
Evaluation,
|
||||
groupByYear,
|
||||
liftTemporal2,
|
||||
Temporal,
|
||||
temporalAverage,
|
||||
temporalCumul
|
||||
} from 'Engine/period'
|
||||
import { Unit } from 'Engine/units'
|
||||
import { DottedName } from 'Types/rule'
|
||||
import { coerceArray } from '../../utils'
|
||||
|
||||
export default function parse(parse, k, v) {
|
||||
const rule = parse(v.règle)
|
||||
if (!v['valeurs cumulées']) {
|
||||
throw new Error(
|
||||
'Il manque la clé `valeurs cumulées` dans le mécanisme régularisation'
|
||||
)
|
||||
}
|
||||
|
||||
const variables = coerceArray(v['valeurs cumulées']).map(parse) as Array<{
|
||||
dottedName: DottedName
|
||||
category: string
|
||||
name: 'string'
|
||||
}>
|
||||
if (variables.some(({ category }) => category !== 'reference')) {
|
||||
throw new Error(
|
||||
'Le mécanisme régularisation attend des noms de règles sous la clé `valeurs cumulées`'
|
||||
)
|
||||
}
|
||||
|
||||
return {
|
||||
evaluate,
|
||||
explanation: {
|
||||
rule,
|
||||
variables
|
||||
},
|
||||
category: 'mecanism',
|
||||
name: 'taux progressif',
|
||||
type: 'numeric',
|
||||
unit: rule.unit
|
||||
}
|
||||
}
|
||||
|
||||
function getMonthlyCumulatedValuesOverYear(
|
||||
year: number,
|
||||
variable: Temporal<Evaluation<number>>,
|
||||
unit: Unit
|
||||
): Temporal<Evaluation<number>> {
|
||||
const start = convertToString(new Date(year, 0, 1))
|
||||
const cumulatedPeriods = [...Array(12).keys()]
|
||||
.map(monthNumber => ({
|
||||
start,
|
||||
end: convertToString(new Date(year, monthNumber + 1, 0))
|
||||
}))
|
||||
.map(period => {
|
||||
const temporal = liftTemporal2(
|
||||
(filter, value) => filter && value,
|
||||
createTemporalEvaluation(true, period),
|
||||
variable
|
||||
)
|
||||
return {
|
||||
...period,
|
||||
value: temporalCumul(temporal, unit)
|
||||
}
|
||||
})
|
||||
|
||||
return cumulatedPeriods
|
||||
}
|
||||
|
||||
function evaluate(
|
||||
cache,
|
||||
situation,
|
||||
parsedRules,
|
||||
node: ReturnType<typeof parse>
|
||||
) {
|
||||
const evaluate = evaluateNode.bind(null, cache, situation, parsedRules)
|
||||
|
||||
function recalculWith(situationGate: (dottedName: DottedName) => any, node) {
|
||||
const newSituation = (dottedName: DottedName) =>
|
||||
situationGate(dottedName) ?? situation(dottedName)
|
||||
return evaluateNode({ _meta: cache._meta }, newSituation, parsedRules, node)
|
||||
}
|
||||
|
||||
function regulariseYear(temporalEvaluation: Temporal<Evaluation<number>>) {
|
||||
if (temporalEvaluation.filter(({ value }) => value !== false).length <= 1) {
|
||||
return temporalEvaluation
|
||||
}
|
||||
|
||||
const currentYear = getYear(temporalEvaluation[0].start as string)
|
||||
const cumulatedVariables = node.explanation.variables.reduce(
|
||||
(acc, parsedVariable) => {
|
||||
const evaluation = evaluate(parsedVariable)
|
||||
if (!evaluation.temporalValue) {
|
||||
evaluationError(
|
||||
cache._meta.contextRule,
|
||||
`Dans le mécanisme régularisation, la valeur annuelle ${parsedVariable.name} n'est pas une variables temporelle`
|
||||
)
|
||||
}
|
||||
return {
|
||||
...acc,
|
||||
[parsedVariable.dottedName]: getMonthlyCumulatedValuesOverYear(
|
||||
currentYear,
|
||||
evaluation.temporalValue,
|
||||
evaluation.unit
|
||||
)
|
||||
}
|
||||
},
|
||||
{}
|
||||
)
|
||||
|
||||
const cumulatedMonthlyEvaluations = [...Array(12).keys()].map(i => ({
|
||||
start: convertToString(new Date(currentYear, i, 1)),
|
||||
end: convertToString(new Date(currentYear, i + 1, 0)),
|
||||
value: recalculWith(
|
||||
dottedName => cumulatedVariables[dottedName]?.[i].value,
|
||||
node.explanation.rule
|
||||
).nodeValue
|
||||
}))
|
||||
const temporalRégularisée = cumulatedMonthlyEvaluations.map(
|
||||
(period, i) => ({
|
||||
...period,
|
||||
value: period.value - (cumulatedMonthlyEvaluations[i - 1]?.value ?? 0)
|
||||
})
|
||||
)
|
||||
|
||||
return temporalRégularisée as Temporal<Evaluation<number>>
|
||||
}
|
||||
|
||||
const evaluation = evaluate(node.explanation.rule)
|
||||
const temporalValue = evaluation.temporalValue
|
||||
const evaluationWithRegularisation = groupByYear(
|
||||
temporalValue as Temporal<Evaluation<number>>
|
||||
)
|
||||
.map(regulariseYear)
|
||||
.flat()
|
||||
|
||||
return {
|
||||
...node,
|
||||
temporalValue: evaluationWithRegularisation,
|
||||
explanation: evaluation,
|
||||
nodeValue: temporalAverage(temporalValue),
|
||||
missingVariables: evaluation.missingVariables,
|
||||
unit: evaluation.unit
|
||||
}
|
||||
}
|
|
@ -2,7 +2,7 @@ import { evaluateNode } from 'Engine/evaluation'
|
|||
import {
|
||||
createTemporalEvaluation,
|
||||
narrowTemporalValue,
|
||||
periodAverage
|
||||
temporalAverage
|
||||
} from 'Engine/period'
|
||||
import { Temporal } from './../period'
|
||||
|
||||
|
@ -19,35 +19,46 @@ function evaluate(
|
|||
parsedRules
|
||||
)
|
||||
|
||||
const start = node.period.start && evaluateAttribute(node.period.start)
|
||||
const end = node.period.end && evaluateAttribute(node.period.end)
|
||||
const explanation = evaluateAttribute(node.explanation)
|
||||
const start =
|
||||
node.explanation.period.start &&
|
||||
evaluateAttribute(node.explanation.period.start)
|
||||
const end =
|
||||
node.explanation.period.end &&
|
||||
evaluateAttribute(node.explanation.period.end)
|
||||
const value = evaluateAttribute(node.explanation.value)
|
||||
const period = {
|
||||
start: start?.nodeValue ?? null,
|
||||
end: end?.nodeValue ?? null
|
||||
}
|
||||
|
||||
const temporalValue = explanation.temporalValue
|
||||
? narrowTemporalValue(period, explanation.temporalValue)
|
||||
: createTemporalEvaluation(explanation.nodeValue, period)
|
||||
const temporalValue = value.temporalValue
|
||||
? narrowTemporalValue(period, value.temporalValue)
|
||||
: createTemporalEvaluation(value.nodeValue, period)
|
||||
// TODO explanation missingVariables / period missing variables
|
||||
|
||||
return {
|
||||
...node,
|
||||
nodeValue: periodAverage(temporalValue as Temporal<number>),
|
||||
nodeValue: temporalAverage(temporalValue as Temporal<number>, value.unit),
|
||||
temporalValue,
|
||||
period: { start, end },
|
||||
explanation
|
||||
explanation: {
|
||||
period: { start, end },
|
||||
value
|
||||
},
|
||||
unit: value.unit
|
||||
}
|
||||
}
|
||||
|
||||
export default function parseVariableTemporelle(parse, __, v: any) {
|
||||
const explanation = parse(v.explanation)
|
||||
return {
|
||||
evaluate,
|
||||
explanation: parse(v.explanation),
|
||||
period: {
|
||||
start: v.period.start && parse(v.period.start),
|
||||
end: v.period.end && parse(v.period.end)
|
||||
}
|
||||
explanation: {
|
||||
period: {
|
||||
start: v.period.start && parse(v.period.start),
|
||||
end: v.period.end && parse(v.period.end)
|
||||
},
|
||||
value: explanation
|
||||
},
|
||||
unit: explanation.unit
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,6 +9,7 @@ import durée from 'Engine/mecanisms/durée'
|
|||
import encadrement from 'Engine/mecanisms/encadrement'
|
||||
import grille from 'Engine/mecanisms/grille'
|
||||
import operation from 'Engine/mecanisms/operation'
|
||||
import régularisation from 'Engine/mecanisms/régularisation'
|
||||
import tauxProgressif from 'Engine/mecanisms/tauxProgressif'
|
||||
import variableTemporelle from 'Engine/mecanisms/variableTemporelle'
|
||||
import variations from 'Engine/mecanisms/variations'
|
||||
|
@ -183,6 +184,7 @@ const statelessParseFunction = {
|
|||
'une de ces conditions': mecanismOneOf,
|
||||
'toutes ces conditions': mecanismAllOf,
|
||||
somme: mecanismSum,
|
||||
régularisation,
|
||||
multiplication: mecanismProduct,
|
||||
produit: mecanismProduct,
|
||||
temporalValue: variableTemporelle,
|
||||
|
|
|
@ -1,8 +1,16 @@
|
|||
import { convertToDate, getRelativeDate } from 'Engine/date'
|
||||
import {
|
||||
convertToDate,
|
||||
getDifferenceInDays,
|
||||
getDifferenceInMonths,
|
||||
getDifferenceInYears,
|
||||
getRelativeDate,
|
||||
getYear
|
||||
} from 'Engine/date'
|
||||
import { Unit } from './units'
|
||||
|
||||
export type Period<Date> = {
|
||||
start: Date | null
|
||||
end: Date | null
|
||||
export type Period<T> = {
|
||||
start: T | null
|
||||
end: T | null
|
||||
}
|
||||
|
||||
export function parsePeriod<Date>(word: string, date: Date): Period<Date> {
|
||||
|
@ -38,7 +46,6 @@ export function parsePeriod<Date>(word: string, date: Date): Period<Date> {
|
|||
}
|
||||
}
|
||||
if (word === 'en') {
|
||||
console.log(word, date)
|
||||
return { start: null, end: null }
|
||||
}
|
||||
if (startWords.includes(word)) {
|
||||
|
@ -58,14 +65,14 @@ export function parsePeriod<Date>(word: string, date: Date): Period<Date> {
|
|||
|
||||
// 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)]
|
||||
type Evaluation<T> = T | false | null
|
||||
export type Evaluation<T> = T | false | null
|
||||
|
||||
type EvaluatedNode<T> = {
|
||||
export type EvaluatedNode<T> = {
|
||||
nodeValue: Evaluation<T>
|
||||
temporalValue?: Temporal<Evaluation<T>>
|
||||
}
|
||||
|
||||
type TemporalNode<T> = Temporal<{ nodeValue: Evaluation<T> }>
|
||||
export type TemporalNode<T> = Temporal<{ nodeValue: Evaluation<T> }>
|
||||
export type Temporal<T> = Array<Period<string> & { value: T }>
|
||||
|
||||
export function narrowTemporalValue<T>(
|
||||
|
@ -102,7 +109,7 @@ export function createTemporalEvaluation<T>(
|
|||
return temporalValue
|
||||
}
|
||||
|
||||
export function pure<T>(value: T): Temporal<T> {
|
||||
export function pureTemporal<T>(value: T): Temporal<T> {
|
||||
return [{ start: null, end: null, value }]
|
||||
}
|
||||
|
||||
|
@ -117,7 +124,7 @@ export function mapTemporal<T1, T2>(
|
|||
}))
|
||||
}
|
||||
|
||||
function liftTemporal2<T1, T2, T3>(
|
||||
export function liftTemporal2<T1, T2, T3>(
|
||||
fn: (value1: T1, value2: T2) => T3,
|
||||
temporalValue1: Temporal<T1>,
|
||||
temporalValue2: Temporal<T2>
|
||||
|
@ -133,14 +140,14 @@ export function concatTemporals<T, U>(
|
|||
): Temporal<Array<T>> {
|
||||
return temporalValues.reduce(
|
||||
(values, value) => liftTemporal2((a, b) => [...a, b], values, value),
|
||||
pure([]) as Temporal<Array<T>>
|
||||
pureTemporal([]) as Temporal<Array<T>>
|
||||
)
|
||||
}
|
||||
|
||||
export function liftTemporalNode<T>(node: EvaluatedNode<T>): TemporalNode<T> {
|
||||
const { temporalValue, ...baseNode } = node
|
||||
if (!temporalValue) {
|
||||
return pure(baseNode)
|
||||
return pureTemporal(baseNode)
|
||||
}
|
||||
return mapTemporal(
|
||||
nodeValue => ({
|
||||
|
@ -211,6 +218,84 @@ export function zipTemporals<T1, T2>(
|
|||
throw new EvalError('All case should have been covered')
|
||||
}
|
||||
|
||||
function beginningOfNextYear(date: string): string {
|
||||
return `01/01/${getYear(date) + 1}`
|
||||
}
|
||||
|
||||
function endsOfPreviousYear(date: string): string {
|
||||
return `31/12/${getYear(date) - 1}`
|
||||
}
|
||||
|
||||
function splitStartsAt<T>(
|
||||
fn: (date: string) => string,
|
||||
temporal: Temporal<T>
|
||||
): Temporal<T> {
|
||||
return temporal.reduce((acc, period) => {
|
||||
const { start, end } = period
|
||||
const newStart = start === null ? start : fn(start)
|
||||
if (compareEndDate(newStart, end) !== -1) {
|
||||
return [...acc, period]
|
||||
}
|
||||
console.assert(newStart !== null)
|
||||
return [
|
||||
...acc,
|
||||
{ ...period, end: getRelativeDate(newStart as string, -1) },
|
||||
{ ...period, start: newStart }
|
||||
]
|
||||
}, [] as Temporal<T>)
|
||||
}
|
||||
|
||||
function splitEndsAt<T>(
|
||||
fn: (date: string) => string,
|
||||
temporal: Temporal<T>
|
||||
): Temporal<T> {
|
||||
return temporal.reduce((acc, period) => {
|
||||
const { start, end } = period
|
||||
const newEnd = end === null ? end : fn(end)
|
||||
if (compareStartDate(start, newEnd) !== -1) {
|
||||
return [...acc, period]
|
||||
}
|
||||
console.assert(newEnd !== null)
|
||||
return [
|
||||
...acc,
|
||||
{ ...period, end: newEnd },
|
||||
{ ...period, start: getRelativeDate(newEnd as string, 1) }
|
||||
]
|
||||
}, [] as Temporal<T>)
|
||||
}
|
||||
|
||||
export function groupByYear<T>(temporalValue: Temporal<T>): Array<Temporal<T>> {
|
||||
return (
|
||||
// First step: split period by year if needed
|
||||
splitEndsAt(
|
||||
endsOfPreviousYear,
|
||||
splitStartsAt(beginningOfNextYear, temporalValue)
|
||||
)
|
||||
// Second step: group period by year
|
||||
.reduce((acc, period) => {
|
||||
const [currentTemporal, ...otherTemporal] = acc
|
||||
if (currentTemporal === undefined) {
|
||||
return [[period]]
|
||||
}
|
||||
const firstPeriod = currentTemporal[0]
|
||||
console.assert(
|
||||
firstPeriod !== undefined &&
|
||||
firstPeriod.end !== null &&
|
||||
period.start !== null,
|
||||
'invariant non verifié'
|
||||
)
|
||||
if (
|
||||
(firstPeriod.end as string).slice(-4) !==
|
||||
(period.start as string).slice(-4)
|
||||
) {
|
||||
return [[period], ...acc]
|
||||
}
|
||||
return [[...currentTemporal, period], ...otherTemporal]
|
||||
}, [] as Array<Temporal<T>>)
|
||||
.reverse()
|
||||
)
|
||||
}
|
||||
|
||||
function simplify<T>(temporalValue: Temporal<T>): Temporal<T> {
|
||||
return temporalValue
|
||||
}
|
||||
|
@ -247,8 +332,9 @@ function compareEndDate(
|
|||
return convertToDate(dateA) < convertToDate(dateB) ? -1 : 1
|
||||
}
|
||||
|
||||
export function periodAverage(
|
||||
temporalValue: Temporal<Evaluation<number>>
|
||||
export function temporalAverage(
|
||||
temporalValue: Temporal<Evaluation<number>>,
|
||||
unit?: Unit
|
||||
): Evaluation<number> {
|
||||
temporalValue = temporalValue.filter(({ value }) => value !== false)
|
||||
const first = temporalValue[0]
|
||||
|
@ -265,7 +351,7 @@ export function periodAverage(
|
|||
if (last.end != null) {
|
||||
return first.value
|
||||
}
|
||||
return first.value + last.value / 2
|
||||
return (first.value + last.value) / 2
|
||||
}
|
||||
|
||||
if (temporalValue.some(({ value }) => value == null)) {
|
||||
|
@ -273,11 +359,14 @@ export function periodAverage(
|
|||
}
|
||||
let totalWeight = 0
|
||||
const weights = temporalValue.map(({ start, end, value }) => {
|
||||
const day = 1000 * 60 * 60 * 24
|
||||
const weight =
|
||||
convertToDate(end as string).getTime() -
|
||||
convertToDate(start as string).getTime() +
|
||||
day
|
||||
let weight = 0
|
||||
if (unit?.denominators.includes('mois')) {
|
||||
weight = getDifferenceInMonths(start, end)
|
||||
} else if (unit?.denominators.includes('année')) {
|
||||
weight = getDifferenceInYears(start, end)
|
||||
} else {
|
||||
weight = getDifferenceInDays(start, end)
|
||||
}
|
||||
totalWeight += weight
|
||||
return (value as number) * weight
|
||||
})
|
||||
|
@ -286,3 +375,41 @@ export function periodAverage(
|
|||
0
|
||||
)
|
||||
}
|
||||
|
||||
export function temporalCumul(
|
||||
temporalValue: Temporal<Evaluation<number>>,
|
||||
unit: Unit
|
||||
): Evaluation<number> {
|
||||
temporalValue = temporalValue.filter(({ value }) => value !== false)
|
||||
const first = temporalValue[0]
|
||||
const last = temporalValue[temporalValue.length - 1]
|
||||
if (!temporalValue.length) {
|
||||
return false
|
||||
}
|
||||
|
||||
// La variable est définie sur un interval infini
|
||||
if (first.start == null || last.end == null) {
|
||||
if (first.start != null) {
|
||||
return !last.value ? last.value : last.value > 0 ? Infinity : -Infinity
|
||||
}
|
||||
if (last.end != null) {
|
||||
return !last.value ? last.value : last.value > 0 ? Infinity : -Infinity
|
||||
}
|
||||
return null
|
||||
}
|
||||
if (temporalValue.some(({ value }) => value == null)) {
|
||||
return null
|
||||
}
|
||||
|
||||
return temporalValue.reduce((acc, { start, end, value }) => {
|
||||
let weight = 1
|
||||
if (unit?.denominators.includes('mois')) {
|
||||
weight = getDifferenceInMonths(start, end)
|
||||
} else if (unit?.denominators.includes('année')) {
|
||||
weight = getDifferenceInYears(start, end)
|
||||
} else if (unit?.denominators.includes('jour')) {
|
||||
weight = getDifferenceInDays(start, end)
|
||||
}
|
||||
return value * weight + acc
|
||||
}, 0)
|
||||
}
|
||||
|
|
|
@ -0,0 +1,19 @@
|
|||
import { expect } from 'chai'
|
||||
import { getDifferenceInMonths } from '../source/engine/date'
|
||||
|
||||
describe('Date : getDifferenceInMonths', () => {
|
||||
it('should compute the difference for one full month', () => {
|
||||
expect(getDifferenceInMonths('01/01/2020', '31/01/2020')).to.equal(1)
|
||||
})
|
||||
it('should compute the difference for one month and one day', () => {
|
||||
expect(getDifferenceInMonths('01/01/2020', '01/02/2020')).to.equal(
|
||||
1 + 1 / 29
|
||||
)
|
||||
})
|
||||
it('should compute the difference for 2 days between months', () => {
|
||||
expect(getDifferenceInMonths('31/01/2020', '01/02/2020')).to.approximately(
|
||||
1 / 31 + 1 / 29,
|
||||
0.000000000001
|
||||
)
|
||||
})
|
||||
})
|
|
@ -0,0 +1,40 @@
|
|||
salaire:
|
||||
unité: €/mois
|
||||
formule:
|
||||
somme:
|
||||
- 3300 €/mois | du 01/01/2020 | au 29/02/2020
|
||||
- 3600 €/mois | du 01/03/2020 | au 31/12/2020
|
||||
|
||||
plafond sécurité sociale:
|
||||
unité: €/mois
|
||||
formule: 3500 €/mois | du 01/01/2020 | au 31/12/2020
|
||||
|
||||
retraite:
|
||||
formule:
|
||||
multiplication:
|
||||
assiette: salaire
|
||||
plafond: plafond sécurité sociale
|
||||
taux: 10%
|
||||
|
||||
retraite . avec régularisation:
|
||||
formule:
|
||||
régularisation:
|
||||
règle: retraite
|
||||
valeurs cumulées:
|
||||
- salaire
|
||||
- plafond sécurité sociale
|
||||
|
||||
régularisation . avant passage:
|
||||
formule: retraite . avec régularisation | du 01/01/2020 | au 29/02/2020
|
||||
exemples:
|
||||
- valeur attendue: 330
|
||||
|
||||
régularisation . test mois régularisés:
|
||||
formule: retraite . avec régularisation | du 01/03/2020 | au 30/06/2020
|
||||
exemples:
|
||||
- valeur attendue: 360
|
||||
|
||||
régularisation . test mois après régularisation:
|
||||
formule: retraite . avec régularisation | du 01/07/2020 | au 31/12/2020
|
||||
exemples:
|
||||
- valeur attendue: 350
|
|
@ -164,15 +164,15 @@ variable temporelle numérique . multiplication avec valeur au dessus du plafond
|
|||
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 * 6 [janvier-juin]
|
||||
# # + 214.839 [juillet]
|
||||
# # + 220 * 4 [aout-novembre]
|
||||
# # + 337.7 [décembre]
|
||||
# # /12 mois
|
||||
# - valeur attendue: 219.378
|
||||
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 * 6 [janvier-juin]
|
||||
# + 214.839 [juillet]
|
||||
# + 220 * 4 [aout-novembre]
|
||||
# + 337.7 [décembre]
|
||||
# /12 mois
|
||||
- valeur attendue: 219.378
|
||||
# test . proratisation du salaire avec entrée en cours de mois:
|
||||
# formule: salaire brut [avril 2019]
|
||||
# exemples:
|
||||
|
|
|
@ -2,6 +2,7 @@ import { expect } from 'chai'
|
|||
import {
|
||||
concatTemporals,
|
||||
createTemporalEvaluation,
|
||||
groupByYear,
|
||||
zipTemporals
|
||||
} from '../source/engine/period'
|
||||
|
||||
|
@ -69,3 +70,63 @@ describe('Periods : concat', () => {
|
|||
])
|
||||
})
|
||||
})
|
||||
|
||||
describe('Periods : groupByYear', () => {
|
||||
const invariants = temporalYear => {
|
||||
const startDate = temporalYear[0].start
|
||||
const endDate = temporalYear.slice(-1)[0].end
|
||||
expect(
|
||||
startDate === null || startDate.startsWith('01/01'),
|
||||
'starts at the beginning of a year'
|
||||
)
|
||||
expect(
|
||||
endDate === null || endDate.startsWith('31/12'),
|
||||
'stops at the end of a year'
|
||||
)
|
||||
}
|
||||
it('should handle constant value', () => {
|
||||
const value = createTemporalEvaluation(10)
|
||||
expect(groupByYear(value)).to.deep.equal([value])
|
||||
})
|
||||
it('should handle changing value', () => {
|
||||
const value = createTemporalEvaluation(10, {
|
||||
start: '06/06/2020',
|
||||
end: '20/12/2020'
|
||||
})
|
||||
const result = groupByYear(value)
|
||||
expect(result).to.have.length(3)
|
||||
result.forEach(invariants)
|
||||
})
|
||||
it('should handle changing value over several years', () => {
|
||||
const value = createTemporalEvaluation(10, {
|
||||
start: '06/06/2020',
|
||||
end: '20/12/2022'
|
||||
})
|
||||
const result = groupByYear(value)
|
||||
expect(result).to.have.length(5)
|
||||
result.forEach(invariants)
|
||||
})
|
||||
it('should handle complex case', () => {
|
||||
const result = groupByYear(
|
||||
concatTemporals([
|
||||
createTemporalEvaluation(1, {
|
||||
start: '06/06/2020',
|
||||
end: '20/12/2022'
|
||||
}),
|
||||
createTemporalEvaluation(2, {
|
||||
start: '01/01/1991',
|
||||
end: '20/12/1992'
|
||||
}),
|
||||
createTemporalEvaluation(3, {
|
||||
start: '31/01/1990',
|
||||
end: '20/12/2021'
|
||||
}),
|
||||
createTemporalEvaluation(4, {
|
||||
start: '31/12/2020',
|
||||
end: '01/01/2021'
|
||||
})
|
||||
])
|
||||
)
|
||||
result.forEach(invariants)
|
||||
})
|
||||
})
|
||||
|
|
Loading…
Reference in New Issue