diff --git a/publicode/rules/salarié.yaml b/publicode/rules/salarié.yaml
index 7138a0e3e..c8685d5f9 100644
--- a/publicode/rules/salarié.yaml
+++ b/publicode/rules/salarié.yaml
@@ -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:
diff --git a/source/engine/date.ts b/source/engine/date.ts
index 2d2a8260a..04bf8b824 100644
--- a/source/engine/date.ts
+++ b/source/engine/date.ts
@@ -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
+}
diff --git a/source/engine/evaluation.tsx b/source/engine/evaluation.tsx
index 4ef7bfc14..216a264fc 100644
--- a/source/engine/evaluation.tsx
+++ b/source/engine/evaluation.tsx
@@ -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
+ }
}
diff --git a/source/engine/mecanisms.js b/source/engine/mecanisms.js
index 1e915cdcd..337e8cb31 100644
--- a/source/engine/mecanisms.js
+++ b/source/engine/mecanisms.js
@@ -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) => (
-
-
- -
-
- cible:{' '}
-
- {makeJsx(explanation.cible)}
-
- -
-
- montant à atteindre:{' '}
-
- {makeJsx(explanation.montant)}
-
-
-
- )
- }
-}
-
export let mecanismSynchronisation = (recurse, k, v) => {
let evaluate = (cache, situationGate, parsedRules, node) => {
let APIExplanation = evaluateNode(
diff --git a/source/engine/mecanisms/régularisation.ts b/source/engine/mecanisms/régularisation.ts
new file mode 100644
index 000000000..a1fda1450
--- /dev/null
+++ b/source/engine/mecanisms/régularisation.ts
@@ -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>,
+ unit: Unit
+): Temporal> {
+ 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
+) {
+ 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>) {
+ 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>
+ }
+
+ const evaluation = evaluate(node.explanation.rule)
+ const temporalValue = evaluation.temporalValue
+ const evaluationWithRegularisation = groupByYear(
+ temporalValue as Temporal>
+ )
+ .map(regulariseYear)
+ .flat()
+
+ return {
+ ...node,
+ temporalValue: evaluationWithRegularisation,
+ explanation: evaluation,
+ nodeValue: temporalAverage(temporalValue),
+ missingVariables: evaluation.missingVariables,
+ unit: evaluation.unit
+ }
+}
diff --git a/source/engine/mecanisms/variableTemporelle.ts b/source/engine/mecanisms/variableTemporelle.ts
index 90c9eb748..7b540fc3e 100644
--- a/source/engine/mecanisms/variableTemporelle.ts
+++ b/source/engine/mecanisms/variableTemporelle.ts
@@ -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),
+ nodeValue: temporalAverage(temporalValue as Temporal, 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
}
}
diff --git a/source/engine/parse.tsx b/source/engine/parse.tsx
index e82875715..0fe14112d 100644
--- a/source/engine/parse.tsx
+++ b/source/engine/parse.tsx
@@ -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,
diff --git a/source/engine/period.ts b/source/engine/period.ts
index 0a6771add..4b2135d9c 100644
--- a/source/engine/period.ts
+++ b/source/engine/period.ts
@@ -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 = {
- start: Date | null
- end: Date | null
+export type Period = {
+ start: T | null
+ end: T | null
}
export function parsePeriod(word: string, date: Date): Period {
@@ -38,7 +46,6 @@ export function parsePeriod(word: string, date: Date): Period {
}
}
if (word === 'en') {
- console.log(word, date)
return { start: null, end: null }
}
if (startWords.includes(word)) {
@@ -58,14 +65,14 @@ export function parsePeriod(word: string, date: Date): Period {
// 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 | false | null
+export type Evaluation = T | false | null
-type EvaluatedNode = {
+export type EvaluatedNode = {
nodeValue: Evaluation
temporalValue?: Temporal>
}
-type TemporalNode = Temporal<{ nodeValue: Evaluation }>
+export type TemporalNode = Temporal<{ nodeValue: Evaluation }>
export type Temporal = Array & { value: T }>
export function narrowTemporalValue(
@@ -102,7 +109,7 @@ export function createTemporalEvaluation(
return temporalValue
}
-export function pure(value: T): Temporal {
+export function pureTemporal(value: T): Temporal {
return [{ start: null, end: null, value }]
}
@@ -117,7 +124,7 @@ export function mapTemporal(
}))
}
-function liftTemporal2(
+export function liftTemporal2(
fn: (value1: T1, value2: T2) => T3,
temporalValue1: Temporal,
temporalValue2: Temporal
@@ -133,14 +140,14 @@ export function concatTemporals(
): Temporal> {
return temporalValues.reduce(
(values, value) => liftTemporal2((a, b) => [...a, b], values, value),
- pure([]) as Temporal>
+ pureTemporal([]) as Temporal>
)
}
export function liftTemporalNode(node: EvaluatedNode): TemporalNode {
const { temporalValue, ...baseNode } = node
if (!temporalValue) {
- return pure(baseNode)
+ return pureTemporal(baseNode)
}
return mapTemporal(
nodeValue => ({
@@ -211,6 +218,84 @@ export function zipTemporals(
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(
+ fn: (date: string) => string,
+ temporal: Temporal
+): Temporal {
+ 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)
+}
+
+function splitEndsAt(
+ fn: (date: string) => string,
+ temporal: Temporal
+): Temporal {
+ 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)
+}
+
+export function groupByYear(temporalValue: Temporal): Array> {
+ 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>)
+ .reverse()
+ )
+}
+
function simplify(temporalValue: Temporal): Temporal {
return temporalValue
}
@@ -247,8 +332,9 @@ function compareEndDate(
return convertToDate(dateA) < convertToDate(dateB) ? -1 : 1
}
-export function periodAverage(
- temporalValue: Temporal>
+export function temporalAverage(
+ temporalValue: Temporal>,
+ unit?: Unit
): Evaluation {
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>,
+ unit: Unit
+): Evaluation {
+ 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)
+}
diff --git a/test/date.test.js b/test/date.test.js
new file mode 100644
index 000000000..b8c6779ee
--- /dev/null
+++ b/test/date.test.js
@@ -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
+ )
+ })
+})
diff --git a/test/mécanismes/régularisation.yaml b/test/mécanismes/régularisation.yaml
new file mode 100644
index 000000000..afb32a89e
--- /dev/null
+++ b/test/mécanismes/régularisation.yaml
@@ -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
diff --git a/test/mécanismes/variable-temporelle.yaml b/test/mécanismes/variable-temporelle.yaml
index 6e5d6bc69..85672060b 100644
--- a/test/mécanismes/variable-temporelle.yaml
+++ b/test/mécanismes/variable-temporelle.yaml
@@ -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:
diff --git a/test/period.test.js b/test/period.test.js
index 014231d30..416c82aec 100644
--- a/test/period.test.js
+++ b/test/period.test.js
@@ -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)
+ })
+})