⚙️ Ajoute le mécanisme régularisation

- améliore la gestion des unités pour les variables temporelles
pull/866/head
Johan Girod 2020-02-24 18:34:38 +01:00
parent 0a5aba9078
commit 665943288a
12 changed files with 587 additions and 189 deletions

View File

@ -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:

View File

@ -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
}

View File

@ -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
}
}

View File

@ -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(

View File

@ -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
}
}

View File

@ -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
}
}

View File

@ -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,

View File

@ -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)
}

19
test/date.test.js Normal file
View File

@ -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
)
})
})

View File

@ -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

View File

@ -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:

View File

@ -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)
})
})