Merge pull request #603 from betagouv/units

Units
pull/616/head
Mael 2019-08-03 15:52:13 +02:00 committed by GitHub
commit 593febd811
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
47 changed files with 766 additions and 629 deletions

View File

@ -8,7 +8,7 @@ import type {
SetSituationBranchAction
} from 'Types/ActionsTypes'
// $FlowFixMe
import { clearFields, reset } from 'redux-form'
import { change, reset } from 'redux-form'
import { deletePersistedSimulation } from '../storage/persistSimulation'
import type { Thunk } from 'Types/ActionsTypes'
@ -25,14 +25,15 @@ export const goToQuestion = (question: string): StepAction => ({
name: 'unfold',
step: question
})
export const skipQuestion = (
question: string
export const validateStepWithValue = (
dottedName,
value: any
): Thunk<StepAction> => dispatch => {
dispatch(clearFields('conversation', false, false, question))
dispatch(change('conversation', dottedName, value))
dispatch({
type: 'STEP_ACTION',
name: 'fold',
step: question
step: dottedName
})
}

View File

@ -113,17 +113,17 @@ class Distribution extends Component<Props, State> {
<div className="distribution-chart__total">
<span />
<RuleLink {...salaireNet} />
<Value {...salaireNet} unit="€" numFractionDigits={0} />
<Value {...salaireNet} unit="€" maximumFractionDigits={0} />
<span>+</span>
<Trans>Cotisations</Trans>
<Value numFractionDigits={0} unit="€">
<Value maximumFractionDigits={0} unit="€">
{total.partPatronale + total.partSalariale}
</Value>
<span />
<div className="distribution-chart__total-border" />
<span>=</span>
<RuleLink {...salaireChargé} />
<Value {...salaireChargé} unit="€" numFractionDigits={0} />
<Value {...salaireChargé} unit="€" maximumFractionDigits={0} />
</div>
</>
)
@ -154,7 +154,7 @@ let ChartItemBar = ({ styles, colour, montant, total }) => (
margin-left: 1em;
color: var(--textColourOnWhite);
`}>
<Value numFractionDigits={0} unit="€">
<Value maximumFractionDigits={0} unit="€">
{montant}
</Value>
</div>

View File

@ -52,8 +52,8 @@ export default compose(
return
})
return (
<div id="PeriodSwitch">
<div className="base ui__ small toggle">
<span id="PeriodSwitch">
<span className="base ui__ small toggle">
<label>
<Field
name="période"
@ -82,8 +82,8 @@ export default compose(
<Trans>mois</Trans>
</span>
</label>
</div>
</div>
</span>
</span>
)
})

View File

@ -626,7 +626,7 @@ const RuleValueLink = compose(
sitePaths.documentation.index + '/' + encodeRuleName(rule.dottedName)
}>
<Value
numFractionDigits={0}
maximumFractionDigits={0}
{...rule}
unit={
/* //TODO the unit should be integrated in the leaf rules of base.yaml and infered by mecanisms. Will be done in a future release*/

View File

@ -8,12 +8,17 @@ const NumberFormat = memoizeWith(
Intl.NumberFormat
)
let numberFormatter = (style, numFractionDigits = 2) => (value, language) =>
export let numberFormatter = ({
style,
maximumFractionDigits,
minimumFractionDigits = 0,
language
}) => value =>
NumberFormat(language, {
style,
currency: 'EUR',
maximumFractionDigits: numFractionDigits,
minimumFractionDigits: numFractionDigits
maximumFractionDigits,
minimumFractionDigits
}).format(value)
// let booleanTranslations = { true: '✅', false: '❌' }
@ -34,7 +39,8 @@ export default withLanguage(
nodeValue: value,
unit,
nilValueSymbol,
numFractionDigits,
maximumFractionDigits,
minimumFractionDigits,
children,
negative,
language,
@ -63,11 +69,24 @@ export default withLanguage(
nodeValue.nom
) : valueType === 'boolean' ? (
booleanTranslations[language][nodeValue]
) : unit === '€' ? (
numberFormatter('currency', numFractionDigits)(nodeValue, language)
) : unitText === '€' ? (
numberFormatter({
style: 'currency',
maximumFractionDigits,
minimumFractionDigits,
language
})(nodeValue)
) : unitText === '%' ? (
numberFormatter({ style: 'percent', maximumFractionDigits: 3 })(
nodeValue
)
) : (
<>
{numberFormatter('decimal', numFractionDigits)(nodeValue)}
{numberFormatter({
style: 'decimal',
minimumFractionDigits,
maximumFractionDigits
})(nodeValue)}
&nbsp;
{unitText}
</>

View File

@ -1,4 +1,8 @@
import { goToQuestion, resetSimulation, skipQuestion } from 'Actions/actions'
import {
goToQuestion,
resetSimulation,
validateStepWithValue
} from 'Actions/actions'
import { T } from 'Components'
import QuickLinks from 'Components/QuickLinks'
import { getInputComponent } from 'Engine/generateQuestions'
@ -15,6 +19,7 @@ import {
import * as Animate from 'Ui/animate'
import Aide from './Aide'
import './conversation.css'
import { findRuleByDottedName } from 'Engine/rules'
export default compose(
reduxForm({
@ -28,7 +33,7 @@ export default compose(
previousAnswers: state.conversationSteps.foldedSteps,
nextSteps: nextStepsSelector(state)
}),
{ resetSimulation, skipQuestion, goToQuestion }
{ resetSimulation, validateStepWithValue, goToQuestion }
)
)(function Conversation({
nextSteps,
@ -37,14 +42,18 @@ export default compose(
customEndMessages,
flatRules,
resetSimulation,
skipQuestion,
goToQuestion
goToQuestion,
validateStepWithValue
}) {
const goToNext = () => skipQuestion(nextSteps[0])
const setDefault = () =>
validateStepWithValue(
currentQuestion,
findRuleByDottedName(flatRules, currentQuestion).defaultValue
)
const goToPrevious = () => goToQuestion(previousAnswers.slice(-1)[0])
const handleKeyDown = ({ key }) => {
if (['Escape'].includes(key)) {
goToNext()
setDefault()
}
}
return nextSteps.length ? (
@ -67,7 +76,7 @@ export default compose(
</>
)}
<button
onClick={goToNext}
onClick={setDefault}
className="ui__ simple small skip button right">
Passer
</button>

View File

@ -35,12 +35,25 @@ export var FormDecorator = formType => RenderField =>
helpVisible: false
}
render() {
let { stepAction, fieldName, inversion, setFormValue } = this.props
let {
stepAction,
fieldName,
inversion,
setFormValue,
unit
} = this.props
let submit = cause => stepAction('fold', fieldName, cause),
stepProps = {
...this.props,
submit,
setFormValue: (value, name = fieldName) => setFormValue(name, value)
setFormValue: (value, name = fieldName) =>
setFormValue(name, value),
...(unit === '%'
? {
format: x => (x == null ? null : x * 100),
normalize: x => (x == null ? null : x / 100)
}
: {})
}
return (

View File

@ -55,7 +55,6 @@ export default compose(
/>
{suffixed && (
<label className="suffix" htmlFor={'step-' + dottedName}>
{unit}
{rulePeriod && (
<span>
{' '}

View File

@ -23,24 +23,22 @@
display: inline-block;
}
.node.inlineExpression:not(.comparison):not(.negation) {
.node.inlineExpression:not(.comparison) {
padding-left: 0;
display: flex;
align-items: baseline;
flex-direction: row;
flex-wrap: wrap;
justify-content: space-between
}
.nodeContent {
margin-right: 0.3em;
}
#rule-rules
.inlineExpression:not(.comparison):not(.negation)
> .situationValue {
#rule-rules .inlineExpression:not(.comparison) > .situationValue {
margin-left: 0.3em;
margin-bottom: 0.6em;
margin-top: 0.3em;
align-items: flex-end;
flex: 1;
text-align: right;
}
@ -114,13 +112,6 @@
margin-right: 0.6em;
}
.maximum .description,
.composanteName {
font-weight: 500;
margin-bottom: 0.4em;
margin-left: 1em;
}
.leaf .situationValue {
text-align: center;
}
@ -147,10 +138,6 @@
margin-top: 0.6em;
}
.composantes .composanteName::first-letter {
text-transform: capitalize;
}
.mecanism {
border: 1px solid;
max-width: 100%;
@ -166,13 +153,8 @@
.mecanism-result {
position: absolute;
display: none;
bottom: 0;
bottom: 0px;
right: 0;
border-top: 1px solid;
border-left: 1px solid;
border-top-left-radius: 0.3rem;
border-color: inherit;
padding: 0.1rem 0.6rem;
}
#rule-rules.showValues .mecanism-result {
display: initial;
@ -198,7 +180,6 @@
}
.variable,
.nodeHead,
.operator {
display: inline-block;
}
@ -215,8 +196,7 @@
.mecanism.cond *:not(.nodeContent) > .variable .name,
.mecanism.variations *:not(.nodeContent) > .variable .name,
.inlineExpression.comparison,
.inlineExpression.negation {
.inlineExpression.comparison {
display: flex;
align-items: baseline;
}
@ -228,8 +208,7 @@
> .variable
.name
.situationValue,
.inlineExpression.comparison > .situationValue,
.inlineExpression.negation > .situationValue {
.inlineExpression.comparison > .situationValue {
order: -1;
}

View File

@ -50,9 +50,6 @@
padding: 1em 2em;
}
#ruleHeader #PeriodSwitch {
margin: 0.3em 1em;
}
#ruleHeader #PeriodSwitch img {
display: none;
}

View File

@ -1,9 +1,7 @@
import PeriodSwitch from 'Components/PeriodSwitch'
import withColours from 'Components/utils/withColours'
import { path } from 'ramda'
import React from 'react'
import emoji from 'react-easy-emoji'
import { Trans } from 'react-i18next'
import { capitalise0 } from '../../utils'
import { Markdown } from '../utils/markdown'
import Destinataire from './Destinataire'
@ -21,8 +19,7 @@ let RuleHeader = withColours(
name,
title,
icon,
colours,
valuesToShow
colours
}) => (
<section id="ruleHeader">
<header className="ui__ plain card">
@ -40,39 +37,14 @@ let RuleHeader = withColours(
<div id="ruleHeader__description">
<Markdown source={description || question} />
</div>
{(type || flatRule['période']) && (
<div id="ruleHeader__infobox">
{type && (
<div className="infobox__item">
<h4>Type&nbsp;:</h4>
<Trans>{capitalise0(type)}</Trans>
</div>
)}
{do {
let period = flatRule['période']
period && (
<div className="infobox__item">
<h4>Période :</h4>
{valuesToShow && period === 'flexible' ? (
<PeriodSwitch />
) : (
<div className="inlineMecanism">
<span
className="name"
data-term-definition="période"
style={{ background: '#8e44ad' }}>
{period}
</span>
</div>
)}
</div>
)
}}
<Destinataire
destinataire={path([type, 'destinataire'])(flatRule)}
/>
</div>
)}
{do {
let destinataire = path([type, 'destinataire'])(flatRule)
destinataire && (
<div id="ruleHeader__infobox">
<Destinataire destinataire={destinataire} />
</div>
)
}}
</div>
</section>
)

View File

@ -20,12 +20,6 @@ h2 small {
font-size: 75%;
}
#rule #ruleValue {
text-align: center;
font-size: 200%;
margin-bottom: 0.6em;
margin-top: 0.4em;
}
#rule #ruleDefault {
text-align: center;

View File

@ -30,6 +30,7 @@ import Examples from './Examples'
import RuleHeader from './Header'
import References from './References'
import './Rule.css'
import PeriodSwitch from 'Components/PeriodSwitch'
let LazySource = React.lazy(() => import('./RuleSource'))
@ -63,6 +64,7 @@ export default compose(
namespaceRules = findRuleByNamespace(flatRules, dottedName)
let displayedRule = analysedExample || analysedRule
return (
<>
{this.state.viewSource ? (
@ -100,7 +102,23 @@ export default compose(
/>
<section id="rule-content">
<div id="ruleValue">
<div
id="ruleValue"
css={`
display: flex;
justify-content: center;
flex-wrap: wrap;
align-items: center;
> .value {
font-size: 220%;
}
margin: 0.6em 0;
> * {
margin: 0 0.6em;
}
`}>
<Value
{...displayedRule}
nilValueSymbol={
@ -109,6 +127,10 @@ export default compose(
: null
}
/>
<Period
period={flatRule['période']}
valuesToShow={valuesToShow}
/>
</div>
{displayedRule.defaultValue != null && (
<div id="ruleDefault">
@ -201,7 +223,7 @@ let NamespaceRulesList = compose(
return (
<section>
<h2>
<Trans>Règles associées</Trans>
<Trans>Pages associées</Trans>
</h2>
<ul>
{namespaceRules.map(r => (
@ -224,3 +246,19 @@ let NamespaceRulesList = compose(
</section>
)
})
let Period = ({ period, valuesToShow }) =>
period ? (
valuesToShow && period === 'flexible' ? (
<PeriodSwitch />
) : (
<span className="inlineMecanism">
<span
className="name"
data-term-definition="période"
style={{ background: '#8e44ad' }}>
{period}
</span>
</span>
)
) : null

View File

@ -16,7 +16,7 @@ import {
export let makeJsx = node =>
typeof node.jsx == 'function'
? node.jsx(node.nodeValue, node.explanation, node.lazyEval)
? node.jsx(node.nodeValue, node.explanation, node.lazyEval, node.unit)
: node.jsx
export let collectNodeMissing = node => node.missingVariables || {}

View File

@ -32,11 +32,8 @@ Comparable -> ( AdditionSubstraction | NonNumericTerminal) {% ([[e]]) => e %}
NonNumericTerminal ->
Boolean {% id %}
| String {% id %}
| NegatedVariable {% id %}
NegatedVariable -> "≠" _ Variable {% ([,,{variable}]) => ({'≠': {explanation: variable} }) %}
FilteredVariable -> Variable _ Filter {% filteredVariable %}
Filter -> "[" VariableFragment "]" {% ([,filter]) => filter %}

View File

@ -1,5 +1,6 @@
/* Those are postprocessor functions for the Nearley grammar.ne.
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 { parseUnit } from 'Engine/units'
export let operation = operationType => ([A, , operator, , B]) => ({
[operator]: {
@ -41,6 +42,7 @@ export let percentage = d => ({
constant: {
rawNode: d,
type: 'percentage',
unit: parseUnit('%'),
nodeValue:
parseFloat(d[0].join('') + (d[1] ? d[1][0] + d[1][1].join('') : '')) / 100
}

View File

@ -7,7 +7,8 @@ import React from 'react'
import { Trans } from 'react-i18next'
import { makeJsx } from '../evaluation'
import './Barème.css'
import { formatNumber, Node, NodeValuePointer } from './common'
import { Node, NodeValuePointer } from './common'
import { numberFormatter } from 'Components/Value'
export let BarèmeAttributes = ({ explanation, lazyEval = identity }) => (
<>
@ -36,7 +37,8 @@ let Component = withLanguage(function Barème({
nodeValue,
explanation,
barèmeType,
lazyEval
lazyEval,
unit
}) {
return (
<ShowValuesConsumer>
@ -45,6 +47,7 @@ let Component = withLanguage(function Barème({
classes="mecanism barème"
name={barèmeType === 'marginal' ? 'barème' : 'barème linéaire'}
value={nodeValue}
unit={unit}
child={
<ul className="properties">
<BarèmeAttributes explanation={explanation} lazyEval={lazyEval} />
@ -96,12 +99,13 @@ let Component = withLanguage(function Barème({
<b>
<Trans>Taux final</Trans> :{' '}
</b>
{formatNumber(
(nodeValue /
lazyEval(explanation['assiette']).nodeValue) *
100,
language
)}{' '}
<NodeValuePointer
data={
(nodeValue /
lazyEval(explanation['assiette']).nodeValue) *
100
}
/>
%
</>
)}
@ -132,16 +136,17 @@ let Tranche = ({
<td key="tranche">
{maxOnly ? (
<>
<Trans>En-dessous de</Trans> {formatNumber(maxOnly, language)}
<Trans>En-dessous de</Trans>{' '}
{numberFormatter({ language })(maxOnly)}
</>
) : minOnly ? (
<>
<Trans>Au-dessus de</Trans> {formatNumber(minOnly, language)}
<Trans>Au-dessus de</Trans> {numberFormatter({ language })(minOnly)}
</>
) : (
<>
<Trans>De</Trans> {formatNumber(min, language)} <Trans>à</Trans>{' '}
{formatNumber(max, language)}
<Trans>De</Trans> {numberFormatter({ language })(min)}{' '}
<Trans>à</Trans> {numberFormatter({ language })(max)}
</>
)}
</td>
@ -156,6 +161,9 @@ let Tranche = ({
}
//eslint-disable-next-line
export default barèmeType => (nodeValue, explanation, lazyEval = identity) => (
<Component {...{ nodeValue, explanation, barèmeType, lazyEval }} />
)
export default barèmeType => (
nodeValue,
explanation,
lazyEval = identity,
unit
) => <Component {...{ nodeValue, explanation, barèmeType, lazyEval, unit }} />

View File

@ -7,7 +7,7 @@ import withLanguage from 'Components/utils/withLanguage'
import { BarèmeAttributes } from './Barème'
import { sortObjectByKeys } from 'Engine/mecanismViews/common'
let Comp = withLanguage(function Barème({ nodeValue, explanation }) {
let Comp = withLanguage(function Barème({ nodeValue, explanation, unit }) {
return (
<ShowValuesConsumer>
{showValues => (
@ -15,6 +15,7 @@ let Comp = withLanguage(function Barème({ nodeValue, explanation }) {
classes="mecanism barème"
name="barème continu"
value={nodeValue}
unit={unit}
child={
<ul className="properties">
<BarèmeAttributes explanation={explanation} />
@ -60,6 +61,6 @@ let Comp = withLanguage(function Barème({ nodeValue, explanation }) {
})
//eslint-disable-next-line
export default (nodeValue, explanation) => (
<Comp {...{ nodeValue, explanation }} />
export default (nodeValue, explanation, _, unit) => (
<Comp {...{ nodeValue, explanation, unit }} />
)

View File

@ -3,16 +3,8 @@
counter-reset: li;
padding-left: 1em;
}
.composantes > ol > li.composante::before {
counter-increment: li;
}
.composantes > ol > li.composante::before {
content: counter(li) ')';
color: grey;
display: inline-block;
width: 1em;
margin-left: -1em;
.composantes > ol > li > ul > li {
list-style-type: none;
}
.composantes .composanteAttributes {

View File

@ -6,11 +6,13 @@ import { Trans } from 'react-i18next'
import { toPairs } from 'ramda'
import writtenNumbers from '../../locales/writtenNumbers.yaml'
import withLanguage from 'Components/utils/withLanguage'
import colours from 'Engine/mecanismViews/colours'
let Comp = withLanguage(function Composantes({
language,
nodeValue,
explanation
explanation,
unit
}) {
return (
<Node
@ -18,20 +20,31 @@ let Comp = withLanguage(function Composantes({
name="composantes"
inline
value={nodeValue}
unit={unit}
child={
<>
<p>
<Trans>Cette règle est la somme de</Trans>{' '}
<p css="margin-bottom: 1em">
<Trans>La somme de</Trans>{' '}
{writtenNumbers[language][explanation.length]}{' '}
<InlineMecanism name="composantes" /> :
</p>
<ol>
{explanation.map(c => [
<li className="composante" key={JSON.stringify(c.composante)}>
<ul className="composanteAttributes">
{explanation.map((c, i) => [
<li
className="composante"
css={``}
key={JSON.stringify(c.composante)}>
<ul
className="composanteAttributes"
css={`
border-left: 4px solid ${colours('composantes')};
`}>
{toPairs(c.composante).map(([k, v]) => (
<li key={k} className="composanteName">
<span>
<span
css={`
color: ${colours('composantes')};
`}>
<Trans>{k}</Trans>:{' '}
</span>
<span>
@ -41,6 +54,15 @@ let Comp = withLanguage(function Composantes({
))}
</ul>
<div className="content">{makeJsx(c)}</div>
<div
css={`
text-align: center;
width: 100%;
font-size: 2.6rem;
margin: 0.4em 0 0.2em;
`}>
{i === explanation.length - 1 ? null : '+'}
</div>
</li>
])}
</ol>
@ -51,6 +73,6 @@ let Comp = withLanguage(function Composantes({
})
// eslint-disable-next-line
export default (nodeValue, explanation) => (
<Comp {...{ nodeValue, explanation }} />
export default (nodeValue, explanation, _, unit) => (
<Comp {...{ nodeValue, explanation, unit }} />
)

View File

@ -4,13 +4,14 @@ import { Trans } from 'react-i18next'
import { Node } from './common'
import './InversionNumérique.css'
export default function ProductView(nodeValue, explanation) {
export default function ProductView(nodeValue, explanation, _, unit) {
return (
// The rate and factor and threshold are given defaut neutral values. If there is nothing to explain, don't display them at all
<Node
classes="mecanism multiplication"
name="multiplication"
value={nodeValue}
unit={unit}
child={
<div
style={{

View File

@ -4,21 +4,22 @@ import { makeJsx } from '../evaluation'
import './Somme.css'
import { Node, NodeValuePointer } from './common'
const SommeNode = ({ explanation, nodeValue }) => (
const SommeNode = ({ explanation, nodeValue, unit }) => (
<Node
classes="mecanism somme"
name="somme"
value={nodeValue}
child={<Table explanation={explanation} />}
unit={unit}
child={<Table explanation={explanation} unit={unit} />}
/>
)
export default SommeNode
let Table = ({ explanation }) => (
let Table = ({ explanation, unit }) => (
<div className="mecanism-somme__table">
<div>
{explanation.map((v, i) => (
<Row key={i} {...{ v, i }} />
<Row key={i} {...{ v, i }} unit={unit} />
))}
</div>
</div>
@ -30,7 +31,7 @@ class Row extends Component {
folded: true
}
render() {
let { v, i } = this.props,
let { v, i, unit } = this.props,
rowFormula = path(['explanation', 'formule', 'explanation'], v),
isSomme = rowFormula && rowFormula.name == 'somme'
@ -50,13 +51,13 @@ class Row extends Component {
)}
</div>
<div className="situationValue value">
<NodeValuePointer data={v.nodeValue} />
<NodeValuePointer data={v.nodeValue} unit={unit} />
</div>
</div>,
...(isSomme && !this.state.folded
? [
<div className="nested" key={v.name + '-nest'}>
<Table explanation={rowFormula.explanation} />
<Table explanation={rowFormula.explanation} unit={unit} />
</div>
]
: [])

View File

@ -12,7 +12,8 @@ import './Variations.css'
let Comp = withLanguage(function Variations({
language,
nodeValue,
explanation
explanation,
unit
}) {
let [expandedVariation, toggleVariation] = useState(null)
@ -23,12 +24,12 @@ let Comp = withLanguage(function Variations({
classes="mecanism variations"
name="variations"
inline
unit={unit}
value={nodeValue}
child={
<>
<p>
<Trans>Cette règle présente</Trans>{' '}
{writtenNumbers[language][explanation.length]}{' '}
<p css="text-transform: capitalize">
<Trans >{writtenNumbers[language][explanation.length]}{' '}</Trans>
<InlineMecanism name="variations" /> :
</p>
<ol>
@ -44,7 +45,7 @@ let Comp = withLanguage(function Variations({
}}>
{!satisfied && showValues && (
<>
non applicable{' '}
<em>non applicable </em>
{expandedVariation !== i ? (
<button
className="ui__ link-button"
@ -111,6 +112,6 @@ let Comp = withLanguage(function Variations({
)
})
// eslint-disable-next-line
export default (nodeValue, explanation) => (
<Comp {...{ nodeValue, explanation }} />
export default (nodeValue, explanation, _, unit) => (
<Comp {...{ nodeValue, explanation, unit }} />
)

View File

@ -13,17 +13,21 @@ import mecanismColours from './colours'
import classnames from 'classnames'
import Value from 'Components/Value'
//TODO remove this one, it should reside in 'Value.js'
export let formatNumber = (data, language) =>
!isNaN(data)
? Intl.NumberFormat(language, { maximumFractionDigits: 4 }).format(data)
: data
export let NodeValuePointer = ({ data, unit }) => (
<span
className={classnames('situationValue', {
boolean: typeof data == 'boolean'
})}>
})}
css={`
background: white;
border-bottom: 0 !important;
padding: 0 0.2rem;
text-decoration: none !important;
font-size: 80%;
box-shadow: 2px 2px 4px 1px #d9d9d9, 0 0 0 1px #d9d9d9;
line-height: 1.6em;
border-radius: 0.2rem;
`}>
<Value nodeValue={data} unit={unit} />
</span>
)
@ -31,7 +35,7 @@ export let NodeValuePointer = ({ data, unit }) => (
// Un élément du graphe de calcul qui a une valeur interprétée (à afficher)
export class Node extends Component {
render() {
let { classes, name, value, child, inline } = this.props,
let { classes, name, value, child, inline, unit } = this.props,
termDefinition = contains('mecanism', classes) && name
return (
@ -39,7 +43,7 @@ export class Node extends Component {
className={classNames(classes, 'node', { inline })}
style={termDefinition ? { borderColor: mecanismColours(name) } : {}}>
{name && !inline && (
<span className="nodeHead">
<div className="nodeHead" css="margin-bottom: 1em">
<LinkButton
className="name"
style={
@ -48,22 +52,29 @@ export class Node extends Component {
data-term-definition={termDefinition}>
<Trans>{name}</Trans>
</LinkButton>
</span>
</div>
)}
{child}{' '}
{name ? (
!isNil(value) && (
<div className="mecanism-result">
<NodeValuePointer data={value} />
<span css="font-size: 90%; margin: 0 .6em">=</span>
<NodeValuePointer data={value} unit={unit} />
</div>
)
) : (
<>
<span
css={`
@media (max-width: 1200px) {
width: 100%;
text-align: right;
}
`}>
{value !== true && value !== false && !isNil(value) && (
<span className="operator"> = </span>
<span className="operator"> =&nbsp;</span>
)}
<NodeValuePointer data={value} />
</>
<NodeValuePointer data={value} unit={unit} />
</span>
)}
</div>
)
@ -94,7 +105,7 @@ export const Leaf = compose(
classes,
dottedName,
name,
value,
nodeValue,
flatRules,
filter,
sitePaths,
@ -114,11 +125,16 @@ export const Leaf = compose(
}>
<span className="name">
{rule.title || capitalise0(name)} {filter}
{!isNil(value) && (
<NodeValuePointer data={value} unit={unit} />
)}
</span>
</Link>
{!isNil(nodeValue) && (
<span
css={`
margin: 0 0.3rem;
`}>
<NodeValuePointer data={nodeValue} unit={unit} />
</span>
)}
</span>
)}
</span>

View File

@ -1,9 +1,7 @@
import { desugarScale } from 'Engine/mecanisms/barème'
import { decompose, devariateExplanation } from 'Engine/mecanisms/utils'
import { decompose } from 'Engine/mecanisms/utils'
import {
add,
any,
aperture,
curry,
equals,
evolve,
@ -12,9 +10,7 @@ import {
head,
is,
isEmpty,
isNil,
keys,
last,
map,
max,
mergeWith,
@ -23,11 +19,7 @@ import {
pipe,
pluck,
prop,
propEq,
reduce,
reduced,
reject,
sort,
subtract,
toPairs
} from 'ramda'
@ -48,98 +40,15 @@ import {
rewriteNode
} from './evaluation'
import Allègement from './mecanismViews/Allègement'
import Barème from './mecanismViews/Barème'
import BarèmeContinu from './mecanismViews/BarèmeContinu'
import { Node, SimpleRuleLink } from './mecanismViews/common'
import InversionNumérique from './mecanismViews/InversionNumérique'
import Product from './mecanismViews/Product'
import Somme from './mecanismViews/Somme'
import Variations from './mecanismViews/Variations'
import { disambiguateRuleReference, findRuleByDottedName } from './rules'
import { anyNull, val } from './traverse-common-functions'
import uniroot from './uniroot'
/* @devariate = true => This function will produce variations of a same mecanism (e.g. product) that share some common properties */
export let mecanismVariations = (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 === undefined
? undefined
: 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) {
// We've reached the eventual defaut case
let evaluatedVariation = {
consequence: evaluateVariationProp(variation.consequence),
satisfied: true
}
return [true, [...result, evaluatedVariation]]
}
if (evaluatedCondition.nodeValue === null)
// one case has missing variables => we can't go further
return [true, [...result, { 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 rewriteNode(node, nodeValue, resolvedExplanation, missingVariables)
}
// TODO - find an appropriate representation
return {
explanation,
evaluate,
jsx: Variations,
category: 'mecanism',
name: 'variations',
type: 'numeric'
}
}
import { inferUnit } from 'Engine/units'
import variations from 'Engine/mecanisms/variations'
export let mecanismOneOf = (recurse, k, v) => {
if (!is(Array, v)) throw new Error('should be array')
@ -253,7 +162,7 @@ export let mecanismNumericalSwitch = (recurse, k, v) => {
// la conséquence peut être un 'string' ou un autre aiguillage numérique
let parseCondition = ([condition, consequence]) => {
let conditionNode = recurse(condition), // can be a 'comparison', a 'variable', TODO a 'negation'
let conditionNode = recurse(condition), // can be a 'comparison', a 'variable'
consequenceNode = mecanismNumericalSwitch(recurse, condition, consequence)
let evaluate = (cache, situationGate, parsedRules, node) => {
@ -496,13 +405,14 @@ export let mecanismSum = (recurse, k, v) => {
return {
evaluate,
// eslint-disable-next-line
jsx: (nodeValue, explanation) => (
<Somme nodeValue={nodeValue} explanation={explanation} />
jsx: (nodeValue, explanation, _, unit) => (
<Somme nodeValue={nodeValue} explanation={explanation} unit={unit} />
),
explanation,
category: 'mecanism',
name: 'somme',
type: 'numeric'
type: 'numeric',
unit: inferUnit('+', explanation.map(r => r.unit))
}
}
@ -573,7 +483,7 @@ export let mecanismProduct = (recurse, k, v) => {
return decompose(recurse, k, v)
}
if (v.variations) {
return mecanismVariations(recurse, k, v, true)
return variations(recurse, k, v, true)
}
let objectShape = {
@ -608,120 +518,13 @@ export let mecanismProduct = (recurse, k, v) => {
explanation,
category: 'mecanism',
name: 'multiplication',
type: 'numeric'
}
}
/* on réécrit en une syntaxe plus bas niveau mais plus régulière les tranches :
`en-dessous de: 1`
devient
```
de: 0
à: 1
```
*/
export let mecanismLinearScale = (recurse, k, v) => {
if (v.composantes) {
//mécanisme de composantes. Voir known-mecanisms.md/composantes
return decompose(recurse, k, v)
}
if (v.variations) {
return mecanismVariations(recurse, k, v, true)
}
let tranches = desugarScale(recurse)(v['tranches']),
objectShape = {
assiette: false,
multiplicateur: defaultNode(1)
}
let effect = ({ assiette, multiplicateur, tranches }) => {
if (val(assiette) === null) return null
let roundedAssiette = Math.round(val(assiette))
let matchedTranche = tranches.find(
({ de: min, à: max }) =>
roundedAssiette >= val(multiplicateur) * min &&
roundedAssiette <= max * val(multiplicateur)
type: 'numeric',
unit: inferUnit(
'*',
[explanation.assiette, explanation.taux, explanation.facteur].map(
el => el.unit
)
)
if (!matchedTranche) return 0
if (matchedTranche.taux)
return matchedTranche.taux.nodeValue * val(assiette)
return matchedTranche.montant
}
let explanation = {
...parseObject(recurse, objectShape, v),
tranches
},
evaluate = evaluateObject(objectShape, effect)
return {
evaluate,
jsx: Barème('linéaire'),
explanation,
category: 'mecanism',
name: 'barème linéaire',
barème: 'en taux',
type: 'numeric'
}
}
export let mecanismContinuousScale = (recurse, k, v) => {
let objectShape = {
assiette: false,
multiplicateur: defaultNode(1)
}
let returnRate = v['retourne seulement le taux'] === 'oui'
let effect = ({ assiette, multiplicateur, points }) => {
if (anyNull([assiette, multiplicateur])) return null
//We'll build a linear function given the two constraints that must be respected
let result = pipe(
toPairs,
// we don't rely on the sorting of objects
sort(([k1], [k2]) => k1 - k2),
points => [...points, [Infinity, last(points)[1]]],
aperture(2),
reduce((_, [[lowerLimit, lowerRate], [upperLimit, upperRate]]) => {
let x1 = val(multiplicateur) * lowerLimit,
x2 = val(multiplicateur) * upperLimit,
y1 = val(assiette) * val(recurse(lowerRate)),
y2 = val(assiette) * val(recurse(upperRate))
if (val(assiette) > x1 && val(assiette) <= x2) {
// Outside of these 2 limits, it's a linear function a * x + b
let a = (y2 - y1) / (x2 - x1),
b = y1 - x1 * a,
nodeValue = a * val(assiette) + b,
taux = nodeValue / val(assiette)
return reduced({
nodeValue: returnRate ? taux : nodeValue,
additionalExplanation: {
seuil: val(assiette) / val(multiplicateur),
taux
}
})
}
}, 0)
)(points)
return result
}
let explanation = {
...parseObject(recurse, objectShape, v),
points: v.points,
returnRate
},
evaluate = evaluateObject(objectShape, effect)
return {
evaluate,
jsx: BarèmeContinu,
explanation,
category: 'mecanism',
name: 'barème continu',
type: 'numeric'
}
}

View File

@ -0,0 +1,63 @@
import { defaultNode, evaluateObject } from 'Engine/evaluation'
import BarèmeContinu from 'Engine/mecanismViews/BarèmeContinu'
import { val, anyNull } from 'Engine/traverse-common-functions'
import { parseUnit } from 'Engine/units'
import { parseObject } from 'Engine/evaluation'
import { reduce, toPairs, sort, aperture, pipe, reduced, last } from 'ramda'
export default (recurse, k, v) => {
let objectShape = {
assiette: false,
multiplicateur: defaultNode(1)
}
let returnRate = v['retourne seulement le taux'] === 'oui'
let effect = ({ assiette, multiplicateur, points }) => {
if (anyNull([assiette, multiplicateur])) return null
//We'll build a linear function given the two constraints that must be respected
let result = pipe(
toPairs,
// we don't rely on the sorting of objects
sort(([k1], [k2]) => k1 - k2),
points => [...points, [Infinity, last(points)[1]]],
aperture(2),
reduce((_, [[lowerLimit, lowerRate], [upperLimit, upperRate]]) => {
let x1 = val(multiplicateur) * lowerLimit,
x2 = val(multiplicateur) * upperLimit,
y1 = val(assiette) * val(recurse(lowerRate)),
y2 = val(assiette) * val(recurse(upperRate))
if (val(assiette) > x1 && val(assiette) <= x2) {
// Outside of these 2 limits, it's a linear function a * x + b
let a = (y2 - y1) / (x2 - x1),
b = y1 - x1 * a,
nodeValue = a * val(assiette) + b,
taux = nodeValue / val(assiette)
return reduced({
nodeValue: returnRate ? taux : nodeValue,
additionalExplanation: {
seuil: val(assiette) / val(multiplicateur),
taux
}
})
}
}, 0)
)(points)
return result
}
let explanation = {
...parseObject(recurse, objectShape, v),
points: v.points,
returnRate
},
evaluate = evaluateObject(objectShape, effect)
return {
evaluate,
jsx: BarèmeContinu,
explanation,
category: 'mecanism',
name: 'barème continu',
type: 'numeric',
unit: returnRate ? parseUnit('%') : explanation.assiette.unit
}
}

View File

@ -0,0 +1,66 @@
import { defaultNode, evaluateObject } from 'Engine/evaluation'
import Barème from 'Engine/mecanismViews/Barème'
import variations from 'Engine/mecanisms/variations'
import { decompose } from 'Engine/mecanisms/utils'
import { val } from 'Engine/traverse-common-functions'
import { inferUnit, parseUnit } from 'Engine/units'
import { parseObject } from 'Engine/evaluation'
import { desugarScale } from './barème'
/* on réécrit en une syntaxe plus bas niveau mais plus régulière les tranches :
`en-dessous de: 1`
devient
```
de: 0
à: 1
```
*/
export default (recurse, k, v) => {
if (v.composantes) {
//mécanisme de composantes. Voir known-mecanisms.md/composantes
return decompose(recurse, k, v)
}
if (v.variations) {
return variations(recurse, k, v, true)
}
let tranches = desugarScale(recurse)(v['tranches']),
objectShape = {
assiette: false,
multiplicateur: defaultNode(1)
}
let effect = ({ assiette, multiplicateur, tranches }) => {
if (val(assiette) === null) return null
let roundedAssiette = Math.round(val(assiette))
let matchedTranche = tranches.find(
({ de: min, à: max }) =>
roundedAssiette >= val(multiplicateur) * min &&
roundedAssiette <= max * val(multiplicateur)
)
if (!matchedTranche) return 0
if (matchedTranche.taux)
return matchedTranche.taux.nodeValue * val(assiette)
return matchedTranche.montant
}
let explanation = {
...parseObject(recurse, objectShape, v),
tranches
},
evaluate = evaluateObject(objectShape, effect)
return {
evaluate,
jsx: Barème('linéaire'),
explanation,
category: 'mecanism',
name: 'barème linéaire',
barème: 'en taux',
type: 'numeric',
unit: explanation.assiette.unit
}
}

View File

@ -1,9 +1,10 @@
import { defaultNode, E, rewriteNode } from 'Engine/evaluation'
import { mecanismVariations } from 'Engine/mecanisms'
import variations from 'Engine/mecanisms/variations'
import { decompose } from 'Engine/mecanisms/utils'
import Barème from 'Engine/mecanismViews/Barème'
import { val } from 'Engine/traverse-common-functions'
import { evolve, has, pluck, sum } from 'ramda'
import { inferUnit, parseUnit } from 'Engine/units'
export let desugarScale = recurse => tranches =>
tranches
@ -38,7 +39,7 @@ export default (recurse, k, v) => {
return decompose(recurse, k, v)
}
if (v.variations) {
return mecanismVariations(recurse, k, v, true)
return variations(recurse, k, v, true)
}
let { assiette, multiplicateur } = v,
@ -85,6 +86,7 @@ export default (recurse, k, v) => {
jsx: Barème('marginal'),
category: 'mecanism',
name: 'barème',
barème: 'marginal'
barème: 'marginal',
unit: inferUnit('*', [explanation.assiette.unit, parseUnit('%')])
}
}

View File

@ -0,0 +1,62 @@
import React from 'react'
import { curry, map } from 'ramda'
import { inferUnit } from 'Engine/units'
import {
evaluateNode,
makeJsx,
mergeMissing,
rewriteNode
} from 'Engine/evaluation'
import { Node } from 'Engine/mecanismViews/common'
export default (k, operatorFunction, symbol) => (recurse, k, v) => {
let evaluate = (cache, situation, parsedRules, node) => {
let explanation = map(
curry(evaluateNode)(cache, situation, parsedRules),
node.explanation
),
value1 = explanation[0].nodeValue,
value2 = explanation[1].nodeValue,
nodeValue =
value1 == null || value2 == null
? null
: operatorFunction(value1, value2),
missingVariables = mergeMissing(
explanation[0].missingVariables,
explanation[1].missingVariables
)
return rewriteNode(node, nodeValue, explanation, missingVariables)
}
let explanation = v.explanation.map(recurse)
let unit = inferUnit(k, [explanation[0].unit, explanation[1].unit])
let jsx = (nodeValue, explanation) => (
<Node
classes={'inlineExpression ' + k}
value={nodeValue}
unit={unit}
child={
<span className="nodeContent">
<span className="fa fa" />
{makeJsx(explanation[0])}
<span className="operator">{symbol || k}</span>
{makeJsx(explanation[1])}
</span>
}
/>
)
return {
...v,
evaluate,
jsx,
operator: symbol || k,
// is this useful ? text: rawNode,
explanation,
unit
}
}

View File

@ -1,6 +1,7 @@
import Composantes from 'Engine/mecanismViews/Composantes'
import { add, dissoc, objOf } from 'ramda'
import { evaluateArrayWithFilter } from 'Engine/evaluation'
import { inferUnit } from 'Engine/units'
export let decompose = (recurse, k, v) => {
let subProps = dissoc('composantes')(v),
@ -28,21 +29,8 @@ export let decompose = (recurse, k, v) => {
evaluate: evaluateArrayWithFilter(filter, add, 0),
category: 'mecanism',
name: 'composantes',
type: 'numeric'
type: 'numeric',
unit: inferUnit('+', explanation.map(e => e.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
}

View File

@ -0,0 +1,110 @@
import { inferUnit } from 'Engine/units'
import {
bonus,
collectNodeMissing,
evaluateNode,
mergeAllMissing,
mergeMissing,
rewriteNode
} from 'Engine/evaluation'
import { reject, pluck, isNil, filter, dissoc, reduce } from 'ramda'
import Variations from 'Engine/mecanismViews/Variations'
/* @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 rewriteNode(node, nodeValue, 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
}

View File

@ -2,11 +2,15 @@
// In a specific file
// TODO import them automatically
// TODO convert the legacy functions to new files
import barème from 'Engine/mecanisms/barème.js'
import barème from 'Engine/mecanisms/barème'
import barèmeContinu from 'Engine/mecanisms/barème-continu'
import barèmeLinéaire from 'Engine/mecanisms/barème-linéaire'
import variations from 'Engine/mecanisms/variations'
import operation from 'Engine/mecanisms/operation'
import { Parser } from 'nearley'
import {
add,
curry,
divide,
equals,
gt,
@ -15,7 +19,6 @@ import {
without,
lt,
lte,
map,
multiply,
propOr,
subtract,
@ -25,15 +28,12 @@ import {
T
} from 'ramda'
import React from 'react'
import { evaluateNode, makeJsx, mergeMissing, rewriteNode } from './evaluation'
import Grammar from './grammar.ne'
import {
mecanismAllOf,
mecanismComplement,
mecanismContinuousScale,
mecanismError,
mecanismInversion,
mecanismLinearScale,
mecanismMax,
mecanismMin,
mecanismNumericalSwitch,
@ -42,16 +42,9 @@ import {
mecanismReduction,
mecanismSum,
mecanismSynchronisation,
mecanismVariations,
mecanismOnePossibility
} from './mecanisms'
import { Node } from './mecanismViews/common'
import {
parseNegatedReference,
parseReference,
parseReferenceTransforms
} from './parseReference'
import { inferUnit } from 'Engine/units'
import { parseReferenceTransforms } from './parseReference'
export let parse = (rules, rule, parsedRules) => rawNode => {
let onNodeType = cond([
@ -130,7 +123,7 @@ export let parseObject = (rules, rule, parsedRules) => rawNode => {
operationDispatch = fromPairs(
Object.entries(knownOperations).map(([k, [f, symbol]]) => [
k,
mecanismOperation(k, f, symbol)
operation(k, f, symbol)
])
)
@ -141,21 +134,17 @@ export let parseObject = (rules, rule, parsedRules) => rawNode => {
somme: mecanismSum,
multiplication: mecanismProduct,
barème,
'barème linéaire': mecanismLinearScale,
'barème continu': mecanismContinuousScale,
'barème linéaire': barèmeLinéaire,
'barème continu': barèmeContinu,
'le maximum de': mecanismMax,
'le minimum de': mecanismMin,
complément: mecanismComplement,
'une possibilité': mecanismOnePossibility(rule.dottedName),
'inversion numérique': mecanismInversion(rule.dottedName),
allègement: mecanismReduction,
variations: mecanismVariations,
variations,
synchronisation: mecanismSynchronisation,
...operationDispatch,
'≠': () =>
parseNegatedReference(
parseReference(rules, rule, parsedRules)(v.explanation)
),
filter: () =>
parseReferenceTransforms(rules, rule, parsedRules)({
filter: v.filter,
@ -171,6 +160,7 @@ export let parseObject = (rules, rule, parsedRules) => rawNode => {
constant: () => ({
type: v.type,
nodeValue: v.nodeValue,
unit: v.unit,
// eslint-disable-next-line
jsx: () => <span className={v.type}>{v.rawNode}</span>
})
@ -179,58 +169,3 @@ export let parseObject = (rules, rule, parsedRules) => rawNode => {
return action(parse(rules, rule, parsedRules), k, v)
}
let mecanismOperation = (k, operatorFunction, symbol) => (recurse, k, v) => {
let evaluate = (cache, situation, parsedRules, node) => {
let explanation = map(
curry(evaluateNode)(cache, situation, parsedRules),
node.explanation
),
value1 = explanation[0].nodeValue,
value2 = explanation[1].nodeValue,
nodeValue =
value1 == null || value2 == null
? null
: operatorFunction(value1, value2),
missingVariables = mergeMissing(
explanation[0].missingVariables,
explanation[1].missingVariables
)
return rewriteNode(node, nodeValue, explanation, missingVariables)
}
let explanation = v.explanation.map(recurse)
let unit = inferUnit(
k,
explanation[0].unit || undefined,
explanation[1].unit || undefined
)
let jsx = (nodeValue, explanation) => (
<Node
classes={'inlineExpression ' + k}
value={nodeValue}
child={
<span className="nodeContent">
<span className="fa fa" />
{makeJsx(explanation[0])}
<span className="operator">{symbol || k}</span>
{makeJsx(explanation[1])}
</span>
}
/>
)
return {
...v,
evaluate,
jsx,
operator: symbol || k,
// is this useful ? text: rawNode,
explanation,
unit
}
}

View File

@ -17,9 +17,13 @@ export let parseReference = (rules, rule, parsedRules, filter) => ({
let partialReference = fragments.join(' . '),
dottedName = disambiguateRuleReference(rules, rule, partialReference)
let inInversionFormula = rule.formule?.['inversion numérique']
let parsedRule =
parsedRules[dottedName] ||
parseRule(rules, findRuleByDottedName(rules, dottedName), parsedRules)
// the 'inversion numérique' formula should not exist. The instructions to the evaluation should be enough to infer that an inversion is necessary (assuming it is possible, the client decides this)
(!inInversionFormula &&
parseRule(rules, findRuleByDottedName(rules, dottedName), parsedRules))
let evaluate = (cache, situation, parsedRules, node) => {
let dottedName = node.dottedName,
@ -93,14 +97,16 @@ export let parseReference = (rules, rule, parsedRules, filter) => ({
evaluate,
//eslint-disable-next-line react/display-name
jsx: nodeValue => (
<Leaf
classes="variable filtered"
filter={filter}
name={fragments.join(' . ')}
dottedName={dottedName}
value={nodeValue}
unit={parsedRule.unit}
/>
<>
<Leaf
classes="variable filtered"
filter={filter}
name={fragments.join(' . ')}
dottedName={dottedName}
nodeValue={nodeValue}
unit={parsedRule.unit}
/>
</>
),
name: partialReference,
@ -155,11 +161,7 @@ export let parseReferenceTransforms = (
if (!rule.période && !inlinePeriodTransform) {
if (supportedPeriods.includes(ruleToTransform.période))
throw new Error(
`Attention, une variable sans période, ${
rule.dottedName
}, qui appelle une variable à période, ${
ruleToTransform.dottedName
}, c'est suspect !
`Attention, une variable sans période, ${rule.dottedName}, qui appelle une variable à période, ${ruleToTransform.dottedName}, c'est suspect !
Si la période de la variable appelée est neutralisée dans la formule de calcul, par exemple un montant mensuel divisé par 30 (comprendre 30 jours), utilisez "période: aucune" pour taire cette erreur et rassurer tout le monde.
`
@ -217,40 +219,3 @@ export let parseReferenceTransforms = (
evaluate: evaluateTransforms(node.evaluate)
}
}
export let parseNegatedReference = variable => {
let evaluate = (cache, situation, parsedRules, node) => {
let explanation = evaluateNode(
cache,
situation,
parsedRules,
node.explanation
),
nodeValue = explanation.nodeValue == null ? null : !explanation.nodeValue,
missingVariables = explanation.missingVariables
return rewriteNode(node, nodeValue, explanation, missingVariables)
}
let jsx = (nodeValue, explanation) => (
<Node
classes="inlineExpression negation"
value={nodeValue}
child={
<span className="nodeContent">
<Trans i18nKey="inlineExpressionNegation">Non</Trans>{' '}
{makeJsx(explanation)}
</span>
}
/>
)
return {
evaluate,
jsx,
category: 'mecanism',
name: 'négation',
type: 'boolean',
explanation: variable
}
}

View File

@ -1,5 +1,6 @@
import { remove, isEmpty } from 'ramda'
import { remove, isEmpty, unnest } from 'ramda'
//TODO this function does not handle complex units like passenger-kilometer/flight
export let parseUnit = string => {
let [a, b = ''] = string.split('/'),
result = {
@ -9,32 +10,56 @@ export let parseUnit = string => {
return result
}
export let serialiseUnit = ({ numerators, denominators }) => {
let printUnits = units => units.filter(unit => unit !== '%').join('-')
export let serialiseUnit = rawUnit => {
let unit = simplify(rawUnit),
{ numerators = [], denominators = [] } = unit
// the unit '%' is only displayed when it is the only unit
let merge = [...numerators, ...denominators]
if (merge.length === 1 && merge[0] === '%') return '%'
let n = !isEmpty(numerators)
let d = !isEmpty(denominators)
return !n && !d
? ''
: n && !d
? numerators.join('')
: !n && d
? `/${denominators.join('')}`
: `${numerators.join('')} / ${denominators.join('')}`
let string =
!n && !d
? ''
: n && !d
? printUnits(numerators)
: !n && d
? `/${printUnits(denominators)}`
: `${printUnits(numerators)} / ${printUnits(denominators)}`
return string
}
let noUnit = { numerators: [], denominators: [] }
export let inferUnit = (operator, unit1 = noUnit, unit2 = noUnit) =>
operator === '*'
? simplify({
numerators: [...unit1.numerators, ...unit2.numerators],
denominators: [...unit1.denominators, ...unit2.denominators]
})
: operator === '/'
? inferUnit('*', unit1, {
numerators: unit2.denominators,
denominators: unit2.numerators
})
: null
export let inferUnit = (operator, rawUnits) => {
let units = rawUnits.map(u => u || noUnit)
if (operator === '*')
return simplify({
numerators: unnest(units.map(u => u.numerators)),
denominators: unnest(units.map(u => u.denominators))
})
if (operator === '/') {
if (units.length !== 2)
throw new Error('Infer units of a division with units.length !== 2)')
return inferUnit('*', [
units[0],
{
numerators: units[1].denominators,
denominators: units[1].numerators
}
])
}
if (operator === '-' || operator === '+') {
return rawUnits.find(u => u)
}
return null
}
export let removeOnce = element => list => {
let index = list.indexOf(element)
if (index > -1) return remove(index, 1)(list)

View File

@ -28,7 +28,6 @@ radio_Non: No
radio_Aucun: None
oui: yes
non: no
inlineExpressionNegation: Not
déplier: show more
replier: show less
Salaire brut: Gross salary
@ -42,7 +41,7 @@ Quel est le salaire mensuel ?: What is the monthly salary?
back: Back to your simulation
ambiguous: More than one rule with this name found. Which one are you looking for ?
Références: Relevant legal information (fr)
Règles du groupe: Rules of the group
Pages associées: Related pages
Pas de sources officielles: No official information
Votre avis nous intéresse !: Your opinion matters!
afficher les sources complémentaires: display additional information sources
@ -63,7 +62,6 @@ satisfaction-mailto: click here to directly write an email
Votre adresse e-mail: Your email address
envoyer: send
Rechercher: Search
Règle non applicable: Not applicable
Situation incomplète: More information needed
Destinataire: Levied by
cotisation: contribution
@ -178,8 +176,7 @@ legalNotice:
contact@mon-entreprise.beta.gouv.fr
</0>
Cette règle est la somme de: This rule is the sum of
Cette règle présente: This rule has
La somme de: This rule is the sum of
Si: If
Sinon: Else
Alors: Then

View File

@ -753,7 +753,7 @@
- si:
toutes ces conditions:
- brut de base [mensuel] < SMIC [mensuel]
- assimilé salarié
- assimilé salarié != oui
niveau: avertissement
message: |
Le salaire saisi est inférieur au SMIC.
@ -990,7 +990,6 @@
- espace: contrat salarié
nom: indemnités salarié
période: flexible
unité:
formule:
somme:
- CDD . indemnités salarié CDD
@ -1014,6 +1013,7 @@
description: Le plafond de Sécurité sociale est le montant maximum des rémunérations à prendre en compte pour le calcul de certaines cotisations.
période: mois
formule: 3377
unité:
références:
2019: https://www.urssaf.fr/portail/home/actualites/toute-lactualite-employeur/plafond-de-la-securite-social-1.html
arrêté: https://www.legifrance.gouv.fr/affichTexte.do?cidTexte=JORFTEXT000036171732
@ -1649,7 +1649,7 @@
question: Quel est l'effectif de l'entreprise ?
description: |
De nombreuses cotisations patronales varient selon l'effectif de l'entreprise.
unité: _
unité: employés
suggestions:
1: 1
20: 20
@ -1657,20 +1657,17 @@
1000: 1000
par défaut: 1
- espace: entreprise
nom: ratio alternants
formule: ratio alternants saisi / 100
- espace: entreprise
nom: ratio alternants saisi
nom: ratio alternants
question: Quelle est la fraction de contrats d'alternance dans l'effectif moyen de l'entreprise ?
titre: Fraction d'alternants
description: |
Cette fraction détermine la contribution supplémentaire pour l'apprentissage pour les entreprises concernées.
unité: '%'
suggestions:
1: 1
5: 5
1%: 0.1
5%: 0.5
par défaut: 0
- espace: entreprise
@ -2039,8 +2036,8 @@
- si:
toutes ces conditions:
- cotisations . assiette < plafond de réduction
- statut JEI
- assimilé salarié
- statut JEI != oui
- assimilé salarié != oui
alors: 3.45%
- sinon: 5.25%
références:
@ -2101,11 +2098,11 @@
composantes:
- attributs:
composante: base
nom: base
taux: 4%
- attributs:
composante: contribution exceptionnelle temporaire
nom: contribution exceptionnelle temporaire
description: |
Instaurée le 1er octobre 2017, applicable jusquau 30 septembre 2020 au plus tard.
taux: 0.05%
@ -2140,10 +2137,10 @@
# Répartition arbitraire, en sachant que l'employeur doit prendre en charge au minimum 50%
- attributs:
dû par: employeur
taux: part employeur / 100
taux: part employeur
- attributs:
dû par: salarié
taux: part salarié / 100
taux: part salarié
exemples:
- nom: forfait à 40€
situation:
@ -2152,7 +2149,7 @@
- nom: forfait à 100€ payé par l'employeur
situation:
forfait: 100
part employeur: 100
part employeur: 1
valeur attendue: 100
- espace: contrat salarié . complémentaire santé
@ -2161,11 +2158,11 @@
question: Quel est la part de la complémentaire santé payée par l'employeur ?
unité: '%'
suggestions:
50%: 50
100%: 100
par défaut: 50
50%: 0.50
100%: 1
par défaut: 0.50
contrôles:
- si: part employeur < 50
- si: part employeur < 50%
niveau: avertissement
message: La part employeur de la complémentaire santé doit être de 50% au minimum
@ -2173,7 +2170,7 @@
nom: part salarié
description: Part de la complémentaire santé payée par l'employé. Ne peut pas être supérieure à 50%
unité: '%'
formule: 100 - part employeur
formule: 100% - part employeur
- espace: contrat salarié . complémentaire santé
nom: forfait
@ -2397,27 +2394,15 @@
assiette: cotisations . assiette
composantes:
- attributs:
composante: maladie, maternité, invalidité, décès
nom: maladie, maternité, invalidité, décès
dû par: employeur
taux:
variations:
- si:
toutes ces conditions:
- cotisations . assiette < plafond de réduction employeur
- ≠ statut JEI
- ≠ assimilé salarié
alors: 7%
- sinon: 13%
taux: taux employeur
- attributs:
composante: maladie, maternité, invalidité, décès
nom: maladie, maternité, invalidité, décès
dû par: salarié
taux:
variations:
- si: régime alsace moselle
alors: 1.5%
- sinon: 0%
taux: taux salarié
- attributs:
composante: Contribution Solidarité Autonomie
nom: Contribution Solidarité Autonomie
abbréviation: CSA
dû par: employeur
références:
@ -2425,6 +2410,30 @@
- https://www.service-public.fr/professionnels-entreprises/vosdroits/F32872
taux: 0.3%
- espace: contrat salarié . maladie
nom: taux employeur
période: aucune
formule:
variations:
- si:
toutes ces conditions:
- cotisations . assiette < plafond de réduction employeur
- statut JEI != oui
- assimilé salarié != oui
alors: 7%
- sinon: 13%
- espace: contrat salarié . maladie
nom: taux salarié
période: aucune
formule:
variations:
- si: régime alsace moselle
alors: 1.5%
- sinon: 0%
- espace: contrat salarié . maladie
nom: plafond de réduction employeur
période: flexible
@ -3328,7 +3337,7 @@
formule:
toutes ces conditions:
- entreprise . catégorie d'activité . libérale règlementée
- rattachée à la CIPAV
- rattachée à la CIPAV != oui
note: D'autres conditions d'exclusions existent, il faudra les compléter, mais la question de la catégorie d'activité doit avant être complétée.
- nom: indépendant
@ -3373,7 +3382,7 @@
formule:
une de ces conditions:
- toutes ces conditions:
- contrat salarié
- contrat salarié != oui
- entreprise . catégorie d'activité . libérale règlementée
- toutes ces conditions:
- entreprise . année d'activité
@ -3548,7 +3557,7 @@
titre: assiette retraite de base
formule:
variations:
- si: situation personnelle . RSA
- si: situation personnelle . RSA != oui
alors:
le maximum de:
- 11.5% * plafond sécurité sociale temps plein
@ -3620,7 +3629,7 @@
titre: assiette invalidité et décès
formule:
variations:
- si: situation personnelle . RSA
- si: situation personnelle . RSA != oui
alors:
le maximum de:
- 11.5% * plafond sécurité sociale temps plein
@ -3648,7 +3657,6 @@
- espace: indépendant . cotisations et contributions . CSG et CRDS
nom: assiette
note: Il faut vérifier que l'assiette de la CSG et CRDS est correcte. Pourquoi les cotisations sont-elles dans l'assiette ? La retraite complémentaire doit l'être aussi ?
période: flexible
formule:
somme:
@ -4146,9 +4154,9 @@
- espace: protection sociale . retraite
nom: mois cotisés
unité: mois
formule: 172 * 3
notes: On prends l'hypotèse d'une retraite à taux plein pour un travailleur né en 1973 ou après
unité:
- espace: protection sociale . retraite
nom: complémentaire salarié
@ -4170,12 +4178,14 @@
nom: points acquis
formule: points acquis par mois * mois cotisés
période: aucune
unité: points
références:
service-public.fr: https://www.service-public.fr/particuliers/vosdroits/F15396
- espace: protection sociale . retraite . complémentaire salarié
nom: points acquis par mois
période: mois
unité: points/mois
formule: contrat salarié . retraite complémentaire / prix d'achat du point
- espace: protection sociale . retraite . complémentaire salarié

View File

@ -580,7 +580,7 @@ contrat salarié . salaire . brut de base:
- si:
toutes ces conditions:
- 'brut de base [mensuel] < SMIC [mensuel]'
- assimilé salarié
- assimilé salarié != oui
niveau: avertissement
message: |
The wage entered is lower than the minimum wage.
@ -599,7 +599,7 @@ contrat salarié . salaire . brut de base:
- si:
toutes ces conditions:
- 'brut de base [mensuel] < SMIC [mensuel]'
- assimilé salarié
- assimilé salarié != oui
niveau: avertissement
message: |
Le salaire saisi est inférieur au SMIC.

View File

@ -145,7 +145,7 @@ describe('inversions', () => {
- si: cadre
alors:
taux: 80%
- si: cadre
- si: cadre != oui
alors:
taux: 70%

View File

@ -11,11 +11,12 @@ import { collectMissingVariables } from '../source/engine/generateQuestions'
import testSuites from './load-mecanism-tests'
import * as R from 'ramda'
import { isNumeric } from '../source/utils'
import { serialiseUnit } from 'Engine/units'
describe('Mécanismes', () =>
testSuites.map(([suiteName, suite]) =>
suite.map(
({ exemples, test }) =>
({ exemples, test, 'unité attendue': unit }) =>
exemples &&
describe(`Suite ${suiteName}, test : ${test ||
'Nom de test (propriété "test") manquant dans la variable contenant ces "exemples"'}`, () =>
@ -51,6 +52,11 @@ describe('Mécanismes', () =>
if (expectedMissing) {
expect(missing).to.eql(expectedMissing)
}
if (unit) {
expect(target.unit).not.to.be.equal(undefined)
expect(serialiseUnit(target.unit)).to.eql(unit)
}
})
))
)

View File

@ -1,9 +1,9 @@
- nom: base
unité: _
unité: £
formule: 300
- nom: assiette
unité: _
unité: £
- test: Simple
formule:
@ -14,7 +14,7 @@
0: 0%
0.4: 3.16%
1.1: 6.35%
unité attendue: £
exemples:
- nom: Premier point
situation:
@ -39,11 +39,12 @@
- nom: base deux
unité: _
unité: µ
formule: 300
- nom: assiette deux
unité: _
unité: µ
- test: Retour de taux, pas d'assiette
formule:
barème continu:
@ -54,6 +55,7 @@
0.75: 100%
1: 0%
retourne seulement le taux: oui
unité attendue: '%'
exemples:
- nom: Premier point
situation:

View File

@ -14,6 +14,7 @@
taux: 10%
- au-dessus de: 2000
taux: 15%
unité attendue:
exemples:
- nom: "petite assiette"
@ -51,6 +52,7 @@
- au-dessus de: 2000
montant: 400
unité attendue:
exemples:
- nom: "petite assiette"
situation:

View File

@ -17,7 +17,7 @@
taux: 3%
- au-dessus de: 3
taux: 1%
unité attendue:
exemples:
- nom: 'petite assiette'
situation:
@ -51,6 +51,7 @@
taux: 9%
- au-dessus de: 2
taux: 29%
unité attendue:
exemples:
- nom:
@ -61,12 +62,14 @@
- nom: ma condition
- nom: taux variable
- test: taux variable
formule:
variations:
- si: ma condition
alors: 29%
- sinon: 56%
unité attendue: '%'
exemples: []
- nom: deuxième barème
test: Barème à taux variable
@ -79,6 +82,7 @@
taux: taux variable
- au-dessus de: 1
taux: 90%
unité attendue:
exemples:
- nom: taux faible

View File

@ -1,12 +1,17 @@
- test: Composantes
formule:
multiplication:
assiette: 100
assiette: richesse
composantes:
- taux: 8%
- taux: 2%
unité attendue: crédits
exemples:
- nom:
situation:
valeur attendue: 10
- nom: richesse
unité: crédits
formule: 100

View File

@ -29,10 +29,13 @@
- valeur attendue: 29
- nom: salaire de base
unité: $
- nom: contrat . salaire de base
- test: multiplication
formule: salaire de base * 3
unité attendue: $
exemples:
- situation:
salaire de base: 1000
@ -46,9 +49,11 @@
valeur attendue: 3000
- nom: taux
unité: '%'
- test: soustraction
formule: 1 - taux
unité attendue: '%'
exemples:
- situation:
taux: 0.89
@ -56,6 +61,7 @@
- test: addition
formule: salaire de base + 2000
unité attendue: $
exemples:
- situation:
salaire de base: 3000
@ -78,6 +84,27 @@
salaire de base: 3000
valeur attendue: 1000
- test: division deux
formule: 2000 / salaire de base
unité attendue: /$
exemples:
- situation:
salaire de base: 3000
valeur attendue: 0.66667
- nom: nombre de personnes
unité: personne
- test: division trois
formule: salaire de base / nombre de personnes
unité attendue: $ / personne
exemples:
- situation:
salaire de base: 3000
nombre de personnes: 10
valeur attendue: 300
- test: comparaison stricte
formule: salaire de base < 3001
exemples:
@ -133,7 +160,7 @@
- valeur attendue: true
- test: négation
formule: CDD . poursuivi en CDI
formule: CDD . poursuivi en CDI != oui
exemples:
- situation:
CDD . poursuivi en CDI: oui
@ -150,6 +177,7 @@
- test: multiplication et pourcentage
formule: 38.1% * salaire de base
unité attendue: $
exemples:
- situation:
salaire de base: 1000

View File

@ -6,7 +6,7 @@
multiplication:
assiette: mon assiette
taux: 3%
unité attendue:
exemples:
- nom: entier
situation:
@ -51,9 +51,10 @@
valeur attendue: 1.5
- nom: mon facteur
unité: _
unité: patates
- test: Multiplication à facteur
unité attendue: patates
formule:
multiplication:
assiette: 100
@ -75,6 +76,7 @@
plafond: mon plafond
taux: 0.5%
unité attendue: €-patates
exemples:
- nom:
situation:

View File

@ -21,7 +21,7 @@
variations:
- si: statut cadre
alors: 2300
- si: statut cadre
- si: statut cadre != oui
alors: 2100
exemples:
@ -143,7 +143,7 @@
- si: statut cadre
alors:
taux: 8%
- si: statut cadre
- si: statut cadre != oui
alors:
taux: 5%

View File

@ -19,7 +19,7 @@ describe('Units', () => {
it('should work with simple use case *', () => {
let unit1 = { numerators: ['m'], denominators: ['s'] }
let unit2 = { numerators: ['s'], denominators: [] }
let unit = inferUnit('*', unit1, unit2)
let unit = inferUnit('*', [unit1, unit2])
expect(unit).to.deep.equal({
numerators: ['m'],
@ -29,7 +29,7 @@ describe('Units', () => {
it('should work with simple use case / ', () => {
let unit1 = { numerators: ['m'], denominators: ['s'] }
let unit2 = { numerators: ['m'], denominators: [] }
let unit = inferUnit('/', unit1, unit2)
let unit = inferUnit('/', [unit1, unit2])
expect(unit).to.deep.equal({
numerators: [],
@ -39,7 +39,7 @@ describe('Units', () => {
it('should work with advanced use case /', () => {
let unit1 = { numerators: ['a', 'b', 'a', 'z'], denominators: ['c'] }
let unit2 = { numerators: ['a', 'e', 'f'], denominators: ['z', 'c'] }
let unit = inferUnit('/', unit1, unit2)
let unit = inferUnit('/', [unit1, unit2])
expect(unit).to.deep.equal({
numerators: ['b', 'a', 'z', 'z'],