Merge pull request #866 from betagouv/engine-next
RFC : Gestion des variables temporelles et de la proratisationpull/932/head
commit
140913f973
|
@ -1,5 +1,6 @@
|
|||
const fr = Cypress.env('language') === 'fr'
|
||||
const inputSelector = 'input.currencyInput__input:not([name$="charges"])'
|
||||
const chargeInputSelector = 'input.currencyInput__input[name$="charges"]'
|
||||
describe('Simulateurs', function() {
|
||||
if (!fr) {
|
||||
return
|
||||
|
@ -15,7 +16,7 @@ describe('Simulateurs', function() {
|
|||
it('should display a result when entering a value in any of the currency input', () => {
|
||||
cy.contains('€ / an').click()
|
||||
if (['indépendant', 'assimilé-salarié'].includes(simulateur)) {
|
||||
cy.get('input.currencyInput__input[name$="charges"]').type(1000)
|
||||
cy.get(chargeInputSelector).type(1000)
|
||||
}
|
||||
cy.get(inputSelector).each((testedInput, i) => {
|
||||
cy.wrap(testedInput).type('{selectall}60000')
|
||||
|
@ -37,12 +38,21 @@ describe('Simulateurs', function() {
|
|||
cy.get(inputSelector)
|
||||
.first()
|
||||
.type('{selectall}12000')
|
||||
if (['indépendant', 'assimilé-salarié'].includes(simulateur)) {
|
||||
cy.get(chargeInputSelector).type('{selectall}6000')
|
||||
}
|
||||
cy.wait(600)
|
||||
cy.contains('€ / mois').click()
|
||||
cy.get(inputSelector)
|
||||
.first()
|
||||
.invoke('val')
|
||||
.should('match', /1[\s]000/)
|
||||
if (['indépendant', 'assimilé-salarié'].includes(simulateur)) {
|
||||
cy.get(chargeInputSelector)
|
||||
.first()
|
||||
.invoke('val')
|
||||
.should('be', '500')
|
||||
}
|
||||
})
|
||||
|
||||
it('should allow to navigate to a documentation page', function() {
|
||||
|
|
|
@ -426,6 +426,15 @@ dirigeant . indépendant . revenu net de cotisations:
|
|||
|
||||
dirigeant . indépendant . revenu professionnel:
|
||||
unité par défaut: €/an
|
||||
titre: revenu professionnel (net imposable)
|
||||
description: |
|
||||
C'est le revenu net de cotisations déductibles du travailleur indépendant, qui sert de base au calcul des cotisations et de l'impôt pour les indépendants.
|
||||
|
||||
Attention, **notre calcul est fait au régime de croisière**:
|
||||
l'indépendant qui se lance paiera pendant ses 2 premières années un forfait relativement réduit de cotisations sociales. Il devra ensuite régulariser cette situation par rapport au revenu qu'il a vraiment perçu.
|
||||
|
||||
Il faut donc voir ce calcul comme *le montant qui devra de toute façon être payé* à court terme après 2 ans d'exercice.
|
||||
|
||||
formule:
|
||||
inversion numérique:
|
||||
avec:
|
||||
|
@ -437,7 +446,6 @@ dirigeant . indépendant . revenu professionnel:
|
|||
valeurs négatives possibles: oui
|
||||
|
||||
dirigeant . indépendant . assiette des cotisations:
|
||||
unité par défaut: €/an
|
||||
formule:
|
||||
encadrement:
|
||||
plancher: 0
|
||||
|
@ -1032,7 +1040,6 @@ dirigeant . indépendant . cotisations et contributions . cotisations . allocati
|
|||
taux: 3.1%
|
||||
|
||||
dirigeant . indépendant . cotisations et contributions . exonérations:
|
||||
période: flexible
|
||||
formule:
|
||||
somme:
|
||||
- ZFU
|
||||
|
|
|
@ -12,7 +12,6 @@ impôt:
|
|||
produit:
|
||||
assiette: revenu imposable
|
||||
taux: taux du prélèvement à la source
|
||||
- sinon: 0
|
||||
- CEHR
|
||||
- dirigeant . auto-entrepreneur . impôt . versement libératoire . montant
|
||||
|
||||
|
@ -358,6 +357,7 @@ impôt . taux personnalisé:
|
|||
|
||||
revenus net de cotisations:
|
||||
résumé: Avant impôt
|
||||
unité: €/an
|
||||
question: Quel revenu avant impôt voulez-vous toucher ?
|
||||
description: |
|
||||
Il s'agit du revenu net de cotisations et de charges, avant le paiement de l'impôt sur le revenu.
|
||||
|
|
|
@ -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:
|
||||
|
@ -2903,13 +2898,12 @@ contrat salarié . taxe sur les salaires:
|
|||
entreprise . effectif: 1
|
||||
valeur attendue: 0
|
||||
- nom: association non lucrative
|
||||
unités par défaut: [€/mois]
|
||||
situation:
|
||||
entreprise . association non lucrative: oui
|
||||
rémunération . brut de base: 2300
|
||||
entreprise . effectif: 10
|
||||
complémentaire santé . forfait: 0
|
||||
valeur attendue: 48.10
|
||||
valeur attendue: 577
|
||||
|
||||
références:
|
||||
fiche: https://www.service-public.fr/professionnels-entreprises/vosdroits/F22576
|
||||
|
|
|
@ -103,13 +103,13 @@ export default function PaySlip() {
|
|||
<Value
|
||||
nilValueSymbol="—"
|
||||
{...getRule('contrat salarié . cotisations . patronales')}
|
||||
unit="€"
|
||||
printedUnit="€"
|
||||
className="payslip__total"
|
||||
/>
|
||||
<Value
|
||||
nilValueSymbol="—"
|
||||
{...getRule('contrat salarié . cotisations . salariales')}
|
||||
unit="€"
|
||||
printedUnit="€"
|
||||
className="payslip__total"
|
||||
/>
|
||||
{/* Salaire chargé */}
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
import Value from 'Components/Value'
|
||||
import React from 'react'
|
||||
import { Trans } from 'react-i18next'
|
||||
import { useSelector } from 'react-redux'
|
||||
import { defaultUnitSelector } from 'Selectors/analyseSelectors'
|
||||
import RuleLink from './RuleLink'
|
||||
|
||||
export let SalaireBrutSection = ({ getRule }) => {
|
||||
|
@ -43,12 +45,20 @@ export let SalaireBrutSection = ({ getRule }) => {
|
|||
)
|
||||
}
|
||||
|
||||
export let Line = ({ rule, ...props }) => (
|
||||
<>
|
||||
<RuleLink {...rule} />
|
||||
<Value {...rule} nilValueSymbol="—" unit="€" {...props} />
|
||||
</>
|
||||
)
|
||||
export let Line = ({ rule, ...props }) => {
|
||||
const defaultUnit = useSelector(defaultUnitSelector)
|
||||
return (
|
||||
<>
|
||||
<RuleLink {...rule} />
|
||||
<Value
|
||||
{...rule}
|
||||
nilValueSymbol="—"
|
||||
defaultUnit={defaultUnit}
|
||||
{...props}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export let SalaireNetSection = ({ getRule }) => {
|
||||
let avantagesEnNature = getRule(
|
||||
|
|
|
@ -3,13 +3,14 @@ import { parseUnit, serializeUnit } from 'Engine/units'
|
|||
import React from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useDispatch, useSelector } from 'react-redux'
|
||||
import { defaultUnitsSelector } from 'Selectors/analyseSelectors'
|
||||
import { defaultUnitSelector } from 'Selectors/analyseSelectors'
|
||||
import './PeriodSwitch.css'
|
||||
|
||||
export default function PeriodSwitch() {
|
||||
const dispatch = useDispatch()
|
||||
const currentUnit = useSelector(defaultUnitsSelector)[0]
|
||||
const language = useTranslation().i18n.language
|
||||
const currentUnit = useSelector(defaultUnitSelector)
|
||||
|
||||
let units = ['€/mois', '€/an']
|
||||
return (
|
||||
<span id="PeriodSwitch">
|
||||
|
|
|
@ -10,7 +10,7 @@ import { useSelector } from 'react-redux'
|
|||
import { RootState } from 'Reducers/rootReducer'
|
||||
import {
|
||||
analysisWithDefaultsSelector,
|
||||
defaultUnitsSelector
|
||||
defaultUnitSelector
|
||||
} from 'Selectors/analyseSelectors'
|
||||
import * as Animate from 'Ui/animate'
|
||||
|
||||
|
@ -51,9 +51,9 @@ export default function SalaryExplanation() {
|
|||
{emoji('📊')} <Trans>Voir la répartition des cotisations</Trans>
|
||||
</button>
|
||||
</div>
|
||||
<PaySlipSection />
|
||||
<div ref={distributionRef}>
|
||||
<DistributionSection />
|
||||
<PaySlipSection />
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
@ -117,7 +117,7 @@ function RevenueRepatitionSection() {
|
|||
}
|
||||
|
||||
function PaySlipSection() {
|
||||
const unit = useSelector(defaultUnitsSelector)[0]
|
||||
const unit = useSelector(defaultUnitSelector)
|
||||
return (
|
||||
<section>
|
||||
<h2>
|
||||
|
|
|
@ -26,12 +26,16 @@ export type ValueProps = Partial<
|
|||
children: number
|
||||
negative: boolean
|
||||
customCSS: string
|
||||
defaultUnit?: string
|
||||
printedUnit?: string
|
||||
}
|
||||
>
|
||||
|
||||
export default function Value({
|
||||
nodeValue: value,
|
||||
unit,
|
||||
defaultUnit,
|
||||
printedUnit,
|
||||
nilValueSymbol,
|
||||
maximumFractionDigits,
|
||||
minimumFractionDigits,
|
||||
|
|
|
@ -2,7 +2,7 @@ import { toPairs } from 'ramda'
|
|||
import React, { useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useSelector } from 'react-redux'
|
||||
import { defaultUnitsSelector } from 'Selectors/analyseSelectors'
|
||||
import { defaultUnitSelector } from 'Selectors/analyseSelectors'
|
||||
import { convertUnit, parseUnit, Unit } from '../../engine/units'
|
||||
|
||||
type InputSuggestionsProps = {
|
||||
|
@ -20,7 +20,7 @@ export default function InputSuggestions({
|
|||
}: InputSuggestionsProps) {
|
||||
const [suggestion, setSuggestion] = useState<number>()
|
||||
const { t } = useTranslation()
|
||||
const defaultUnit = parseUnit(useSelector(defaultUnitsSelector)[0] ?? '')
|
||||
const defaultUnit = parseUnit(useSelector(defaultUnitSelector) ?? '')
|
||||
if (!suggestions) return null
|
||||
|
||||
return (
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
situation:
|
||||
dirigeant: artiste-auteur
|
||||
unités par défaut: [€/an]
|
||||
unité par défaut: €/an
|
||||
objectifs:
|
||||
- artiste-auteur . cotisations
|
||||
|
|
|
@ -34,7 +34,7 @@ questions:
|
|||
- contrat salarié . complémentaire santé . part employeur
|
||||
- contrat salarié . régime des impatriés
|
||||
|
||||
unités par défaut: [€/an]
|
||||
unité par défaut: €/an
|
||||
situation:
|
||||
dirigeant: 'assimilé salarié'
|
||||
contrat salarié . ATMP . taux réduit: oui
|
||||
|
|
|
@ -16,6 +16,6 @@ questions:
|
|||
liste noire:
|
||||
- entreprise . charges
|
||||
|
||||
unités par défaut: [€/an]
|
||||
unité par défaut: €/an
|
||||
situation:
|
||||
dirigeant: 'auto-entrepreneur'
|
||||
|
|
|
@ -29,6 +29,6 @@ questions:
|
|||
- dirigeant . indépendant . cotisations et contributions . exonérations . âge
|
||||
- dirigeant . indépendant . cotisations et contributions . exonérations . invalidité
|
||||
|
||||
unités par défaut: [€/an]
|
||||
unité par défaut: €/an
|
||||
situation:
|
||||
dirigeant: 'indépendant'
|
||||
|
|
|
@ -18,7 +18,7 @@ questions:
|
|||
- entreprise . catégorie d'activité . restauration ou hébergement
|
||||
- entreprise . catégorie d'activité . libérale règlementée
|
||||
|
||||
unités par défaut: [€/an]
|
||||
unité par défaut: €/an
|
||||
branches:
|
||||
- nom: Assimilé salarié
|
||||
situation:
|
||||
|
|
|
@ -27,6 +27,6 @@ questions:
|
|||
- contrat salarié . statut JEI
|
||||
- contrat salarié . complémentaire santé . part employeur
|
||||
- contrat salarié . régime des impatriés
|
||||
unités par défaut: [€/mois]
|
||||
unité par défaut: €/mois
|
||||
situation:
|
||||
dirigeant: non
|
||||
|
|
|
@ -25,7 +25,7 @@ export default function AnimatedTargetValue({
|
|||
|
||||
// We don't want to show the animated if the difference comes from a change in the unit
|
||||
const currentUnit = useSelector(
|
||||
(state: RootState) => state?.simulation?.defaultUnits[0]
|
||||
(state: RootState) => state?.simulation?.defaultUnit
|
||||
)
|
||||
const previousUnit = useRef(currentUnit)
|
||||
|
||||
|
|
|
@ -18,22 +18,53 @@ export function normalizeDate(
|
|||
return `${pad(day)}/${pad(month)}/${pad(year)}`
|
||||
}
|
||||
|
||||
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)
|
||||
if (!dateStrings.some(dateString => dateString.match(dateRegexp))) {
|
||||
return values
|
||||
}
|
||||
dateStrings.forEach(dateString => {
|
||||
if (!dateString.match(dateRegexp)) {
|
||||
throw new TypeError(
|
||||
`'${dateString}' n'est pas une date valide (format attendu: mm/aaaa ou jj/mm/aaaa)`
|
||||
)
|
||||
}
|
||||
})
|
||||
return dateStrings.map(convertToDate)
|
||||
|
||||
export function convertToString(date: Date): string {
|
||||
return normalizeDate(date.getFullYear(), date.getMonth() + 1, date.getDate())
|
||||
}
|
||||
|
||||
export function getRelativeDate(date: string, dayDifferential: number): string {
|
||||
const relativeDate = new Date(convertToDate(date))
|
||||
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,14 +1,16 @@
|
|||
import { coerceArray } from '../utils'
|
||||
|
||||
export class EngineError extends Error {}
|
||||
export function syntaxError(
|
||||
rules: string[] | string,
|
||||
message: string,
|
||||
originalError?: Error
|
||||
) {
|
||||
throw new Error(
|
||||
throw new EngineError(
|
||||
`\n[ Erreur syntaxique ]
|
||||
➡️ Dans la règle \`${coerceArray(rules).slice(-1)[0]}\`
|
||||
✖️ ${message}
|
||||
${originalError && originalError.message}
|
||||
➡️ Dans la règle \`${coerceArray(rules).slice(-1)[0]}\`
|
||||
✖️ ${message}
|
||||
${originalError ? originalError.message : ''}
|
||||
`
|
||||
)
|
||||
}
|
||||
|
@ -18,11 +20,11 @@ export function evaluationError(
|
|||
message: string,
|
||||
originalError?: Error
|
||||
) {
|
||||
throw new Error(
|
||||
throw new EngineError(
|
||||
`\n[ Erreur d'évaluation ]
|
||||
➡️ Dans la règle \`${coerceArray(rules).slice(-1)[0]}\`
|
||||
✖️ ${message}
|
||||
${originalError && originalError.message}
|
||||
➡️ Dans la règle \`${coerceArray(rules).slice(-1)[0]}\`
|
||||
✖️ ${message}
|
||||
${originalError ? originalError.message : ''}
|
||||
`
|
||||
)
|
||||
}
|
||||
|
@ -34,9 +36,9 @@ export function typeWarning(
|
|||
) {
|
||||
console.warn(
|
||||
`\n[ Erreur de type ]
|
||||
➡️ Dans la règle \`${coerceArray(rules).slice(-1)[0]}\`
|
||||
✖️ ${message}
|
||||
${originalError && originalError.message}
|
||||
➡️ Dans la règle \`${coerceArray(rules).slice(-1)[0]}\`
|
||||
✖️ ${message}
|
||||
${originalError ? originalError.message : ''}
|
||||
`
|
||||
)
|
||||
}
|
||||
|
@ -48,9 +50,9 @@ export function warning(
|
|||
) {
|
||||
console.warn(
|
||||
`\n[ Avertissement ]
|
||||
➡️ Dans la règle \`${coerceArray(rules).slice(-1)[0]}\`
|
||||
⚠️ ${message}
|
||||
💡${solution}
|
||||
➡️ Dans la règle \`${coerceArray(rules).slice(-1)[0]}\`
|
||||
⚠️ ${message}
|
||||
💡 ${solution}
|
||||
`
|
||||
)
|
||||
}
|
||||
|
|
|
@ -102,6 +102,7 @@ export default (cache, situationGate, parsedRules, node) => {
|
|||
node.defaultUnit ||
|
||||
evaluatedFormula.unit
|
||||
|
||||
const temporalValue = evaluatedFormula.temporalValue
|
||||
if (unit) {
|
||||
try {
|
||||
nodeValue = convertNodeToUnit(unit, evaluatedFormula).nodeValue
|
||||
|
@ -121,6 +122,7 @@ export default (cache, situationGate, parsedRules, node) => {
|
|||
...(node.formule && { formule: evaluatedFormula }),
|
||||
nodeValue,
|
||||
unit,
|
||||
temporalValue,
|
||||
isApplicable,
|
||||
missingVariables
|
||||
}
|
||||
|
|
|
@ -1,170 +0,0 @@
|
|||
import {
|
||||
add,
|
||||
any,
|
||||
equals,
|
||||
evolve,
|
||||
filter,
|
||||
fromPairs,
|
||||
is,
|
||||
keys,
|
||||
map,
|
||||
mergeWith,
|
||||
reduce,
|
||||
values
|
||||
} from 'ramda'
|
||||
import React from 'react'
|
||||
import { typeWarning } from './error'
|
||||
import { convertNodeToUnit, simplifyNodeUnit } from './nodeUnits'
|
||||
|
||||
export let makeJsx = node =>
|
||||
typeof node.jsx == 'function'
|
||||
? node.jsx(node.nodeValue, node.explanation, node.lazyEval, node.unit)
|
||||
: node.jsx
|
||||
|
||||
export let collectNodeMissing = node => node.missingVariables || {}
|
||||
|
||||
export let bonus = (missings, hasCondition = true) =>
|
||||
hasCondition ? map(x => x + 0.0001, missings || {}) : missings
|
||||
export let mergeAllMissing = missings =>
|
||||
reduce(mergeWith(add), {}, map(collectNodeMissing, missings))
|
||||
export let mergeMissing = (left, right) =>
|
||||
mergeWith(add, left || {}, right || {})
|
||||
|
||||
export let evaluateNode = (cache, situationGate, parsedRules, node) => {
|
||||
let evaluatedNode = node.evaluate
|
||||
? node.evaluate(cache, situationGate, parsedRules, node)
|
||||
: node
|
||||
if (typeof evaluatedNode.nodeValue !== 'number') {
|
||||
return evaluatedNode
|
||||
}
|
||||
evaluatedNode = node.unité
|
||||
? convertNodeToUnit(node.unit, evaluatedNode)
|
||||
: simplifyNodeUnit(evaluatedNode)
|
||||
return evaluatedNode
|
||||
}
|
||||
const sameUnitValues = (explanation, contextRule, mecanismName) => {
|
||||
const firstNodeWithUnit = explanation.find(node => !!node.unit)
|
||||
if (!firstNodeWithUnit) {
|
||||
return [undefined, explanation.map(({ nodeValue }) => nodeValue)]
|
||||
}
|
||||
const values = explanation.map(node => {
|
||||
try {
|
||||
return convertNodeToUnit(firstNodeWithUnit?.unit, node).nodeValue
|
||||
} catch (e) {
|
||||
typeWarning(
|
||||
contextRule,
|
||||
`Dans le mécanisme ${mecanismName}, les unités des éléments suivants sont incompatibles entre elles : \n\t\t${node?.name ||
|
||||
node?.rawNode}\n\t\t${firstNodeWithUnit?.name ||
|
||||
firstNodeWithUnit?.rawNode}'`,
|
||||
e
|
||||
)
|
||||
return node.nodeValue
|
||||
}
|
||||
})
|
||||
return [firstNodeWithUnit.unit, values]
|
||||
}
|
||||
|
||||
export let evaluateArray = (reducer, start) => (
|
||||
cache,
|
||||
situationGate,
|
||||
parsedRules,
|
||||
node
|
||||
) => {
|
||||
let evaluateOne = child =>
|
||||
evaluateNode(cache, situationGate, parsedRules, child),
|
||||
explanation = map(evaluateOne, node.explanation),
|
||||
[unit, values] = sameUnitValues(
|
||||
explanation,
|
||||
cache._meta.contextRule,
|
||||
node.name
|
||||
),
|
||||
nodeValue = values.some(value => value === null)
|
||||
? null
|
||||
: reduce(reducer, start, values),
|
||||
missingVariables =
|
||||
node.nodeValue == null ? mergeAllMissing(explanation) : {}
|
||||
return {
|
||||
...node,
|
||||
nodeValue,
|
||||
explanation,
|
||||
missingVariables,
|
||||
unit
|
||||
}
|
||||
}
|
||||
|
||||
export let 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 }
|
||||
}
|
||||
|
||||
export let defaultNode = nodeValue => ({
|
||||
nodeValue,
|
||||
// eslint-disable-next-line
|
||||
jsx: nodeValue => <span className="value">{nodeValue}</span>,
|
||||
isDefault: true
|
||||
})
|
||||
|
||||
export let parseObject = (recurse, objectShape, value) => {
|
||||
let recurseOne = key => defaultValue => {
|
||||
if (value[key] == null && !defaultValue)
|
||||
throw new Error(
|
||||
`Il manque une clé '${key}' dans ${JSON.stringify(value)} `
|
||||
)
|
||||
return value[key] != null ? recurse(value[key]) : defaultValue
|
||||
}
|
||||
let transforms = fromPairs(map(k => [k, recurseOne(k)], keys(objectShape)))
|
||||
return evolve(transforms, objectShape)
|
||||
}
|
||||
|
||||
export let evaluateObject = (objectShape, effect) => (
|
||||
cache,
|
||||
situationGate,
|
||||
parsedRules,
|
||||
node
|
||||
) => {
|
||||
let evaluateOne = child =>
|
||||
evaluateNode(cache, situationGate, parsedRules, child)
|
||||
|
||||
let transforms = map(k => [k, evaluateOne], keys(objectShape)),
|
||||
automaticExplanation = evolve(fromPairs(transforms))(node.explanation)
|
||||
// the result of effect can either be just a nodeValue, or an object {additionalExplanation, nodeValue}. The latter is useful for a richer JSX visualisation of the mecanism : the view should not duplicate code to recompute intermediate values (e.g. for a marginal 'barème', the marginal 'tranche')
|
||||
let evaluated = effect(
|
||||
automaticExplanation,
|
||||
cache,
|
||||
situationGate,
|
||||
parsedRules
|
||||
),
|
||||
explanation = is(Object, evaluated)
|
||||
? { ...automaticExplanation, ...evaluated.additionalExplanation }
|
||||
: automaticExplanation,
|
||||
nodeValue = is(Object, evaluated) ? evaluated.nodeValue : evaluated,
|
||||
missingVariables = mergeAllMissing(values(explanation))
|
||||
return simplifyNodeUnit({
|
||||
...node,
|
||||
nodeValue,
|
||||
explanation,
|
||||
missingVariables,
|
||||
unit: explanation.unit
|
||||
})
|
||||
}
|
|
@ -0,0 +1,212 @@
|
|||
import {
|
||||
add,
|
||||
evolve,
|
||||
filter,
|
||||
fromPairs,
|
||||
keys,
|
||||
map,
|
||||
mergeWith,
|
||||
reduce
|
||||
} from 'ramda'
|
||||
import React from 'react'
|
||||
import { typeWarning } from './error'
|
||||
import { convertNodeToUnit, simplifyNodeUnit } from './nodeUnits'
|
||||
import {
|
||||
concatTemporals,
|
||||
EvaluatedNode,
|
||||
liftTemporalNode,
|
||||
mapTemporal,
|
||||
pureTemporal,
|
||||
Temporal,
|
||||
temporalAverage,
|
||||
zipTemporals
|
||||
} from './temporal'
|
||||
|
||||
export let makeJsx = node =>
|
||||
typeof node.jsx == 'function'
|
||||
? node.jsx(node.nodeValue, node.explanation, node.lazyEval, node.unit)
|
||||
: node.jsx
|
||||
|
||||
export let collectNodeMissing = node => node.missingVariables || {}
|
||||
|
||||
export let bonus = (missings, hasCondition = true) =>
|
||||
hasCondition ? map(x => x + 0.0001, missings || {}) : missings
|
||||
export let mergeAllMissing = missings =>
|
||||
reduce(mergeWith(add), {}, map(collectNodeMissing, missings))
|
||||
export let mergeMissing = (left, right) =>
|
||||
mergeWith(add, left || {}, right || {})
|
||||
|
||||
export let evaluateNode = (cache, situationGate, parsedRules, node) => {
|
||||
let evaluatedNode = node.evaluate
|
||||
? node.evaluate(cache, situationGate, parsedRules, node)
|
||||
: node
|
||||
if (typeof evaluatedNode.nodeValue !== 'number') {
|
||||
return evaluatedNode
|
||||
}
|
||||
evaluatedNode = node.unité
|
||||
? convertNodeToUnit(node.unit, evaluatedNode)
|
||||
: simplifyNodeUnit(evaluatedNode)
|
||||
return evaluatedNode
|
||||
}
|
||||
|
||||
function convertNodesToSameUnit(nodes, contextRule, mecanismName) {
|
||||
const firstNodeWithUnit = nodes.find(node => !!node.unit)
|
||||
if (!firstNodeWithUnit) {
|
||||
return nodes
|
||||
}
|
||||
return nodes.map(node => {
|
||||
try {
|
||||
return convertNodeToUnit(firstNodeWithUnit.unit, node)
|
||||
} catch (e) {
|
||||
typeWarning(
|
||||
contextRule,
|
||||
`Dans le mécanisme ${mecanismName}, les unités des éléments suivants sont incompatibles entre elles : \n\t\t${node?.name ||
|
||||
node?.rawNode}\n\t\t${firstNodeWithUnit?.name ||
|
||||
firstNodeWithUnit?.rawNode}'`,
|
||||
e
|
||||
)
|
||||
return node
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export const evaluateArray = (reducer, start) => (
|
||||
cache,
|
||||
situationGate,
|
||||
parsedRules,
|
||||
node
|
||||
) => {
|
||||
const evaluate = evaluateNode.bind(null, cache, situationGate, parsedRules)
|
||||
const evaluatedNodes = convertNodesToSameUnit(
|
||||
node.explanation.map(evaluate),
|
||||
cache._meta.contextRule,
|
||||
node.name
|
||||
)
|
||||
|
||||
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,
|
||||
missingVariables: mergeAllMissing(evaluatedNodes),
|
||||
explanation: evaluatedNodes,
|
||||
unit: evaluatedNodes[0].unit
|
||||
}
|
||||
if (temporalValue.length === 1) {
|
||||
return {
|
||||
...baseEvaluation,
|
||||
nodeValue: temporalValue[0].value
|
||||
}
|
||||
}
|
||||
return {
|
||||
...baseEvaluation,
|
||||
temporalValue,
|
||||
nodeValue: temporalAverage(temporalValue)
|
||||
}
|
||||
}
|
||||
|
||||
export const evaluateArrayWithFilter = (evaluationFilter, reducer, start) => (
|
||||
cache,
|
||||
situationGate,
|
||||
parsedRules,
|
||||
node
|
||||
) => {
|
||||
return evaluateArray(reducer, start)(cache, situationGate, parsedRules, {
|
||||
...node,
|
||||
explanation: filter(evaluationFilter(situationGate), node.explanation)
|
||||
})
|
||||
}
|
||||
|
||||
export let defaultNode = nodeValue => ({
|
||||
nodeValue,
|
||||
// eslint-disable-next-line
|
||||
jsx: nodeValue => <span className="value">{nodeValue}</span>,
|
||||
isDefault: true
|
||||
})
|
||||
|
||||
export let parseObject = (recurse, objectShape, value) => {
|
||||
let recurseOne = key => defaultValue => {
|
||||
if (value[key] == null && !defaultValue)
|
||||
throw new Error(
|
||||
`Il manque une clé '${key}' dans ${JSON.stringify(value)} `
|
||||
)
|
||||
return value[key] != null ? recurse(value[key]) : defaultValue
|
||||
}
|
||||
let transforms = fromPairs(
|
||||
map(k => [k, recurseOne(k)], keys(objectShape)) as any
|
||||
)
|
||||
return evolve(transforms as any, objectShape)
|
||||
}
|
||||
|
||||
export let evaluateObject = (objectShape, effect) => (
|
||||
cache,
|
||||
situationGate,
|
||||
parsedRules,
|
||||
node
|
||||
) => {
|
||||
const evaluate = evaluateNode.bind(null, cache, situationGate, parsedRules)
|
||||
const evaluations = map(evaluate, node.explanation)
|
||||
const temporalExplanations = mapTemporal(
|
||||
Object.fromEntries,
|
||||
concatTemporals(
|
||||
Object.entries(evaluations).map(([key, node]) =>
|
||||
zipTemporals(pureTemporal(key), liftTemporalNode(node))
|
||||
)
|
||||
)
|
||||
)
|
||||
const temporalExplanation = mapTemporal(explanations => {
|
||||
const evaluation = effect(explanations, cache, situationGate, parsedRules)
|
||||
return {
|
||||
...evaluation,
|
||||
explanation: {
|
||||
...explanations,
|
||||
...evaluation.explanation
|
||||
}
|
||||
}
|
||||
}, temporalExplanations)
|
||||
|
||||
const sameUnitTemporalExplanation: Temporal<EvaluatedNode<
|
||||
number
|
||||
>> = convertNodesToSameUnit(
|
||||
temporalExplanation.map(x => x.value),
|
||||
cache._meta.contextRule,
|
||||
node.name
|
||||
).map((node, i) => ({
|
||||
...temporalExplanation[i],
|
||||
value: simplifyNodeUnit(node)
|
||||
}))
|
||||
|
||||
const temporalValue = mapTemporal(
|
||||
({ nodeValue }) => nodeValue,
|
||||
sameUnitTemporalExplanation
|
||||
)
|
||||
const nodeValue = temporalAverage(temporalValue)
|
||||
|
||||
const baseEvaluation = {
|
||||
...node,
|
||||
nodeValue,
|
||||
unit: sameUnitTemporalExplanation[0].value.unit,
|
||||
explanation: evaluations,
|
||||
missingVariables: mergeAllMissing(Object.values(evaluations))
|
||||
}
|
||||
if (sameUnitTemporalExplanation.length === 1) {
|
||||
return {
|
||||
...baseEvaluation,
|
||||
explanation: sameUnitTemporalExplanation[0].value.explanation
|
||||
}
|
||||
}
|
||||
return {
|
||||
...baseEvaluation,
|
||||
temporalValue,
|
||||
temporalExplanation
|
||||
}
|
||||
}
|
|
@ -6,25 +6,33 @@
|
|||
# @preprocessor esmodule
|
||||
|
||||
@{%
|
||||
const {string, filteredVariable, date, variable, variableWithConversion, binaryOperation, unaryOperation, boolean, number, numberWithUnit } = require('./grammarFunctions')
|
||||
const {
|
||||
string, filteredVariable, date, variable, variableWithConversion,
|
||||
temporalNumericValue, binaryOperation, unaryOperation, boolean, number,
|
||||
numberWithUnit
|
||||
} = require('./grammarFunctions')
|
||||
|
||||
const moo = require("moo");
|
||||
|
||||
const dateRegexp = /(?:(?:0?[1-9]|[12][0-9]|3[01])\/)?(?:0?[1-9]|1[012])\/\d{4}/
|
||||
const dateRegexp = `(?:(?:0?[1-9]|[12][0-9]|3[01])\\/)?(?:0?[1-9]|1[012])\\/\\d{4}`
|
||||
const letter = '[a-zA-Z\u00C0-\u017F€$%]';
|
||||
const letterOrNumber = '[a-zA-Z\u00C0-\u017F0-9\']';
|
||||
const word = `${letter}(?:[\-']?${letterOrNumber}+)*`;
|
||||
const word = `${letter}(?:[-']?${letterOrNumber}+)*`;
|
||||
const wordOrNumber = `(?:${word}|${letterOrNumber}+)`
|
||||
const words = `${word}(?:[\\s]?${wordOrNumber}+)*`
|
||||
const periodWord = `\\| ${word}(?:[\\s]${word})*`
|
||||
|
||||
const numberRegExp = '-?(?:[1-9][0-9]+|[0-9])(?:\\.[0-9]+)?';
|
||||
const lexer = moo.compile({
|
||||
date: dateRegexp,
|
||||
'(': '(',
|
||||
')': ')',
|
||||
'[': '[',
|
||||
']': ']',
|
||||
comparison: ['>','<','>=','<=','=','!='],
|
||||
infinity: 'Infinity',
|
||||
colon: " : ",
|
||||
date: new RegExp(dateRegexp),
|
||||
periodWord: new RegExp(periodWord),
|
||||
words: new RegExp(words),
|
||||
number: new RegExp(numberRegExp),
|
||||
string: /'[ \t\.'a-zA-Z\-\u00C0-\u017F0-9 ]+'/,
|
||||
|
@ -33,7 +41,7 @@ const lexer = moo.compile({
|
|||
dot: ' . ',
|
||||
'.': '.',
|
||||
letterOrNumber: new RegExp(letterOrNumber),
|
||||
space: { match: /[\s]+/, lineBreaks: true }
|
||||
space: { match: /[\s]+/, lineBreaks: true },
|
||||
});
|
||||
|
||||
const join = (args) => ({value: (args.map(x => x && x.value).join(""))})
|
||||
|
@ -43,12 +51,20 @@ const flattenJoin = ([a, b]) => Array.isArray(b) ? join([a, ...b]) : a
|
|||
@lexer lexer
|
||||
|
||||
main ->
|
||||
AdditionSubstraction {% id %}
|
||||
| Comparison {% id %}
|
||||
| NonNumericTerminal {% id %}
|
||||
| Negation {% id %}
|
||||
Comparison {% id %}
|
||||
| NumericValue {% id %}
|
||||
| Date {% id %}
|
||||
| NonNumericTerminal {% id %}
|
||||
|
||||
NumericValue ->
|
||||
AdditionSubstraction {% id %}
|
||||
| Negation {% id %}
|
||||
| TemporalNumericValue {% id %}
|
||||
|
||||
TemporalNumericValue ->
|
||||
NumericValue %space %periodWord %space %date {% ([value,,word,,dateString]) => temporalNumericValue(value, word, date([dateString])) %}
|
||||
| NumericValue %space %periodWord %colon Date {% ([value,,word,,date]) => temporalNumericValue(value, word, date) %}
|
||||
|
||||
NumericTerminal ->
|
||||
Variable {% id %}
|
||||
| VariableWithUnitConversion {% id %}
|
||||
|
@ -59,8 +75,7 @@ Negation ->
|
|||
"-" %space Parentheses {% unaryOperation('calculation') %}
|
||||
|
||||
Parentheses ->
|
||||
"(" AdditionSubstraction ")" {% ([,e]) => e %}
|
||||
| "(" Negation ")" {% ([,e]) => e %}
|
||||
"(" NumericValue ")" {% ([,e]) => e %}
|
||||
| NumericTerminal {% id %}
|
||||
|
||||
Date ->
|
||||
|
@ -92,6 +107,7 @@ VariableWithUnitConversion ->
|
|||
Filter -> "." %words {% ([,filter]) => filter %}
|
||||
FilteredVariable -> Variable %space Filter {% filteredVariable %}
|
||||
|
||||
|
||||
AdditionSubstraction ->
|
||||
AdditionSubstraction %space %additionSubstraction %space MultiplicationDivision {% binaryOperation('calculation') %}
|
||||
| MultiplicationDivision {% id %}
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
The advantage of putting them here is to get prettier's JS formatting, since Nealrey doesn't support it https://github.com/kach/nearley/issues/310 */
|
||||
import { normalizeDateString } from 'Engine/date'
|
||||
import { parseUnit } from 'Engine/units'
|
||||
import { parsePeriod } from './temporal'
|
||||
|
||||
export let binaryOperation = operationType => ([A, , operator, , B]) => ({
|
||||
[operator]: {
|
||||
|
@ -25,6 +26,13 @@ export let variableWithConversion = ([{ variable }, , unit]) => ({
|
|||
unitConversion: { explanation: variable, unit: parseUnit(unit.value) }
|
||||
})
|
||||
|
||||
export let temporalNumericValue = (variable, word, date) => ({
|
||||
temporalValue: {
|
||||
explanation: variable,
|
||||
period: parsePeriod(word.value.slice(2), date)
|
||||
}
|
||||
})
|
||||
|
||||
export let variable = ([firstFragment, nextFragment], _, reject) => {
|
||||
const fragments = [firstFragment, ...nextFragment].map(({ value }) => value)
|
||||
if (!nextFragment.length && ['oui', 'non'].includes(firstFragment)) {
|
||||
|
|
|
@ -11,7 +11,6 @@ import './Variations.css'
|
|||
let Comp = function Variations({ nodeValue, explanation, unit }) {
|
||||
let [expandedVariation, toggleVariation] = useState(null)
|
||||
const { i18n } = useTranslation()
|
||||
|
||||
return (
|
||||
<ShowValuesConsumer>
|
||||
{showValues => (
|
||||
|
@ -64,7 +63,7 @@ let Comp = function Variations({ nodeValue, explanation, unit }) {
|
|||
)}
|
||||
{(expandedVariation === i || satisfied || !showValues) && (
|
||||
<div style={{ margin: '1rem 0' }}>
|
||||
{condition && (
|
||||
{!condition.isDefault && (
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
|
@ -89,7 +88,7 @@ let Comp = function Variations({ nodeValue, explanation, unit }) {
|
|||
satisfied
|
||||
})}
|
||||
>
|
||||
{condition ? (
|
||||
{!condition.isDefault ? (
|
||||
<Trans>Alors</Trans>
|
||||
) : (
|
||||
<Trans>Sinon</Trans>
|
||||
|
|
|
@ -3,7 +3,6 @@ import variations from 'Engine/mecanisms/variations'
|
|||
import { convertNodeToUnit } from 'Engine/nodeUnits'
|
||||
import { inferUnit, isPercentUnit } from 'Engine/units'
|
||||
import {
|
||||
add,
|
||||
any,
|
||||
equals,
|
||||
evolve,
|
||||
|
@ -15,11 +14,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 +278,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 +290,7 @@ export let mecanismRecalcul = dottedNameContext => (recurse, k, v) => {
|
|||
])
|
||||
)
|
||||
|
||||
if (currentCache._metaInRecalcul) {
|
||||
if (currentCache._meta.inRecalcul) {
|
||||
return defaultNode(false)
|
||||
}
|
||||
|
||||
|
@ -344,7 +341,10 @@ export let mecanismRecalcul = dottedNameContext => (recurse, k, v) => {
|
|||
export let mecanismSum = (recurse, k, v) => {
|
||||
let explanation = v.map(recurse)
|
||||
|
||||
let evaluate = evaluateArray(add, 0)
|
||||
let evaluate = evaluateArray(
|
||||
(x, y) => (x === false && y === false ? false : x + y),
|
||||
false
|
||||
)
|
||||
|
||||
return {
|
||||
evaluate,
|
||||
|
@ -376,7 +376,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 +431,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 +509,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 +602,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(
|
||||
|
|
|
@ -1,8 +1,16 @@
|
|||
import { evaluationError } from 'Engine/error'
|
||||
import { defaultNode, evaluateNode, mergeAllMissing } from 'Engine/evaluation'
|
||||
import { decompose } from 'Engine/mecanisms/utils'
|
||||
import variations from 'Engine/mecanisms/variations'
|
||||
import Barème from 'Engine/mecanismViews/Barème'
|
||||
import { convertUnit, parseUnit } from '../units'
|
||||
import {
|
||||
liftTemporal2,
|
||||
liftTemporalNode,
|
||||
mapTemporal,
|
||||
temporalAverage
|
||||
} from 'Engine/temporal'
|
||||
import { convertUnit } from '../units'
|
||||
import { parseUnit } from './../units'
|
||||
import {
|
||||
evaluatePlafondUntilActiveTranche,
|
||||
parseTranches
|
||||
|
@ -34,29 +42,27 @@ export default function parse(parse, k, v) {
|
|||
}
|
||||
}
|
||||
|
||||
const evaluate = (
|
||||
cache,
|
||||
situationGate,
|
||||
parsedRules,
|
||||
node: ReturnType<typeof parse>
|
||||
) => {
|
||||
const evaluate = evaluateNode.bind(null, cache, situationGate, parsedRules)
|
||||
const assiette = evaluate(node.explanation.assiette)
|
||||
const multiplicateur = evaluate(node.explanation.multiplicateur)
|
||||
const tranches = evaluatePlafondUntilActiveTranche(
|
||||
evaluate,
|
||||
{
|
||||
parsedTranches: node.explanation.tranches,
|
||||
assiette,
|
||||
multiplicateur
|
||||
},
|
||||
cache
|
||||
).map(tranche => {
|
||||
function evaluateBarème(tranches, assiette, evaluate, cache) {
|
||||
return tranches.map(tranche => {
|
||||
if (tranche.isAfterActive) {
|
||||
return { ...tranche, nodeValue: 0 }
|
||||
}
|
||||
const taux = evaluate(tranche.taux)
|
||||
if ([taux.nodeValue, tranche.nodeValue].some(value => value === null)) {
|
||||
if (taux.temporalValue) {
|
||||
evaluationError(
|
||||
cache._meta.contextRule,
|
||||
"Le taux d'une tranche ne peut pas être une valeur temporelle"
|
||||
)
|
||||
}
|
||||
|
||||
if (
|
||||
[
|
||||
assiette.nodeValue,
|
||||
taux.nodeValue,
|
||||
tranche.plafondValue,
|
||||
tranche.plancherValue
|
||||
].some(value => value === null)
|
||||
) {
|
||||
return {
|
||||
...tranche,
|
||||
taux,
|
||||
|
@ -71,20 +77,61 @@ const evaluate = (
|
|||
nodeValue:
|
||||
(Math.min(assiette.nodeValue, tranche.plafondValue) -
|
||||
tranche.plancherValue) *
|
||||
convertUnit(taux.unit, parseUnit(''), taux.nodeValue)
|
||||
convertUnit(taux.unit, parseUnit(''), taux.nodeValue as number)
|
||||
}
|
||||
})
|
||||
}
|
||||
const evaluate = (
|
||||
cache,
|
||||
situationGate,
|
||||
parsedRules,
|
||||
node: ReturnType<typeof parse>
|
||||
) => {
|
||||
const evaluate = evaluateNode.bind(null, cache, situationGate, parsedRules)
|
||||
const assiette = evaluate(node.explanation.assiette)
|
||||
const multiplicateur = evaluate(node.explanation.multiplicateur)
|
||||
const temporalTranchesPlafond = liftTemporal2(
|
||||
(assiette, multiplicateur) =>
|
||||
evaluatePlafondUntilActiveTranche(
|
||||
evaluate,
|
||||
{
|
||||
parsedTranches: node.explanation.tranches,
|
||||
assiette,
|
||||
multiplicateur
|
||||
},
|
||||
cache
|
||||
),
|
||||
liftTemporalNode(assiette),
|
||||
liftTemporalNode(multiplicateur)
|
||||
)
|
||||
const temporalTranches = liftTemporal2(
|
||||
(tranches, assiette) => evaluateBarème(tranches, assiette, evaluate, cache),
|
||||
temporalTranchesPlafond,
|
||||
liftTemporalNode(assiette)
|
||||
)
|
||||
const temporalValue = mapTemporal(
|
||||
tranches =>
|
||||
tranches.reduce(
|
||||
(value, { nodeValue }) =>
|
||||
nodeValue == null ? null : value + nodeValue,
|
||||
0
|
||||
),
|
||||
temporalTranches
|
||||
)
|
||||
return {
|
||||
...node,
|
||||
nodeValue: tranches.reduce(
|
||||
(value, { nodeValue }) => (nodeValue == null ? null : value + nodeValue),
|
||||
0
|
||||
),
|
||||
missingVariables: mergeAllMissing(tranches),
|
||||
nodeValue: temporalAverage(temporalValue),
|
||||
...(temporalValue.length > 1
|
||||
? {
|
||||
temporalValue
|
||||
}
|
||||
: { missingVariables: mergeAllMissing(temporalTranches[0].value) }),
|
||||
explanation: {
|
||||
assiette,
|
||||
multiplicateur,
|
||||
tranches
|
||||
...(temporalTranches.length > 1
|
||||
? { temporalTranches }
|
||||
: { tranches: temporalTranches[0].value })
|
||||
},
|
||||
unit: assiette.unit
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { convertToDate, normalizeDate } from 'Engine/date'
|
||||
import { convertToDate, convertToString } from 'Engine/date'
|
||||
import {
|
||||
defaultNode,
|
||||
evaluateNode,
|
||||
|
@ -26,12 +26,7 @@ function MecanismDurée({ nodeValue, explanation, unit }) {
|
|||
</Node>
|
||||
)
|
||||
}
|
||||
const today = new Date()
|
||||
const todayString = normalizeDate(
|
||||
today.getFullYear(),
|
||||
today.getMonth() + 1,
|
||||
today.getDate()
|
||||
)
|
||||
const todayString = convertToString(new Date())
|
||||
|
||||
const objectShape = {
|
||||
depuis: defaultNode(todayString),
|
||||
|
|
|
@ -2,6 +2,12 @@ import { defaultNode, evaluateNode, mergeAllMissing } from 'Engine/evaluation'
|
|||
import { decompose } from 'Engine/mecanisms/utils'
|
||||
import variations from 'Engine/mecanisms/variations'
|
||||
import grille from 'Engine/mecanismViews/Grille'
|
||||
import {
|
||||
liftTemporal2,
|
||||
liftTemporalNode,
|
||||
mapTemporal,
|
||||
temporalAverage
|
||||
} from 'Engine/temporal'
|
||||
import { parseUnit } from 'Engine/units'
|
||||
import { lensPath, over } from 'ramda'
|
||||
import {
|
||||
|
@ -35,25 +41,8 @@ export default function parse(parse, k, v) {
|
|||
unit: explanation.tranches[0].montant.unit
|
||||
}
|
||||
}
|
||||
|
||||
const evaluate = (
|
||||
cache,
|
||||
situationGate,
|
||||
parsedRules,
|
||||
node: ReturnType<typeof parse>
|
||||
) => {
|
||||
const evaluate = evaluateNode.bind(null, cache, situationGate, parsedRules)
|
||||
const assiette = evaluate(node.explanation.assiette)
|
||||
const multiplicateur = evaluate(node.explanation.multiplicateur)
|
||||
const tranches = evaluatePlafondUntilActiveTranche(
|
||||
evaluate,
|
||||
{
|
||||
parsedTranches: node.explanation.tranches,
|
||||
assiette,
|
||||
multiplicateur
|
||||
},
|
||||
cache
|
||||
).map(tranche => {
|
||||
const evaluateGrille = (tranches, evaluate) =>
|
||||
tranches.map(tranche => {
|
||||
if (tranche.isActive === false) {
|
||||
return tranche
|
||||
}
|
||||
|
@ -67,19 +56,65 @@ const evaluate = (
|
|||
}
|
||||
})
|
||||
|
||||
const activeTranches = tranches.filter(({ isActive }) => isActive != false)
|
||||
const missingVariables = mergeAllMissing(activeTranches)
|
||||
const nodeValue = activeTranches.length ? activeTranches[0].nodeValue : false
|
||||
const evaluate = (
|
||||
cache,
|
||||
situationGate,
|
||||
parsedRules,
|
||||
node: ReturnType<typeof parse>
|
||||
) => {
|
||||
const evaluate = evaluateNode.bind(null, cache, situationGate, parsedRules)
|
||||
const assiette = evaluate(node.explanation.assiette)
|
||||
const multiplicateur = evaluate(node.explanation.multiplicateur)
|
||||
const temporalTranchesPlafond = liftTemporal2(
|
||||
(assiette, multiplicateur) =>
|
||||
evaluatePlafondUntilActiveTranche(
|
||||
evaluate,
|
||||
{
|
||||
parsedTranches: node.explanation.tranches,
|
||||
assiette,
|
||||
multiplicateur
|
||||
},
|
||||
cache
|
||||
),
|
||||
liftTemporalNode(assiette),
|
||||
liftTemporalNode(multiplicateur)
|
||||
)
|
||||
const temporalTranches = mapTemporal(
|
||||
tranches => evaluateGrille(tranches, evaluate),
|
||||
temporalTranchesPlafond
|
||||
)
|
||||
|
||||
const activeTranches = mapTemporal(tranches => {
|
||||
const activeTranche = tranches.find(tranche => tranche.isActive)
|
||||
if (activeTranche) {
|
||||
return [activeTranche]
|
||||
}
|
||||
const lastTranche = tranches[tranches.length - 1]
|
||||
if (lastTranche.isAfterActive === false) {
|
||||
return [{ nodeValue: false }]
|
||||
}
|
||||
return tranches.filter(tranche => tranche.isActive === null)
|
||||
}, temporalTranches)
|
||||
const temporalValue = mapTemporal(
|
||||
tranches => (tranches[0].isActive === null ? null : tranches[0].nodeValue),
|
||||
activeTranches
|
||||
)
|
||||
|
||||
return {
|
||||
...node,
|
||||
nodeValue: temporalAverage(temporalValue),
|
||||
...(temporalValue.length > 1
|
||||
? {
|
||||
temporalValue
|
||||
}
|
||||
: { missingVariables: mergeAllMissing(activeTranches[0].value) }),
|
||||
explanation: {
|
||||
tranches,
|
||||
assiette,
|
||||
multiplicateur
|
||||
multiplicateur,
|
||||
...(temporalTranches.length > 1
|
||||
? { temporalTranches }
|
||||
: { tranches: temporalTranches[0].value })
|
||||
},
|
||||
missingVariables,
|
||||
nodeValue,
|
||||
unit: activeTranches[0]?.unit ?? node.unit
|
||||
unit: activeTranches[0].value[0]?.unit ?? node.unit
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,12 +1,14 @@
|
|||
import { convertToDate } from 'Engine/date'
|
||||
import { typeWarning } from 'Engine/error'
|
||||
import { evaluateNode, makeJsx, mergeMissing } from 'Engine/evaluation'
|
||||
import { Node } from 'Engine/mecanismViews/common'
|
||||
import { convertNodeToUnit } from 'Engine/nodeUnits'
|
||||
import { liftTemporal2, pureTemporal, temporalAverage } from 'Engine/temporal'
|
||||
import { inferUnit, serializeUnit } from 'Engine/units'
|
||||
import { curry, map } from 'ramda'
|
||||
import React from 'react'
|
||||
import { convertToDateIfNeeded } from '../date.ts'
|
||||
|
||||
const comparisonOperator = ['≠', '=', '<', '>', '≤', '≥']
|
||||
export default (k, operatorFunction, symbol) => (recurse, k, v) => {
|
||||
let evaluate = (cache, situation, parsedRules, node) => {
|
||||
const explanation = map(
|
||||
|
@ -43,30 +45,49 @@ export default (k, operatorFunction, symbol) => (recurse, k, v) => {
|
|||
)
|
||||
}
|
||||
}
|
||||
let node1Value = node1.nodeValue
|
||||
let node2Value = node2.nodeValue
|
||||
try {
|
||||
;[node1Value, node2Value] = convertToDateIfNeeded(
|
||||
node1.nodeValue,
|
||||
node2.nodeValue
|
||||
)
|
||||
} catch (e) {
|
||||
typeWarning(
|
||||
cache._meta.contextRule,
|
||||
`Impossible de convertir une des valeur en date`,
|
||||
e
|
||||
)
|
||||
}
|
||||
let nodeValue = operatorFunction(node1Value, node2Value)
|
||||
|
||||
let unit = inferUnit(k, [node1.unit, node2.unit])
|
||||
return {
|
||||
const baseNode = {
|
||||
...node,
|
||||
nodeValue,
|
||||
unit,
|
||||
explanation,
|
||||
unit: inferUnit(k, [node1.unit, node2.unit]),
|
||||
missingVariables
|
||||
}
|
||||
|
||||
let temporalValue = liftTemporal2(
|
||||
(a, b) => {
|
||||
if (['∕', '-'].includes(node.operator) && a === false) {
|
||||
return false
|
||||
}
|
||||
if (['+'].includes(node.operator) && a === false) {
|
||||
return b
|
||||
}
|
||||
if (['∕', '-', '×', '+'].includes(node.operator) && b === false) {
|
||||
return a
|
||||
}
|
||||
if (
|
||||
!['=', '≠'].includes(node.operator) &&
|
||||
(a === false || b === false)
|
||||
) {
|
||||
return false
|
||||
}
|
||||
if (
|
||||
comparisonOperator.includes(node.operator) &&
|
||||
[a, b].every(value => value.match?.(/[\d]{2}\/[\d]{2}\/[\d]{4}/))
|
||||
) {
|
||||
return operatorFunction(convertToDate(a), convertToDate(b))
|
||||
}
|
||||
return operatorFunction(a, b)
|
||||
},
|
||||
node1.temporalValue ?? pureTemporal(node1.nodeValue),
|
||||
node2.temporalValue ?? pureTemporal(node2.nodeValue)
|
||||
)
|
||||
|
||||
const nodeValue = temporalAverage(temporalValue, baseNode.unit)
|
||||
|
||||
return {
|
||||
...baseNode,
|
||||
nodeValue,
|
||||
...(temporalValue.length > 1 && { temporalValue })
|
||||
}
|
||||
}
|
||||
|
||||
let explanation = v.explanation.map(recurse)
|
||||
|
|
|
@ -0,0 +1,151 @@
|
|||
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/temporal'
|
||||
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
|
||||
}
|
||||
}
|
|
@ -1,4 +1,5 @@
|
|||
import { mergeAllMissing } from 'Engine/evaluation'
|
||||
import { Evaluation } from 'Engine/temporal'
|
||||
import { evolve } from 'ramda'
|
||||
import { evaluationError, typeWarning } from '../error'
|
||||
import { convertUnit, inferUnit } from '../units'
|
||||
|
@ -7,7 +8,6 @@ export const parseTranches = (parse, tranches) => {
|
|||
return tranches
|
||||
.map((t, i) => {
|
||||
if (!t.plafond && i > tranches.length) {
|
||||
console.log(t, i)
|
||||
throw new SyntaxError(
|
||||
`La tranche n°${i} du barème n'a pas de plafond précisé. Seule la dernière tranche peut ne pas être plafonnée`
|
||||
)
|
||||
|
@ -32,35 +32,30 @@ export function evaluatePlafondUntilActiveTranche(
|
|||
}
|
||||
|
||||
const plafond = evaluate(parsedTranche.plafond)
|
||||
if (plafond.temporalValue) {
|
||||
evaluationError(
|
||||
cache._meta.contextRule,
|
||||
'Les valeurs temporelles ne sont pas acceptées pour un plafond de tranche'
|
||||
)
|
||||
}
|
||||
const plancher = tranches[i - 1]
|
||||
? tranches[i - 1].plafond
|
||||
: { nodeValue: 0 }
|
||||
const calculationValues = [plafond, assiette, multiplicateur, plancher]
|
||||
if (calculationValues.some(node => node.nodeValue === null)) {
|
||||
return [
|
||||
[
|
||||
...tranches,
|
||||
{
|
||||
...parsedTranche,
|
||||
plafond,
|
||||
nodeValue: null,
|
||||
isActive: null,
|
||||
isAfterActive: false,
|
||||
missingVariables: mergeAllMissing(calculationValues)
|
||||
}
|
||||
],
|
||||
false
|
||||
]
|
||||
}
|
||||
let plafondValue = plafond.nodeValue * multiplicateur.nodeValue
|
||||
|
||||
let plafondValue: Evaluation<number> =
|
||||
plafond.nodeValue === null || multiplicateur.nodeValue === null
|
||||
? null
|
||||
: plafond.nodeValue * multiplicateur.nodeValue
|
||||
|
||||
try {
|
||||
plafondValue = [Infinity || 0].includes(plafondValue)
|
||||
? plafondValue
|
||||
: convertUnit(
|
||||
inferUnit('*', [plafond.unit, multiplicateur.unit]),
|
||||
assiette.unit,
|
||||
plafondValue
|
||||
)
|
||||
plafondValue =
|
||||
plafondValue === Infinity || plafondValue === 0
|
||||
? plafondValue
|
||||
: convertUnit(
|
||||
inferUnit('*', [plafond.unit, multiplicateur.unit]),
|
||||
assiette.unit,
|
||||
plafondValue
|
||||
)
|
||||
} catch (e) {
|
||||
typeWarning(
|
||||
cache._meta.contextRule,
|
||||
|
@ -69,9 +64,37 @@ export function evaluatePlafondUntilActiveTranche(
|
|||
e
|
||||
)
|
||||
}
|
||||
|
||||
let plancherValue = tranches[i - 1] ? tranches[i - 1].plafondValue : 0
|
||||
if (!!tranches[i - 1] && plafondValue <= plancherValue) {
|
||||
const isAfterActive =
|
||||
plancherValue === null || assiette.nodeValue === null
|
||||
? null
|
||||
: plancherValue > assiette.nodeValue
|
||||
|
||||
const calculationValues = [plafond, assiette, multiplicateur, plancher]
|
||||
if (calculationValues.some(node => node.nodeValue === null)) {
|
||||
return [
|
||||
[
|
||||
...tranches,
|
||||
{
|
||||
...parsedTranche,
|
||||
plafond,
|
||||
plafondValue,
|
||||
plancherValue,
|
||||
nodeValue: null,
|
||||
isActive: null,
|
||||
isAfterActive,
|
||||
missingVariables: mergeAllMissing(calculationValues)
|
||||
}
|
||||
],
|
||||
false
|
||||
]
|
||||
}
|
||||
|
||||
if (
|
||||
!!tranches[i - 1] &&
|
||||
!!plancherValue &&
|
||||
<number>plafondValue <= plancherValue
|
||||
) {
|
||||
evaluationError(
|
||||
cache._meta.contextRule,
|
||||
`Le plafond de la tranche n°${i +
|
||||
|
@ -84,10 +107,10 @@ export function evaluatePlafondUntilActiveTranche(
|
|||
plafond,
|
||||
plancherValue,
|
||||
plafondValue,
|
||||
isAfterActive: false,
|
||||
isAfterActive,
|
||||
isActive:
|
||||
assiette.nodeValue >= plancherValue &&
|
||||
assiette.nodeValue < plafondValue
|
||||
assiette.nodeValue < <number>plafondValue
|
||||
}
|
||||
|
||||
return [[...tranches, tranche], tranche.isActive]
|
||||
|
|
|
@ -0,0 +1,63 @@
|
|||
import { evaluateNode } from 'Engine/evaluation'
|
||||
import {
|
||||
createTemporalEvaluation,
|
||||
narrowTemporalValue,
|
||||
temporalAverage
|
||||
} from 'Engine/temporal'
|
||||
import { Temporal } from './../temporal'
|
||||
|
||||
function evaluate(
|
||||
cache: any,
|
||||
situation: any,
|
||||
parsedRules: any,
|
||||
node: ReturnType<typeof parseVariableTemporelle>
|
||||
) {
|
||||
const evaluateAttribute = evaluateNode.bind(
|
||||
null,
|
||||
cache,
|
||||
situation,
|
||||
parsedRules
|
||||
)
|
||||
|
||||
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 = value.temporalValue
|
||||
? narrowTemporalValue(period, value.temporalValue)
|
||||
: createTemporalEvaluation(value.nodeValue, period)
|
||||
// TODO explanation missingVariables / period missing variables
|
||||
return {
|
||||
...node,
|
||||
nodeValue: temporalAverage(temporalValue as Temporal<number>, value.unit),
|
||||
temporalValue,
|
||||
explanation: {
|
||||
period: { start, end },
|
||||
value
|
||||
},
|
||||
unit: value.unit
|
||||
}
|
||||
}
|
||||
|
||||
export default function parseVariableTemporelle(parse, __, v: any) {
|
||||
const explanation = parse(v.explanation)
|
||||
return {
|
||||
evaluate,
|
||||
explanation: {
|
||||
period: {
|
||||
start: v.period.start && parse(v.period.start),
|
||||
end: v.period.end && parse(v.period.end)
|
||||
},
|
||||
value: explanation
|
||||
},
|
||||
unit: explanation.unit
|
||||
}
|
||||
}
|
|
@ -1,117 +0,0 @@
|
|||
import {
|
||||
bonus,
|
||||
collectNodeMissing,
|
||||
evaluateNode,
|
||||
mergeAllMissing,
|
||||
mergeMissing
|
||||
} from 'Engine/evaluation'
|
||||
import Variations from 'Engine/mecanismViews/Variations'
|
||||
import { inferUnit } from 'Engine/units'
|
||||
import { dissoc, filter, isNil, pluck, reduce, reject } from 'ramda'
|
||||
|
||||
/* @devariate = true => This function will produce variations of a same mecanism (e.g. product) that share some common properties */
|
||||
export default (recurse, k, v, devariate) => {
|
||||
let explanation = devariate
|
||||
? devariateExplanation(recurse, k, v)
|
||||
: v.map(({ si, alors, sinon }) =>
|
||||
sinon !== undefined
|
||||
? { consequence: recurse(sinon), condition: undefined }
|
||||
: { consequence: recurse(alors), condition: recurse(si) }
|
||||
)
|
||||
|
||||
let evaluate = (cache, situationGate, parsedRules, node) => {
|
||||
let evaluateVariationProp = prop =>
|
||||
prop && evaluateNode(cache, situationGate, parsedRules, prop),
|
||||
// mark the satisfied variation if any in the explanation
|
||||
[, resolvedExplanation] = reduce(
|
||||
([resolved, result], variation) => {
|
||||
if (resolved) return [true, [...result, variation]]
|
||||
|
||||
// evaluate the condition
|
||||
let evaluatedCondition = evaluateVariationProp(variation.condition)
|
||||
|
||||
if (evaluatedCondition == undefined) {
|
||||
// No condition : we've reached the eventual defaut case
|
||||
let evaluatedVariation = {
|
||||
consequence: evaluateVariationProp(variation.consequence),
|
||||
satisfied: true
|
||||
}
|
||||
return [true, [...result, evaluatedVariation]]
|
||||
}
|
||||
|
||||
if (evaluatedCondition.nodeValue === null)
|
||||
// the current variation case has missing variables => we can't go further
|
||||
return [
|
||||
true,
|
||||
[...result, { ...variation, condition: evaluatedCondition }]
|
||||
]
|
||||
|
||||
if (evaluatedCondition.nodeValue === true) {
|
||||
let evaluatedVariation = {
|
||||
condition: evaluatedCondition,
|
||||
consequence: evaluateVariationProp(variation.consequence),
|
||||
satisfied: true
|
||||
}
|
||||
return [true, [...result, evaluatedVariation]]
|
||||
}
|
||||
return [false, [...result, variation]]
|
||||
},
|
||||
[false, []]
|
||||
)(node.explanation),
|
||||
satisfiedVariation = resolvedExplanation.find(v => v.satisfied),
|
||||
nodeValue = satisfiedVariation
|
||||
? satisfiedVariation.consequence.nodeValue
|
||||
: null
|
||||
|
||||
let leftMissing = mergeAllMissing(
|
||||
reject(isNil, pluck('condition', resolvedExplanation))
|
||||
),
|
||||
candidateVariations = filter(
|
||||
node => !node.condition || node.condition.nodeValue !== false,
|
||||
resolvedExplanation
|
||||
),
|
||||
rightMissing = mergeAllMissing(
|
||||
reject(isNil, pluck('consequence', candidateVariations))
|
||||
),
|
||||
missingVariables = satisfiedVariation
|
||||
? collectNodeMissing(satisfiedVariation.consequence)
|
||||
: mergeMissing(bonus(leftMissing), rightMissing)
|
||||
|
||||
return {
|
||||
...node,
|
||||
nodeValue,
|
||||
...(satisfiedVariation && { unit: satisfiedVariation?.consequence.unit }),
|
||||
explanation: resolvedExplanation,
|
||||
missingVariables
|
||||
}
|
||||
}
|
||||
|
||||
// TODO - find an appropriate representation
|
||||
return {
|
||||
explanation,
|
||||
evaluate,
|
||||
jsx: Variations,
|
||||
category: 'mecanism',
|
||||
name: 'variations',
|
||||
type: 'numeric',
|
||||
unit: inferUnit(
|
||||
'+',
|
||||
explanation.map(r => r.consequence.unit)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export let devariateExplanation = (recurse, mecanismKey, v) => {
|
||||
let fixedProps = dissoc('variations')(v),
|
||||
explanation = v.variations.map(({ si, alors, sinon }) => ({
|
||||
consequence: recurse({
|
||||
[mecanismKey]: {
|
||||
...fixedProps,
|
||||
...(sinon || alors)
|
||||
}
|
||||
}),
|
||||
condition: sinon ? undefined : recurse(si)
|
||||
}))
|
||||
|
||||
return explanation
|
||||
}
|
|
@ -0,0 +1,164 @@
|
|||
import { typeWarning } from 'Engine/error'
|
||||
import { defaultNode, evaluateNode } from 'Engine/evaluation'
|
||||
import Variations from 'Engine/mecanismViews/Variations'
|
||||
import { convertNodeToUnit } from 'Engine/nodeUnits'
|
||||
import {
|
||||
liftTemporal2,
|
||||
pureTemporal,
|
||||
sometime,
|
||||
temporalAverage
|
||||
} from 'Engine/temporal'
|
||||
import { inferUnit } from 'Engine/units'
|
||||
import { or } from 'ramda'
|
||||
import { mergeAllMissing } from './../evaluation'
|
||||
|
||||
/* @devariate = true => This function will produce variations of a same mecanism (e.g. product) that share some common properties */
|
||||
export default function parse(recurse, k, v, devariate) {
|
||||
let explanation = devariate
|
||||
? devariateExplanation(recurse, k, v)
|
||||
: v.map(({ si, alors, sinon }) =>
|
||||
sinon !== undefined
|
||||
? { consequence: recurse(sinon), condition: defaultNode(true) }
|
||||
: { consequence: recurse(alors), condition: recurse(si) }
|
||||
)
|
||||
|
||||
// TODO - find an appropriate representation
|
||||
return {
|
||||
explanation,
|
||||
evaluate,
|
||||
jsx: Variations,
|
||||
category: 'mecanism',
|
||||
name: 'variations',
|
||||
type: 'numeric',
|
||||
unit: inferUnit(
|
||||
'+',
|
||||
explanation.map(r => r.consequence.unit)
|
||||
)
|
||||
}
|
||||
}
|
||||
type Variation =
|
||||
| {
|
||||
si: any
|
||||
alors: Object
|
||||
}
|
||||
| {
|
||||
sinon: Object
|
||||
}
|
||||
export let devariateExplanation = (
|
||||
recurse,
|
||||
mecanismKey,
|
||||
v: { variations: Array<Variation> }
|
||||
) => {
|
||||
const { variations, ...fixedProps } = v
|
||||
const explanation = variations.map(variation => ({
|
||||
condition: 'sinon' in variation ? defaultNode(true) : recurse(variation.si),
|
||||
consequence: recurse({
|
||||
[mecanismKey]: {
|
||||
...fixedProps,
|
||||
...('sinon' in variation ? variation.sinon : variation.alors)
|
||||
}
|
||||
})
|
||||
}))
|
||||
|
||||
return explanation
|
||||
}
|
||||
|
||||
function evaluate(
|
||||
cache,
|
||||
situationGate,
|
||||
parsedRules,
|
||||
node: ReturnType<typeof parse>
|
||||
) {
|
||||
const evaluate = evaluateNode.bind(null, cache, situationGate, parsedRules)
|
||||
|
||||
const [temporalValue, explanation, unit] = node.explanation.reduce(
|
||||
(
|
||||
[evaluation, explanations, unit, previousConditions],
|
||||
{ condition, consequence },
|
||||
i: number
|
||||
) => {
|
||||
const previousConditionsAlwaysTrue = !sometime(
|
||||
value => value !== true,
|
||||
previousConditions
|
||||
)
|
||||
if (previousConditionsAlwaysTrue) {
|
||||
return [
|
||||
evaluation,
|
||||
[...explanations, { condition, consequence }],
|
||||
unit,
|
||||
previousConditions
|
||||
]
|
||||
}
|
||||
const evaluatedCondition = evaluate(condition)
|
||||
const currentCondition = liftTemporal2(
|
||||
(previousCond, currentCond) =>
|
||||
previousCond === null ? previousCond : !previousCond && currentCond,
|
||||
previousConditions,
|
||||
evaluatedCondition.temporalValue ??
|
||||
pureTemporal(evaluatedCondition.nodeValue)
|
||||
)
|
||||
const currentConditionAlwaysFalse = !sometime(
|
||||
x => x !== false,
|
||||
currentCondition
|
||||
)
|
||||
if (currentConditionAlwaysFalse) {
|
||||
return [
|
||||
evaluation,
|
||||
[...explanations, { condition: evaluatedCondition, consequence }],
|
||||
unit,
|
||||
previousConditions
|
||||
]
|
||||
}
|
||||
let evaluatedConsequence = evaluate(consequence)
|
||||
|
||||
try {
|
||||
evaluatedConsequence = convertNodeToUnit(unit, evaluatedConsequence)
|
||||
} catch (e) {
|
||||
return typeWarning(
|
||||
cache._meta.contexRule,
|
||||
`L'unité de la branche n° ${i} du mécanisme 'variations' n'est pas compatible avec celle d'une branche précédente`,
|
||||
e
|
||||
)
|
||||
}
|
||||
const currentValue = liftTemporal2(
|
||||
(cond, value) => cond && value,
|
||||
currentCondition,
|
||||
evaluatedConsequence.temporalValue ??
|
||||
pureTemporal(evaluatedConsequence.nodeValue)
|
||||
)
|
||||
return [
|
||||
liftTemporal2(or, evaluation, currentValue),
|
||||
[
|
||||
...explanations,
|
||||
{
|
||||
condition: evaluatedCondition,
|
||||
satisfied: !!evaluatedCondition.nodeValue,
|
||||
consequence: evaluatedConsequence
|
||||
}
|
||||
],
|
||||
unit || evaluatedConsequence.unit,
|
||||
liftTemporal2(or, previousConditions, currentCondition)
|
||||
]
|
||||
},
|
||||
[pureTemporal(false), [], node.unit, pureTemporal(false)]
|
||||
)
|
||||
const nodeValue = temporalAverage(temporalValue, unit)
|
||||
const missingVariables = mergeAllMissing(
|
||||
explanation.reduce(
|
||||
(values, { condition, consequence }) => [
|
||||
...values,
|
||||
condition,
|
||||
consequence
|
||||
],
|
||||
[]
|
||||
)
|
||||
)
|
||||
return {
|
||||
...node,
|
||||
nodeValue,
|
||||
unit,
|
||||
explanation,
|
||||
missingVariables,
|
||||
...(temporalValue.length > 1 && { temporalValue })
|
||||
}
|
||||
}
|
|
@ -1,3 +1,4 @@
|
|||
import { EvaluatedNode, mapTemporal } from './temporal'
|
||||
import {
|
||||
areUnitConvertible,
|
||||
convertUnit,
|
||||
|
@ -6,10 +7,11 @@ import {
|
|||
} from './units'
|
||||
|
||||
export function simplifyNodeUnit(node) {
|
||||
if (!node.unit || !node.nodeValue) {
|
||||
if (!node.unit || node.nodeValue === false || node.nodeValue == null) {
|
||||
return node
|
||||
}
|
||||
const [unit, nodeValue] = simplifyUnitWithValue(node.unit, node.nodeValue)
|
||||
|
||||
return {
|
||||
...node,
|
||||
unit,
|
||||
|
@ -35,12 +37,19 @@ export const getNodeDefaultUnit = (node, cache) => {
|
|||
)
|
||||
}
|
||||
|
||||
export function convertNodeToUnit(to: Unit, node) {
|
||||
export function convertNodeToUnit(to: Unit, node: EvaluatedNode<number>) {
|
||||
return {
|
||||
...node,
|
||||
nodeValue: node.unit
|
||||
? convertUnit(node.unit, to, node.nodeValue)
|
||||
: node.nodeValue,
|
||||
temporalValue:
|
||||
node.temporalValue && node.unit
|
||||
? mapTemporal(
|
||||
value => convertUnit(node.unit, to, value),
|
||||
node.temporalValue
|
||||
)
|
||||
: node.temporalValue,
|
||||
unit: to
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,7 +9,9 @@ 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'
|
||||
import { Grammar, Parser } from 'nearley'
|
||||
import {
|
||||
|
@ -25,7 +27,7 @@ import {
|
|||
subtract
|
||||
} from 'ramda'
|
||||
import React from 'react'
|
||||
import { syntaxError } from './error'
|
||||
import { EngineError, syntaxError } from './error'
|
||||
import grammar from './grammar.ne'
|
||||
import {
|
||||
mecanismAllOf,
|
||||
|
@ -86,6 +88,17 @@ export const parseExpression = (rule, rawNode) => {
|
|||
}
|
||||
|
||||
const parseMecanism = (rules, rule, parsedRules) => rawNode => {
|
||||
if (Array.isArray(rawNode)) {
|
||||
syntaxError(
|
||||
rule.dottedName,
|
||||
`
|
||||
Il manque le nom du mécanisme pour le tableau : [${rawNode
|
||||
.map(x => `'${x}'`)
|
||||
.join(', ')}]
|
||||
Les mécanisme possibles sont : 'somme', 'le maximum de', 'le minimum de', 'toutes ces conditions', 'une de ces conditions'.
|
||||
`
|
||||
)
|
||||
}
|
||||
if (Object.keys(rawNode).length > 1) {
|
||||
syntaxError(
|
||||
rule.dottedName,
|
||||
|
@ -136,7 +149,14 @@ Le mécanisme ${mecanismName} est inconnu.
|
|||
Vérifiez qu'il n'y ait pas d'erreur dans l'orthographe du nom.`
|
||||
)
|
||||
}
|
||||
return parseFn(parse(rules, rule, parsedRules), mecanismName, values)
|
||||
try {
|
||||
return parseFn(parse(rules, rule, parsedRules), mecanismName, values)
|
||||
} catch (e) {
|
||||
if (e instanceof EngineError) {
|
||||
throw e
|
||||
}
|
||||
syntaxError(rule.dottedName, e.message)
|
||||
}
|
||||
}
|
||||
|
||||
const knownOperations = {
|
||||
|
@ -164,8 +184,10 @@ const statelessParseFunction = {
|
|||
'une de ces conditions': mecanismOneOf,
|
||||
'toutes ces conditions': mecanismAllOf,
|
||||
somme: mecanismSum,
|
||||
régularisation,
|
||||
multiplication: mecanismProduct,
|
||||
produit: mecanismProduct,
|
||||
temporalValue: variableTemporelle,
|
||||
arrondi: mecanismRound,
|
||||
barème,
|
||||
grille,
|
||||
|
|
|
@ -149,10 +149,11 @@ Par défaut, seul le premier s'applique. Si vous voulez un autre comportement, v
|
|||
|
||||
if (cached) return addReplacementMissingVariable(cached)
|
||||
|
||||
let cacheNode = (nodeValue, missingVariables, explanation) => {
|
||||
let cacheNode = (nodeValue, missingVariables, explanation, temporalValue) => {
|
||||
cache[cacheName] = {
|
||||
...node,
|
||||
nodeValue,
|
||||
temporalValue,
|
||||
...(explanation && {
|
||||
explanation
|
||||
}),
|
||||
|
@ -183,7 +184,8 @@ Par défaut, seul le premier s'applique. Si vous voulez un autre comportement, v
|
|||
return cacheNode(
|
||||
evaluation.nodeValue,
|
||||
evaluation.missingVariables,
|
||||
evaluation
|
||||
evaluation,
|
||||
evaluation.temporalValue
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -83,8 +83,16 @@ export default (rules, rule, parsedRules) => {
|
|||
parsedRules,
|
||||
node.explanation
|
||||
),
|
||||
{ nodeValue, unit, missingVariables } = explanation
|
||||
return { ...node, nodeValue, unit, missingVariables, explanation }
|
||||
{ nodeValue, unit, missingVariables, temporalValue } = explanation
|
||||
|
||||
return {
|
||||
...node,
|
||||
nodeValue,
|
||||
unit,
|
||||
missingVariables,
|
||||
explanation,
|
||||
temporalValue
|
||||
}
|
||||
}
|
||||
|
||||
let child = parse(rules, rule, parsedRules)(value)
|
||||
|
@ -129,8 +137,8 @@ export default (rules, rule, parsedRules) => {
|
|||
...parsedRoot,
|
||||
evaluate,
|
||||
parsed: true,
|
||||
isDisabledBy: [],
|
||||
defaultUnit: parsedRoot.defaultUnit || parsedRoot.formule?.unit,
|
||||
isDisabledBy: [],
|
||||
replacedBy: []
|
||||
}
|
||||
parsedRules[rule.dottedName]['rendu non applicable'] = {
|
||||
|
@ -147,6 +155,7 @@ export default (rules, rule, parsedRules) => {
|
|||
missingVariables: mergeAllMissing(isDisabledBy)
|
||||
}
|
||||
},
|
||||
|
||||
jsx: (_nodeValue, { isDisabledBy }) => {
|
||||
return (
|
||||
isDisabledBy.length > 0 && (
|
||||
|
|
|
@ -1,6 +1,30 @@
|
|||
import { parseUnit } from 'Engine/units'
|
||||
import rawRules from 'Publicode/rules'
|
||||
import { assoc, chain, dropLast, filter, fromPairs, is, isNil, join, last, map, path, pipe, propEq, props, range, reduce, reduced, reject, split, take, toPairs, trim, when } from 'ramda'
|
||||
import {
|
||||
assoc,
|
||||
chain,
|
||||
dropLast,
|
||||
filter,
|
||||
fromPairs,
|
||||
is,
|
||||
isNil,
|
||||
join,
|
||||
last,
|
||||
map,
|
||||
path,
|
||||
pipe,
|
||||
propEq,
|
||||
props,
|
||||
range,
|
||||
reduce,
|
||||
reduced,
|
||||
reject,
|
||||
split,
|
||||
take,
|
||||
toPairs,
|
||||
trim,
|
||||
when
|
||||
} from 'ramda'
|
||||
import translations from '../locales/rules-en.yaml'
|
||||
// TODO - should be in UI, not engine
|
||||
import { capitalise0, coerceArray } from '../utils'
|
||||
|
@ -89,7 +113,7 @@ export let ruleParents = dottedName => {
|
|||
*/
|
||||
export let disambiguateRuleReference = (
|
||||
allRules,
|
||||
{ dottedName, name },
|
||||
{ dottedName },
|
||||
partialName
|
||||
) => {
|
||||
let pathPossibilities = [
|
||||
|
@ -113,9 +137,8 @@ export let disambiguateRuleReference = (
|
|||
return found.dottedName
|
||||
}
|
||||
|
||||
throw new Error(
|
||||
`OUUUUPS la référence '${partialName}' dans la règle '${name}' est introuvable dans la base`
|
||||
)
|
||||
throw new Error(`La référence '${partialName}' est introuvable.
|
||||
Vérifiez que l'orthographe et l'espace de nom sont corrects`)
|
||||
}
|
||||
|
||||
export let collectDefaults = pipe(
|
||||
|
|
|
@ -0,0 +1,438 @@
|
|||
import {
|
||||
convertToDate,
|
||||
getDifferenceInDays,
|
||||
getDifferenceInMonths,
|
||||
getDifferenceInYears,
|
||||
getRelativeDate,
|
||||
getYear
|
||||
} from 'Engine/date'
|
||||
import { Unit } from './units'
|
||||
|
||||
export type Period<T> = {
|
||||
start: T | null
|
||||
end: T | null
|
||||
}
|
||||
|
||||
export function parsePeriod<Date>(word: string, date: Date): Period<Date> {
|
||||
const startWords = [
|
||||
'depuis',
|
||||
'depuis le',
|
||||
'depuis la',
|
||||
'à partir de',
|
||||
'à partir du',
|
||||
'du'
|
||||
]
|
||||
const endWords = [
|
||||
"jusqu'à",
|
||||
"jusqu'au",
|
||||
"jusqu'à la",
|
||||
'avant',
|
||||
'avant le',
|
||||
'avant la',
|
||||
'au'
|
||||
]
|
||||
const intervalWords = ['le', 'en']
|
||||
if (!startWords.concat(endWords, intervalWords).includes(word)) {
|
||||
throw new SyntaxError(
|
||||
`Le mot clé '${word}' n'est pas valide. Les mots clés possible sont les suivants :\n\t ${startWords.join(
|
||||
', '
|
||||
)}`
|
||||
)
|
||||
}
|
||||
if (word === 'le') {
|
||||
return {
|
||||
start: date,
|
||||
end: date
|
||||
}
|
||||
}
|
||||
if (word === 'en') {
|
||||
return { start: null, end: null }
|
||||
}
|
||||
if (startWords.includes(word)) {
|
||||
return {
|
||||
start: date,
|
||||
end: null
|
||||
}
|
||||
}
|
||||
if (endWords.includes(word)) {
|
||||
return {
|
||||
start: null,
|
||||
end: date
|
||||
}
|
||||
}
|
||||
throw new Error('Non implémenté')
|
||||
}
|
||||
|
||||
// Idée : une évaluation est un n-uple : (value, unit, missingVariable, isApplicable)
|
||||
// Une temporalEvaluation est une liste d'evaluation sur chaque période. : [(Evaluation, Period)]
|
||||
export type Evaluation<T> = T | false | null
|
||||
|
||||
export type EvaluatedNode<T> = {
|
||||
unit: Unit
|
||||
nodeValue: Evaluation<T>
|
||||
temporalValue?: Temporal<Evaluation<T>>
|
||||
explanation?: Object
|
||||
missingVariables?: Object
|
||||
}
|
||||
|
||||
export type TemporalNode<T> = Temporal<{ nodeValue: Evaluation<T> }>
|
||||
export type Temporal<T> = Array<Period<string> & { value: T }>
|
||||
|
||||
export function narrowTemporalValue<T>(
|
||||
period: Period<string>,
|
||||
temporalValue: Temporal<Evaluation<T>>
|
||||
): Temporal<Evaluation<T>> {
|
||||
return liftTemporal2(
|
||||
(value, filter) => filter && value,
|
||||
temporalValue,
|
||||
createTemporalEvaluation(true, period)
|
||||
)
|
||||
}
|
||||
|
||||
// Returns a temporal value that's true for the given period and false otherwise.
|
||||
export function createTemporalEvaluation<T>(
|
||||
value: Evaluation<T>,
|
||||
period: Period<string> = { start: null, end: null }
|
||||
): Temporal<Evaluation<T>> {
|
||||
let temporalValue = [{ ...period, value }]
|
||||
if (period.start != null) {
|
||||
temporalValue.unshift({
|
||||
start: null,
|
||||
end: getRelativeDate(period.start, -1),
|
||||
value: false
|
||||
})
|
||||
}
|
||||
if (period.end != null) {
|
||||
temporalValue.push({
|
||||
start: getRelativeDate(period.end, 1),
|
||||
end: null,
|
||||
value: false
|
||||
})
|
||||
}
|
||||
return temporalValue
|
||||
}
|
||||
|
||||
export function pureTemporal<T>(value: T): Temporal<T> {
|
||||
return [{ start: null, end: null, value }]
|
||||
}
|
||||
|
||||
export function mapTemporal<T1, T2>(
|
||||
fn: (value: T1) => T2,
|
||||
temporalValue: Temporal<T1>
|
||||
): Temporal<T2> {
|
||||
return temporalValue.map(({ start, end, value }) => ({
|
||||
start,
|
||||
end,
|
||||
value: fn(value)
|
||||
}))
|
||||
}
|
||||
export function sometime<T1>(
|
||||
fn: (value: T1) => boolean,
|
||||
temporalValue: Temporal<T1>
|
||||
): boolean {
|
||||
return temporalValue.some(({ start, end, value }) => fn(value))
|
||||
}
|
||||
|
||||
export function liftTemporal2<T1, T2, T3>(
|
||||
fn: (value1: T1, value2: T2) => T3,
|
||||
temporalValue1: Temporal<T1>,
|
||||
temporalValue2: Temporal<T2>
|
||||
): Temporal<T3> {
|
||||
return mapTemporal(
|
||||
([a, b]) => fn(a, b),
|
||||
zipTemporals(temporalValue1, temporalValue2)
|
||||
)
|
||||
}
|
||||
|
||||
export function concatTemporals<T, U>(
|
||||
temporalValues: Array<Temporal<T>>
|
||||
): Temporal<Array<T>> {
|
||||
return temporalValues.reduce(
|
||||
(values, value) => liftTemporal2((a, b) => [...a, b], values, value),
|
||||
pureTemporal([]) as Temporal<Array<T>>
|
||||
)
|
||||
}
|
||||
|
||||
export function liftTemporalNode<T>(node: EvaluatedNode<T>): TemporalNode<T> {
|
||||
const { temporalValue, ...baseNode } = node
|
||||
if (!temporalValue) {
|
||||
return pureTemporal(baseNode)
|
||||
}
|
||||
return mapTemporal(
|
||||
nodeValue => ({
|
||||
...baseNode,
|
||||
nodeValue
|
||||
}),
|
||||
temporalValue
|
||||
)
|
||||
}
|
||||
|
||||
export function zipTemporals<T1, T2>(
|
||||
temporalValue1: Temporal<T1>,
|
||||
temporalValue2: Temporal<T2>,
|
||||
acc: Temporal<[T1, T2]> = []
|
||||
): Temporal<[T1, T2]> {
|
||||
if (!temporalValue1.length && !temporalValue2.length) {
|
||||
return acc
|
||||
}
|
||||
const [value1, ...rest1] = temporalValue1
|
||||
const [value2, ...rest2] = temporalValue2
|
||||
console.assert(value1.start === value2.start)
|
||||
const endDateComparison = compareEndDate(value1.end, value2.end)
|
||||
|
||||
// End dates are equals
|
||||
if (endDateComparison === 0) {
|
||||
return zipTemporals(rest1, rest2, [
|
||||
...acc,
|
||||
{ ...value1, value: [value1.value, value2.value] }
|
||||
])
|
||||
}
|
||||
// Value1 lasts longuer than value1
|
||||
if (endDateComparison > 0) {
|
||||
console.assert(value2.end !== null)
|
||||
return zipTemporals(
|
||||
[
|
||||
{ ...value1, start: getRelativeDate(value2.end as string, 1) },
|
||||
...rest1
|
||||
],
|
||||
rest2,
|
||||
[
|
||||
...acc,
|
||||
{
|
||||
...value2,
|
||||
value: [value1.value, value2.value]
|
||||
}
|
||||
]
|
||||
)
|
||||
}
|
||||
|
||||
// Value2 lasts longuer than value1
|
||||
if (endDateComparison < 0) {
|
||||
console.assert(value1.end !== null)
|
||||
return zipTemporals(
|
||||
rest1,
|
||||
[
|
||||
{ ...value2, start: getRelativeDate(value1.end as string, 1) },
|
||||
...rest2
|
||||
],
|
||||
[
|
||||
...acc,
|
||||
{
|
||||
...value1,
|
||||
value: [value1.value, value2.value]
|
||||
}
|
||||
]
|
||||
)
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
function compareStartDate(
|
||||
dateA: string | null,
|
||||
dateB: string | null
|
||||
): -1 | 0 | 1 {
|
||||
if (dateA == dateB) {
|
||||
return 0
|
||||
}
|
||||
if (dateA == null) {
|
||||
return -1
|
||||
}
|
||||
if (dateB == null) {
|
||||
return 1
|
||||
}
|
||||
return convertToDate(dateA) < convertToDate(dateB) ? -1 : 1
|
||||
}
|
||||
|
||||
function compareEndDate(
|
||||
dateA: string | null,
|
||||
dateB: string | null
|
||||
): -1 | 0 | 1 {
|
||||
if (dateA == dateB) {
|
||||
return 0
|
||||
}
|
||||
if (dateA == null) {
|
||||
return 1
|
||||
}
|
||||
if (dateB == null) {
|
||||
return -1
|
||||
}
|
||||
return convertToDate(dateA) < convertToDate(dateB) ? -1 : 1
|
||||
}
|
||||
|
||||
export function temporalAverage(
|
||||
temporalValue: Temporal<Evaluation<number>>,
|
||||
unit?: Unit
|
||||
): Evaluation<number> {
|
||||
temporalValue = temporalValue.filter(({ value }) => value !== false)
|
||||
if (!temporalValue.length) {
|
||||
return false
|
||||
}
|
||||
if (temporalValue.length === 1) {
|
||||
return temporalValue[0].value
|
||||
}
|
||||
|
||||
if (temporalValue.some(({ value }) => value == null)) {
|
||||
return null
|
||||
}
|
||||
|
||||
const temporalNumber = temporalValue as Temporal<number>
|
||||
const first = temporalNumber[0]
|
||||
const last = temporalNumber[temporalNumber.length - 1]
|
||||
|
||||
// La variable est définie sur un interval infini
|
||||
if (first.start == null || last.end == null) {
|
||||
if (first.start != null) {
|
||||
return last.value
|
||||
}
|
||||
if (last.end != null) {
|
||||
return first.value
|
||||
}
|
||||
return (first.value + last.value) / 2
|
||||
}
|
||||
|
||||
let totalWeight = 0
|
||||
const weights = temporalNumber.map(({ start, end, value }) => {
|
||||
;[start, end] = [start, end] as [string, string]
|
||||
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 * weight
|
||||
})
|
||||
return weights.reduce(
|
||||
(average, weightedValue) => average + weightedValue / totalWeight,
|
||||
0
|
||||
)
|
||||
}
|
||||
|
||||
export function temporalCumul(
|
||||
temporalValue: Temporal<Evaluation<number>>,
|
||||
unit: Unit
|
||||
): Evaluation<number> {
|
||||
temporalValue = temporalValue.filter(({ value }) => value !== false)
|
||||
if (!temporalValue.length) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (temporalValue.some(({ value }) => value == null)) {
|
||||
return null
|
||||
}
|
||||
|
||||
const temporalNumber = temporalValue as Temporal<number>
|
||||
const first = temporalNumber[0]
|
||||
const last = temporalNumber[temporalNumber.length - 1]
|
||||
|
||||
// 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 (temporalNumber.some(({ value }) => value == null)) {
|
||||
return null
|
||||
}
|
||||
|
||||
return temporalNumber.reduce((acc, { start, end, value }) => {
|
||||
;[start, end] = [start, end] as [string, string]
|
||||
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)
|
||||
}
|
|
@ -12,6 +12,7 @@ import {
|
|||
without
|
||||
} from 'ramda'
|
||||
import i18n from '../i18n'
|
||||
import { Evaluation } from './temporal'
|
||||
|
||||
type BaseUnit = string
|
||||
|
||||
|
@ -186,7 +187,13 @@ export function convertUnit(
|
|||
from: Unit | undefined,
|
||||
to: Unit | undefined,
|
||||
value: number
|
||||
) {
|
||||
): number
|
||||
export function convertUnit(
|
||||
from: Unit | undefined,
|
||||
to: Unit | undefined,
|
||||
value: Evaluation<number>
|
||||
): Evaluation<number>
|
||||
export function convertUnit(from, to, value): any {
|
||||
if (!areUnitConvertible(from, to)) {
|
||||
throw new Error(
|
||||
`Impossible de convertir l'unité '${serializeUnit(
|
||||
|
|
|
@ -4207,8 +4207,39 @@ dirigeant . indépendant . revenu net de cotisations:
|
|||
titre.en: net contribution income
|
||||
titre.fr: revenu net de cotisations
|
||||
dirigeant . indépendant . revenu professionnel:
|
||||
titre.en: '[automatic] occupational income'
|
||||
titre.fr: revenu professionnel
|
||||
description.en: >
|
||||
[automatic] This is the net deductible contribution income of the
|
||||
self-employed person, which is used as the basis for the calculation of
|
||||
contributions and tax for self-employed persons.
|
||||
|
||||
|
||||
Attention, **our calculation is made at cruising speed**:
|
||||
|
||||
the self-employed person who starts out will pay a relatively small package
|
||||
of social security contributions for the first 2 years. He will then have to
|
||||
regularise this situation in relation to the income he has actually
|
||||
received.
|
||||
|
||||
|
||||
Therefore, this calculation should be seen as "the amount that will have to
|
||||
be paid* in the short term after 2 years of exercise.
|
||||
description.fr: >
|
||||
C'est le revenu net de cotisations déductibles du travailleur indépendant,
|
||||
qui sert de base au calcul des cotisations et de l'impôt pour les
|
||||
indépendants.
|
||||
|
||||
|
||||
Attention, **notre calcul est fait au régime de croisière**:
|
||||
|
||||
l'indépendant qui se lance paiera pendant ses 2 premières années un forfait
|
||||
relativement réduit de cotisations sociales. Il devra ensuite régulariser
|
||||
cette situation par rapport au revenu qu'il a vraiment perçu.
|
||||
|
||||
|
||||
Il faut donc voir ce calcul comme *le montant qui devra de toute façon être
|
||||
payé* à court terme après 2 ans d'exercice.
|
||||
titre.en: '[automatic] professional income (net taxable)'
|
||||
titre.fr: revenu professionnel (net imposable)
|
||||
dirigeant . indépendant . revenus étrangers:
|
||||
description.en: >
|
||||
[automatic] Foreign income is income declared by self-employed persons in
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { Action } from 'Actions/actions'
|
||||
import { Analysis } from 'Engine/traverse'
|
||||
import { areUnitConvertible, convertUnit, parseUnit, Unit } from 'Engine/units'
|
||||
import { Unit } from 'Engine/units'
|
||||
import { defaultTo, identity, omit, without } from 'ramda'
|
||||
import reduceReducers from 'reduce-reducers'
|
||||
import { combineReducers, Reducer } from 'redux'
|
||||
|
@ -8,6 +8,7 @@ import { analysisWithDefaultsSelector } from 'Selectors/analyseSelectors'
|
|||
import { SavedSimulation } from 'Selectors/storageSelectors'
|
||||
import { DottedName } from 'Types/rule'
|
||||
import i18n, { AvailableLangs } from '../i18n'
|
||||
import { areUnitConvertible, convertUnit, parseUnit } from './../engine/units'
|
||||
import inFranceAppReducer, { Company } from './inFranceAppReducer'
|
||||
import storageRootReducer from './storageReducer'
|
||||
|
||||
|
@ -37,7 +38,7 @@ type Example = null | {
|
|||
name: string
|
||||
situation: object
|
||||
dottedName: DottedName
|
||||
defaultUnits?: Array<Unit>
|
||||
defaultUnit?: Unit
|
||||
}
|
||||
|
||||
function currentExample(state: Example = null, action: Action): Example {
|
||||
|
@ -155,7 +156,7 @@ export type SimulationConfig = Partial<{
|
|||
bloquant: Array<DottedName>
|
||||
situation: Simulation['situation']
|
||||
branches: Array<{ nom: string; situation: SimulationConfig['situation'] }>
|
||||
'unités par défaut': [string]
|
||||
'unité par défaut': string
|
||||
}>
|
||||
|
||||
type Situation = Partial<Record<DottedName, any>>
|
||||
|
@ -165,7 +166,7 @@ export type Simulation = {
|
|||
hiddenControls: Array<string>
|
||||
situation: Situation
|
||||
initialSituation: Situation
|
||||
defaultUnits: [string]
|
||||
defaultUnit: string
|
||||
foldedSteps: Array<DottedName>
|
||||
unfoldedStep?: DottedName | null
|
||||
}
|
||||
|
@ -203,7 +204,7 @@ function simulation(
|
|||
hiddenControls: [],
|
||||
situation: companySituation,
|
||||
initialSituation: companySituation,
|
||||
defaultUnits: config['unités par défaut'] || ['€/mois'],
|
||||
defaultUnit: config['unité par défaut'] || '€/mois',
|
||||
foldedSteps: Object.keys(companySituation) as Array<DottedName>,
|
||||
unfoldedStep: null
|
||||
}
|
||||
|
@ -251,11 +252,11 @@ function simulation(
|
|||
case 'UPDATE_DEFAULT_UNIT':
|
||||
return {
|
||||
...state,
|
||||
defaultUnits: [action.defaultUnit],
|
||||
situation: updateDefaultUnit(state.situation, {
|
||||
toUnit: action.defaultUnit,
|
||||
analysis
|
||||
})
|
||||
}),
|
||||
defaultUnit: action.defaultUnit
|
||||
}
|
||||
}
|
||||
return state
|
||||
|
|
|
@ -110,8 +110,8 @@ let validatedStepsSelector = createSelector(
|
|||
[state => state.simulation?.foldedSteps, targetNamesSelector],
|
||||
(foldedSteps, targetNames) => [...(foldedSteps || []), ...targetNames]
|
||||
)
|
||||
export const defaultUnitsSelector = (state: RootState) =>
|
||||
state.simulation?.defaultUnits || []
|
||||
export const defaultUnitSelector = (state: RootState) =>
|
||||
state.simulation?.defaultUnit ?? '€/mois'
|
||||
let branchesSelector = (state: RootState) => configSelector(state).branches
|
||||
let configSituationSelector = (state: RootState) =>
|
||||
configSelector(state).situation || {}
|
||||
|
@ -172,9 +172,9 @@ export let ruleAnalysisSelector = createSelector(
|
|||
(_, props: { dottedName: DottedName }) => props.dottedName,
|
||||
situationsWithDefaultsSelector,
|
||||
state => state.situationBranch || 0,
|
||||
defaultUnitsSelector
|
||||
defaultUnitSelector
|
||||
],
|
||||
(rules, dottedName, situations, situationBranch, defaultUnits) => {
|
||||
(rules, dottedName, situations, situationBranch, defaultUnit) => {
|
||||
return analyseRule(
|
||||
rules,
|
||||
dottedName,
|
||||
|
@ -184,7 +184,7 @@ export let ruleAnalysisSelector = createSelector(
|
|||
: situations
|
||||
return currentSituation[dottedName]
|
||||
},
|
||||
defaultUnits
|
||||
[defaultUnit]
|
||||
)
|
||||
}
|
||||
)
|
||||
|
@ -217,7 +217,7 @@ export let exampleAnalysisSelector = createSelector(
|
|||
rules,
|
||||
dottedName,
|
||||
(dottedName: DottedName) => situation[dottedName],
|
||||
example?.defaultUnits
|
||||
example?.defaultUnit
|
||||
)
|
||||
)
|
||||
|
||||
|
@ -227,18 +227,16 @@ let makeAnalysisSelector = (situationSelector: SituationSelectorType) =>
|
|||
parsedRulesSelector,
|
||||
targetNamesSelector,
|
||||
situationSelector,
|
||||
defaultUnitsSelector
|
||||
defaultUnitSelector
|
||||
],
|
||||
(parsedRules, targetNames, situations, defaultUnits) => {
|
||||
(parsedRules, targetNames, situations, defaultUnit) => {
|
||||
return mapOrApply(
|
||||
situation =>
|
||||
analyseMany(
|
||||
parsedRules,
|
||||
targetNames,
|
||||
defaultUnits
|
||||
)((dottedName: DottedName) => {
|
||||
return situation[dottedName]
|
||||
}),
|
||||
analyseMany(parsedRules, targetNames, [defaultUnit])(
|
||||
(dottedName: DottedName) => {
|
||||
return situation[dottedName]
|
||||
}
|
||||
),
|
||||
situations
|
||||
)
|
||||
}
|
||||
|
|
|
@ -12,7 +12,6 @@ export function AideDéclarationIndépendantsRécapitulatif() {
|
|||
const siren = useSelector(
|
||||
(state: RootState) => state.inFranceApp.existingCompany?.siren
|
||||
)
|
||||
console.log(useSelector((state: RootState) => state.rules))
|
||||
const componentRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
return (
|
||||
|
|
|
@ -55,7 +55,6 @@ describe('bug-analyse-many', function() {
|
|||
|
||||
const one = analyse(rules, 'cotisations')(stateSelector).targets[0]
|
||||
|
||||
//console.log(many[0].nodeValue, many[1].nodeValue, one.nodeValue)
|
||||
expect(many[1].nodeValue).to.be.closeTo(one.nodeValue, 0.1)
|
||||
})
|
||||
it('should compute the same contributions if asked with analyseMany or analyse', function() {
|
||||
|
|
|
@ -8,7 +8,7 @@ import {
|
|||
nextStepsSelector
|
||||
} from '../source/selectors/analyseSelectors'
|
||||
let baseState = {
|
||||
simulation: { situation: {}, foldedSteps: [] }
|
||||
simulation: { defaultUnit: '€/an', situation: {}, foldedSteps: [] }
|
||||
}
|
||||
|
||||
describe('conversation', function() {
|
||||
|
@ -24,7 +24,11 @@ describe('conversation', function() {
|
|||
rules = rawRules.map(enrichRule),
|
||||
state = merge(baseState, {
|
||||
rules,
|
||||
simulation: { config: { objectifs: ['startHere'] }, foldedSteps: [] }
|
||||
simulation: {
|
||||
defaultUnit: '€/an',
|
||||
config: { objectifs: ['startHere'] },
|
||||
foldedSteps: []
|
||||
}
|
||||
}),
|
||||
currentQuestion = currentQuestionSelector(state)
|
||||
|
||||
|
@ -48,7 +52,11 @@ describe('conversation', function() {
|
|||
|
||||
let step1 = merge(baseState, {
|
||||
rules,
|
||||
simulation: { config: { objectifs: ['startHere'] }, foldedSteps: [] }
|
||||
simulation: {
|
||||
defaultUnit: '€/an',
|
||||
config: { objectifs: ['startHere'] },
|
||||
foldedSteps: []
|
||||
}
|
||||
})
|
||||
let step2 = reducers(
|
||||
assocPath(['simulation', 'situation'], { 'top . aa': '1' }, step1),
|
||||
|
@ -125,7 +133,11 @@ describe('conversation', function() {
|
|||
|
||||
let step1 = merge(baseState, {
|
||||
rules,
|
||||
simulation: { config: { objectifs: ['net'] }, foldedSteps: [] }
|
||||
simulation: {
|
||||
defaultUnit: '€/an',
|
||||
config: { objectifs: ['net'] },
|
||||
foldedSteps: []
|
||||
}
|
||||
})
|
||||
expect(currentQuestionSelector(step1)).to.equal('brut')
|
||||
|
||||
|
@ -148,7 +160,11 @@ describe('real conversation', function() {
|
|||
it('should not have more than X questions', function() {
|
||||
let state = merge(baseState, {
|
||||
rules,
|
||||
simulation: { config: salariéConfig, foldedSteps: [] }
|
||||
simulation: {
|
||||
defaultUnit: '€/an',
|
||||
config: salariéConfig,
|
||||
foldedSteps: []
|
||||
}
|
||||
}),
|
||||
nextSteps = nextStepsSelector(state)
|
||||
|
||||
|
|
|
@ -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
|
||||
)
|
||||
})
|
||||
})
|
|
@ -10,6 +10,7 @@ import {
|
|||
let state = {
|
||||
rules,
|
||||
simulation: {
|
||||
defaultUnit: '€/mois',
|
||||
config: salariéConfig,
|
||||
situation: {
|
||||
'contrat salarié . rémunération . brut de base': '2300',
|
||||
|
|
|
@ -237,8 +237,6 @@ describe('nextSteps', function() {
|
|||
analysis = analyse(rules, 'sum')(stateSelector),
|
||||
result = collectMissingVariables(analysis.targets)
|
||||
|
||||
// console.log('analysis', JSON.stringify(analysis, null, 4))
|
||||
|
||||
expect(result).to.have.lengthOf(1)
|
||||
expect(result[0]).to.equal('top . sum . evt')
|
||||
})
|
||||
|
|
|
@ -88,13 +88,13 @@ impôt sur le revenu:
|
|||
assiette: revenu abattu
|
||||
tranches:
|
||||
- taux: 0%
|
||||
plafond: 9807
|
||||
plafond: 9807 €
|
||||
- taux: 14%
|
||||
plafond: 27086
|
||||
plafond: 27086 €
|
||||
- taux: 30%
|
||||
plafond: 72617
|
||||
plafond: 72617 €
|
||||
- taux: 41%
|
||||
plafond: 153783
|
||||
plafond: 153783 €
|
||||
- taux: 45%
|
||||
|
||||
impôt sur le revenu à payer:
|
||||
|
|
|
@ -31,3 +31,35 @@ Grille:
|
|||
situation:
|
||||
assiette: 999.3
|
||||
valeur attendue: 50
|
||||
|
||||
plafond:
|
||||
unité: €
|
||||
Grille avec valeur manquante:
|
||||
formule:
|
||||
grille:
|
||||
assiette: assiette
|
||||
unité: €
|
||||
tranches:
|
||||
- montant: 100
|
||||
plafond: plafond
|
||||
- montant: 200
|
||||
plafond: 2000 €
|
||||
- montant: 300
|
||||
plafond: 4000 €
|
||||
|
||||
unité attendue: €
|
||||
exemples:
|
||||
- nom: 'variable manquante'
|
||||
situation:
|
||||
assiette: 1000
|
||||
variables manquantes:
|
||||
- plafond
|
||||
valeur attendue: null
|
||||
- nom: 'assiette non concernée par variable manquante'
|
||||
situation:
|
||||
assiette: 3000
|
||||
valeur attendue: 300
|
||||
- nom: 'assiette au delà du plagond'
|
||||
situation:
|
||||
assiette: 5000
|
||||
valeur attendue: false
|
||||
|
|
|
@ -0,0 +1,93 @@
|
|||
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:
|
||||
unité: €/mois
|
||||
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
|
||||
|
||||
# ======================
|
||||
# Exemple plus complexe
|
||||
# ======================
|
||||
|
||||
heures d'absences:
|
||||
# TODO : mettre les heures chaque jour
|
||||
formule:
|
||||
somme:
|
||||
- 2 heures/mois | du 01/01/2020 | au 31/01/2020
|
||||
- 3 heures/mois | du 01/03/2020 | au 31/03/2020
|
||||
|
||||
temps contractuel:
|
||||
formule: 145 heures/mois
|
||||
|
||||
temps de travail effectif:
|
||||
formule: temps contractuel - heures d'absences
|
||||
|
||||
plafond sécurité sociale proratisé:
|
||||
formule:
|
||||
multiplication:
|
||||
assiette: plafond sécurité sociale
|
||||
facteur: temps de travail effectif / 151.67 heures/mois
|
||||
|
||||
taux variable:
|
||||
formule:
|
||||
variations:
|
||||
- si: salaire < plafond sécurité sociale proratisé
|
||||
alors: 10%
|
||||
- sinon: 20%
|
||||
|
||||
cotisation spéciale:
|
||||
unité: €/mois
|
||||
formule:
|
||||
régularisation:
|
||||
règle:
|
||||
multiplication:
|
||||
assiette: salaire
|
||||
taux: taux variable
|
||||
valeurs cumulées:
|
||||
- salaire
|
||||
- plafond sécurité sociale proratisé
|
||||
|
||||
régularisation . test variations 1:
|
||||
formule: cotisation spéciale | du 01/01/2020 | au 31/12/2020
|
||||
exemples:
|
||||
- valeur attendue: 710
|
||||
|
||||
régularisation . test variations 2:
|
||||
formule: cotisation spéciale | du 01/02/2020 | au 29/02/2020
|
||||
exemples:
|
||||
- valeur attendue: 660
|
|
@ -0,0 +1,251 @@
|
|||
variable temporelle numérique . le . valeur:
|
||||
formule: 40 €/mois | le 02/04/2019
|
||||
|
||||
variable temporelle numérique . le . test date applicable:
|
||||
formule: valeur | le 02/04/2019
|
||||
exemples:
|
||||
- valeur attendue: 40
|
||||
|
||||
variable temporelle numérique . le . test date non applicable:
|
||||
formule: valeur | le 02/03/2021
|
||||
exemples:
|
||||
- valeur attendue: false
|
||||
|
||||
variable temporelle numérique . depuis . valeur:
|
||||
formule: 40 €/mois | depuis le 02/04/2019
|
||||
|
||||
variable temporelle numérique . depuis . test date applicable:
|
||||
formule: valeur | depuis le 06/04/2019
|
||||
exemples:
|
||||
- valeur attendue: 40
|
||||
|
||||
variable temporelle numérique . depuis . test date non applicable:
|
||||
formule: valeur | le 08/03/2019
|
||||
exemples:
|
||||
- valeur attendue: false
|
||||
|
||||
variable temporelle numérique . intervalle . valeur:
|
||||
formule: 40 €/mois | du 02/04/2019 | au 04/05/2020
|
||||
|
||||
variable temporelle numérique . intervalle . test date applicable:
|
||||
formule: valeur | le 06/04/2019
|
||||
exemples:
|
||||
- valeur attendue: 40
|
||||
|
||||
variable temporelle numérique . intervalle . test date applicable 2:
|
||||
formule: valeur | depuis le 05/06/2019 | jusqu'au 19/04/2020
|
||||
exemples:
|
||||
- valeur attendue: 40
|
||||
|
||||
variable temporelle numérique . intervalle . test date non applicable:
|
||||
formule: valeur | le 08/03/2021
|
||||
exemples:
|
||||
- valeur attendue: false
|
||||
|
||||
variable temporelle numérique . intervalle . test date non applicable 2:
|
||||
formule: valeur | le 28/01/2019
|
||||
exemples:
|
||||
- valeur attendue: false
|
||||
|
||||
variable temporelle numérique . variable . date limite de paiement:
|
||||
formule: 03/09/2020
|
||||
|
||||
variable temporelle numérique . variable . majorations de retard:
|
||||
formule: '40 €/jour | à partir de : date limite de paiement'
|
||||
|
||||
variable temporelle numérique . variable . test date non applicable:
|
||||
formule: "majorations de retard | jusqu'au : 02/09/2020"
|
||||
exemples:
|
||||
- valeur attendue: false
|
||||
|
||||
variable temporelle numérique . variable . test date non applicable 2:
|
||||
formule: majorations de retard | du 01/02/2020 | au 03/08/2020
|
||||
exemples:
|
||||
- valeur attendue: false
|
||||
|
||||
variable temporelle numérique . variable . test date applicable:
|
||||
formule: 'majorations de retard | depuis la : date limite de paiement'
|
||||
exemples:
|
||||
- valeur attendue: 40
|
||||
|
||||
variable temporelle numérique . variable . test date applicable 2:
|
||||
formule: majorations de retard | le 03/09/2020
|
||||
exemples:
|
||||
- valeur attendue: 40
|
||||
|
||||
prix:
|
||||
formule: (20 €/mois | à partir du 15/11/2019) + (10 €/mois | à partir du 01/02/2020)
|
||||
|
||||
date:
|
||||
variable temporelle numérique . test addition:
|
||||
formule: 'prix | le : date'
|
||||
exemples:
|
||||
- situation:
|
||||
date: 01/01/2019
|
||||
valeur attendue: false
|
||||
- situation:
|
||||
date: 15/12/2019
|
||||
valeur attendue: 20
|
||||
- situation:
|
||||
date: 12/09/2020
|
||||
valeur attendue: 30
|
||||
|
||||
prix avec variations:
|
||||
formule: prix * (50% | du 01/01/2020 | au 31/01/2020)
|
||||
|
||||
début:
|
||||
fin:
|
||||
variable temporelle numérique . expression . multiplication:
|
||||
formule: "prix avec variations | depuis : début | jusqu'à : fin"
|
||||
# 20 [avant janvier] / 10 [pendant janvier] | 30 [pendant et après février]
|
||||
exemples:
|
||||
- situation:
|
||||
début: 01/01/2020
|
||||
fin: 31/01/2020
|
||||
valeur attendue: 10
|
||||
- situation:
|
||||
début: 01/01/2020
|
||||
fin: 29/02/2020
|
||||
valeur attendue: 20
|
||||
- situation:
|
||||
début: 01/02/2020
|
||||
fin: 31/03/2020
|
||||
valeur attendue: 30
|
||||
|
||||
taux associé:
|
||||
formule:
|
||||
variations:
|
||||
- si: prix avec variations >= 20 €/mois
|
||||
alors: 10%/mois
|
||||
- si: prix avec variations < 20 €/mois
|
||||
alors: 60%/mois
|
||||
# Cette formule peut paraître bizarre, mais lorsque multiplication est non
|
||||
# applicable, c'est bien le sinon qui s'applique
|
||||
- sinon: 5%/mois
|
||||
variable temporelle numérique . variation:
|
||||
formule: "taux associé | depuis : début | jusqu'à : fin"
|
||||
exemples:
|
||||
- situation:
|
||||
début: 01/01/2020
|
||||
fin: 31/01/2020
|
||||
valeur attendue: 60
|
||||
- situation:
|
||||
début: 01/01/2020
|
||||
fin: 29/02/2020
|
||||
valeur attendue: 35
|
||||
- situation:
|
||||
début: 01/02/2020
|
||||
fin: 31/03/2020
|
||||
valeur attendue: 10
|
||||
- situation:
|
||||
début: 01/10/2019
|
||||
fin: 30/10/2019
|
||||
valeur attendue: 5
|
||||
|
||||
contrat salarié . date d'embauche:
|
||||
formule: 12/09/2018
|
||||
|
||||
contrat salarié . salaire:
|
||||
formule:
|
||||
somme:
|
||||
- brut de base
|
||||
- primes
|
||||
|
||||
contrat salarié . salaire . brut de base:
|
||||
formule:
|
||||
somme:
|
||||
- "2000€/mois | depuis : date d'embauche | jusqu'au 08/08/2019"
|
||||
- 2200€/mois | depuis le 09/08/2019
|
||||
|
||||
contrat salarié . salaire . primes:
|
||||
formule: 2000€/mois | du 01/12/2019 | au 31/12/2019
|
||||
|
||||
plafond sécurité sociale:
|
||||
formule:
|
||||
somme:
|
||||
- 3377 €/mois | du 01/01/2019 | au 31/12/2019
|
||||
- 3424 €/mois | du 01/01/2020 | au 31/12/2020
|
||||
|
||||
contrat salarié . cotisations . retraite:
|
||||
formule:
|
||||
multiplication:
|
||||
assiette: salaire
|
||||
plafond: plafond sécurité sociale
|
||||
taux: 10%
|
||||
|
||||
variable temporelle numérique . somme:
|
||||
formule: contrat salarié . salaire | du 01/12/2019 | au 31/12/2019
|
||||
exemples:
|
||||
- valeur attendue: 4200 # 2000 + 2200
|
||||
|
||||
variable temporelle numérique . somme avec valeur changeant au cours du mois:
|
||||
formule: contrat salarié . salaire | du 01/08/2019 | au 31/08/2019
|
||||
exemples:
|
||||
- valeur attendue: 2148.387 # (2000 * 8 + 2200 * 23)/31
|
||||
|
||||
variable temporelle numérique . multiplication:
|
||||
formule: contrat salarié . cotisations . retraite | du 01/05/2019 | au 31/05/2019
|
||||
exemples:
|
||||
- valeur attendue: 200 # 2000 * 10%
|
||||
|
||||
variable temporelle numérique . multiplication avec valeur changeant au cours du mois:
|
||||
formule: contrat salarié . cotisations . retraite | du 01/08/2019 | au 31/08/2019
|
||||
exemples:
|
||||
- valeur attendue: 214.839 # (2000 * 8 + 2200 * 23)/31
|
||||
|
||||
variable temporelle numérique . multiplication avec valeur au dessus du plafond:
|
||||
formule: contrat salarié . cotisations . retraite | du 01/12/2019 | au 31/12/2019
|
||||
exemples:
|
||||
- valeur attendue: 337.7 # (2000 * 8 + 2200 * 23)/31
|
||||
|
||||
variable temporelle numérique . multiplication avec valeur sur l'année:
|
||||
formule: contrat salarié . cotisations . retraite | du 01/01/2019 | au 31/12/2019
|
||||
exemples:
|
||||
# 200 * 7 [janvier-juin]
|
||||
# + 214.839 [juillet]
|
||||
# + 220 * 3 [aout-novembre]
|
||||
# + 337.7 [décembre]
|
||||
# /12 mois
|
||||
- valeur attendue: 217.7115
|
||||
# test . proratisation du salaire avec entrée en cours de mois:
|
||||
# formule: salaire brut [avril 2019]
|
||||
# exemples:
|
||||
# - valeur attendue: 400 # (2000 * 6)/30
|
||||
cotisation spéciale:
|
||||
formule:
|
||||
barème:
|
||||
assiette: contrat salarié . salaire
|
||||
multiplicateur: plafond sécurité sociale
|
||||
tranches:
|
||||
- taux: 0%
|
||||
plafond: 10%
|
||||
- taux: 10%
|
||||
plafond: 20%
|
||||
- taux: 30%
|
||||
plafond: 50%
|
||||
- taux: 40%
|
||||
plafond: 100%
|
||||
- taux: 50%
|
||||
|
||||
variable temporelle numérique . barème:
|
||||
formule: cotisation spéciale | du 01/01/2019 | au 31/12/2019
|
||||
exemples:
|
||||
- valeur attendue: 567.438
|
||||
|
||||
grille:
|
||||
formule:
|
||||
barème:
|
||||
assiette: contrat salarié . salaire
|
||||
tranches:
|
||||
- montant: 5 heures
|
||||
plafond: 1000€
|
||||
- montant: 10 heures
|
||||
plafond: 2000 €
|
||||
- montant: 30 heures
|
||||
plafond: 4000 €
|
||||
- montant: 40 heures
|
||||
|
||||
variable temporelle numérique . grille:
|
||||
formule: cotisation spéciale | du 01/01/2019 | au 31/12/2019
|
||||
exemples:
|
||||
- valeur attendue: 567.438
|
|
@ -0,0 +1,132 @@
|
|||
import { expect } from 'chai'
|
||||
import {
|
||||
concatTemporals,
|
||||
createTemporalEvaluation,
|
||||
groupByYear,
|
||||
zipTemporals
|
||||
} from '../source/engine/temporal'
|
||||
|
||||
const neverEnding = value => [{ start: null, end: null, value: value }]
|
||||
describe('Periods : zip', () => {
|
||||
it('should zip two empty temporalValue', () => {
|
||||
const result = zipTemporals([], [])
|
||||
expect(result).to.deep.equal([])
|
||||
})
|
||||
|
||||
it('should zip constant temporalValue', () => {
|
||||
const result = zipTemporals(neverEnding(1), neverEnding(2))
|
||||
expect(result).to.deep.equal(neverEnding([1, 2]))
|
||||
})
|
||||
|
||||
it('should zip changing temporalValue', () => {
|
||||
const value1 = createTemporalEvaluation(true, {
|
||||
start: null,
|
||||
end: '01/08/2020'
|
||||
})
|
||||
const value2 = neverEnding(1)
|
||||
expect(zipTemporals(value1, value2)).to.deep.equal([
|
||||
{ start: null, end: '01/08/2020', value: [true, 1] },
|
||||
{ start: '02/08/2020', end: null, value: [false, 1] }
|
||||
])
|
||||
expect(zipTemporals(value2, value1)).to.deep.equal([
|
||||
{ start: null, end: '01/08/2020', value: [1, true] },
|
||||
{ start: '02/08/2020', end: null, value: [1, false] }
|
||||
])
|
||||
})
|
||||
|
||||
it('should zip two overlapping temporalValue', () => {
|
||||
const value1 = createTemporalEvaluation(1, {
|
||||
start: '01/07/2019',
|
||||
end: '30/06/2020'
|
||||
})
|
||||
const value2 = createTemporalEvaluation(2, {
|
||||
start: '01/01/2019',
|
||||
end: '31/12/2019'
|
||||
})
|
||||
|
||||
expect(zipTemporals(value1, value2)).to.deep.equal([
|
||||
{ start: null, end: '31/12/2018', value: [false, false] },
|
||||
{ start: '01/01/2019', end: '30/06/2019', value: [false, 2] },
|
||||
{ start: '01/07/2019', end: '31/12/2019', value: [1, 2] },
|
||||
{ start: '01/01/2020', end: '30/06/2020', value: [1, false] },
|
||||
{ start: '01/07/2020', end: null, value: [false, false] }
|
||||
])
|
||||
})
|
||||
})
|
||||
|
||||
describe('Periods : concat', () => {
|
||||
it('should merge concat overlapping temporalValue', () => {
|
||||
const value1 = createTemporalEvaluation(10)
|
||||
const value2 = [
|
||||
{ start: null, end: '14/04/2019', value: 100 },
|
||||
{ start: '15/04/2019', end: '08/08/2019', value: 2000 },
|
||||
{ start: '09/08/2019', end: null, value: 200 }
|
||||
]
|
||||
|
||||
expect(concatTemporals([value1, value2])).to.deep.equal([
|
||||
{ start: null, end: '14/04/2019', value: [10, 100] },
|
||||
{ start: '15/04/2019', end: '08/08/2019', value: [10, 2000] },
|
||||
{ start: '09/08/2019', end: null, value: [10, 200] }
|
||||
])
|
||||
})
|
||||
})
|
||||
|
||||
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)
|
||||
})
|
||||
})
|
|
@ -4,9 +4,11 @@
|
|||
|
||||
chiffre affaires:
|
||||
unité par défaut: €/mois
|
||||
par défaut: 0
|
||||
|
||||
charges:
|
||||
par défaut: 0 €/mois
|
||||
unité: €/mois
|
||||
par défaut: 0
|
||||
|
||||
répartition salaire sur dividendes:
|
||||
par défaut: 50
|
||||
|
|
|
@ -0,0 +1,132 @@
|
|||
import { expect } from 'chai'
|
||||
import {
|
||||
concatTemporals,
|
||||
createTemporalEvaluation,
|
||||
groupByYear,
|
||||
zipTemporals
|
||||
} from '../source/engine/temporal'
|
||||
|
||||
const neverEnding = value => [{ start: null, end: null, value: value }]
|
||||
describe('Periods : zip', () => {
|
||||
it('should zip two empty temporalValue', () => {
|
||||
const result = zipTemporals([], [])
|
||||
expect(result).to.deep.equal([])
|
||||
})
|
||||
|
||||
it('should zip constant temporalValue', () => {
|
||||
const result = zipTemporals(neverEnding(1), neverEnding(2))
|
||||
expect(result).to.deep.equal(neverEnding([1, 2]))
|
||||
})
|
||||
|
||||
it('should zip changing temporalValue', () => {
|
||||
const value1 = createTemporalEvaluation(true, {
|
||||
start: null,
|
||||
end: '01/08/2020'
|
||||
})
|
||||
const value2 = neverEnding(1)
|
||||
expect(zipTemporals(value1, value2)).to.deep.equal([
|
||||
{ start: null, end: '01/08/2020', value: [true, 1] },
|
||||
{ start: '02/08/2020', end: null, value: [false, 1] }
|
||||
])
|
||||
expect(zipTemporals(value2, value1)).to.deep.equal([
|
||||
{ start: null, end: '01/08/2020', value: [1, true] },
|
||||
{ start: '02/08/2020', end: null, value: [1, false] }
|
||||
])
|
||||
})
|
||||
|
||||
it('should zip two overlapping temporalValue', () => {
|
||||
const value1 = createTemporalEvaluation(1, {
|
||||
start: '01/07/2019',
|
||||
end: '30/06/2020'
|
||||
})
|
||||
const value2 = createTemporalEvaluation(2, {
|
||||
start: '01/01/2019',
|
||||
end: '31/12/2019'
|
||||
})
|
||||
|
||||
expect(zipTemporals(value1, value2)).to.deep.equal([
|
||||
{ start: null, end: '31/12/2018', value: [false, false] },
|
||||
{ start: '01/01/2019', end: '30/06/2019', value: [false, 2] },
|
||||
{ start: '01/07/2019', end: '31/12/2019', value: [1, 2] },
|
||||
{ start: '01/01/2020', end: '30/06/2020', value: [1, false] },
|
||||
{ start: '01/07/2020', end: null, value: [false, false] }
|
||||
])
|
||||
})
|
||||
})
|
||||
|
||||
describe('Periods : concat', () => {
|
||||
it('should merge concat overlapping temporalValue', () => {
|
||||
const value1 = createTemporalEvaluation(10)
|
||||
const value2 = [
|
||||
{ start: null, end: '14/04/2019', value: 100 },
|
||||
{ start: '15/04/2019', end: '08/08/2019', value: 2000 },
|
||||
{ start: '09/08/2019', end: null, value: 200 }
|
||||
]
|
||||
|
||||
expect(concatTemporals([value1, value2])).to.deep.equal([
|
||||
{ start: null, end: '14/04/2019', value: [10, 100] },
|
||||
{ start: '15/04/2019', end: '08/08/2019', value: [10, 2000] },
|
||||
{ start: '09/08/2019', end: null, value: [10, 200] }
|
||||
])
|
||||
})
|
||||
})
|
||||
|
||||
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