Merge pull request #719 from betagouv/dottedname

Utilisation de la notation pointée pour définir les règles
pull/738/head
Maxime Quandalle 2019-10-14 16:46:04 +02:00 committed by GitHub
commit f72ab75fb2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
27 changed files with 642 additions and 1122 deletions

View File

@ -10,7 +10,7 @@ import { Router } from 'react-router-dom'
import reducers from 'Reducers/rootReducer'
import { applyMiddleware, compose, createStore } from 'redux'
import thunk from 'redux-thunk'
import { getIframeOption, inIframe } from './utils'
import { inIframe } from './utils'
const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose
@ -73,10 +73,13 @@ export default class Provider extends PureComponent {
this.props.tracker.disconnectFromHistory()
}
render() {
const iframeCouleur = new URLSearchParams(
document.location.search.substring(1)
).get('couleur')
return (
// If IE < 11 display nothing
<ReduxProvider store={this.store}>
<ThemeColoursProvider colour={getIframeOption('couleur')}>
<ThemeColoursProvider colour={iframeCouleur}>
<TrackerProvider value={this.props.tracker}>
<SitePathProvider value={this.props.sitePaths}>
<I18nextProvider i18n={i18next}>

View File

@ -1,5 +1,4 @@
import { React, T } from 'Components'
import { serialiseUnit } from 'Engine/units'
import { useTranslation } from 'react-i18next'
import { formatValue } from 'Engine/format'
@ -42,8 +41,6 @@ export default function Value({
</span>
)
let valueType = typeof nodeValue,
unitText =
unit !== null && (typeof unit == 'object' ? serialiseUnit(unit) : unit),
formattedValue =
valueType === 'string' ? (
<T>{nodeValue}</T>
@ -56,8 +53,8 @@ export default function Value({
minimumFractionDigits,
maximumFractionDigits,
language,
value: nodeValue,
unit: unitText
unit,
value: nodeValue
})
)

View File

@ -1,39 +1,44 @@
import { toPairs } from 'ramda'
import React from 'react'
import { capitalise0 } from '../../utils'
import references from 'Règles/ressources/références/références.yaml'
import './References.css'
export default function References({ refs }) {
const renderRef = ([name, link]) => {
let refKey = findRefKey(link),
refData = (refKey && references[refKey]) || {},
domain = cleanDomain(link)
const findRefKey = link =>
Object.keys(references).find(r => link.indexOf(r) > -1)
return (
<li key={name}>
<span className="imageWrapper">
{refData.image && (
<img
src={require('Règles/ressources/références/' + refData.image)}
/>
)}
</span>
<a href={link} target="_blank">
{capitalise0(name)}
</a>
<span className="url">{domain}</span>
</li>
)
}
const findRefKey = link =>
Object.keys(references).find(r => link.indexOf(r) > -1)
const cleanDomain = link =>
(link.indexOf('://') > -1 ? link.split('/')[2] : link.split('/')[0]).replace(
'www.',
''
)
const cleanDomain = link =>
(link.indexOf('://') > -1
? link.split('/')[2]
: link.split('/')[0]
).replace('www.', '')
let references = toPairs(refs)
return <ul className="references">{references.map(renderRef)}</ul>
function Ref({ name, link }) {
let refKey = findRefKey(link),
refData = (refKey && references[refKey]) || {},
domain = cleanDomain(link)
return (
<li key={name}>
<span className="imageWrapper">
{refData.image && (
<img src={require('Règles/ressources/références/' + refData.image)} />
)}
</span>
<a href={link} target="_blank">
{capitalise0(name)}
</a>
<span className="url">{domain}</span>
</li>
)
}
export default function References({ refs }) {
let references = toPairs(refs)
return (
<ul className="references">
{references.map(([name, link]) => (
<Ref key={link} name={name} link={link} />
))}
</ul>
)
}

View File

@ -1,4 +1,3 @@
import { buildDottedName } from 'Engine/rules'
import { safeDump } from 'js-yaml'
import React from 'react'
import emoji from 'react-easy-emoji'
@ -6,7 +5,7 @@ import rules from 'Règles/base.yaml'
import ColoredYaml from './ColoredYaml'
export default function RuleSource({ dottedName }) {
let source = rules.filter(rule => buildDottedName(rule).includes(dottedName))
let source = rules[dottedName]
return (
<div id="RuleSource" className="ui__ container">

View File

@ -58,7 +58,7 @@ export function formatValue({
if (typeof value !== 'number') {
return value
}
const serializedUnit = typeof unit == 'object' ? serialiseUnit(unit) : unit
const serializedUnit = serialiseUnit(unit, value)
switch (serializedUnit) {
case '€':

View File

@ -25,7 +25,7 @@ import {
} from 'ramda'
import React from 'react'
import { findRuleByDottedName, queryRule } from './rules'
import {serialiseUnit} from 'Engine/units'
import { serialiseUnit } from 'Engine/units'
/*
COLLECTE DES VARIABLES MANQUANTES

View File

@ -10,8 +10,16 @@ let inputToStateSelector = rules => input => dottedName =>
...input
}[dottedName])
let enrichRules = input =>
(typeof input === 'string' ? safeLoad(input) : input).map(enrichRule)
let enrichRules = input => {
const rules = typeof input === 'string' ? safeLoad(input) : input
const rulesList = Array.isArray(rules)
? rules
: Object.entries(rules).map(([dottedName, rule]) => ({
dottedName,
...rule
}))
return rulesList.map(enrichRule)
}
export default {
evaluate: (targetInput, input, config) => {

View File

@ -37,13 +37,14 @@ Functions working on one rule */
export let enrichRule = rule => {
try {
let unit = rule.unité && parseUnit(rule.unité)
const dottedName = rule.dottedName || rule.nom
const name = nameLeaf(dottedName)
return {
...rule,
dottedName,
name,
type: possibleVariableTypes.find(t => has(t, rule) || rule.type === t),
name: rule['nom'],
title: capitalise0(rule['titre'] || rule['nom']),
ns: rule['espace'],
dottedName: buildDottedName(rule),
title: capitalise0(rule['titre'] || name),
defaultValue: rule['par défaut'],
examples: rule['exemples'],
icons: rule['icônes'],
@ -56,9 +57,6 @@ export let enrichRule = rule => {
}
}
export let buildDottedName = rule =>
rule['espace'] ? [rule['espace'], rule['nom']].join(' . ') : rule['nom']
// les variables dans les tests peuvent être exprimées relativement à l'espace de nom de la règle,
// comme dans sa formule
export let disambiguateExampleSituation = (rules, rule) =>
@ -166,7 +164,7 @@ export let findRule = (rules, nameOrDottedName) =>
: findRuleByName(rules, nameOrDottedName)
export let findRuleByNamespace = (allRules, ns) =>
allRules.filter(propEq('ns', ns))
allRules.filter(rule => parentName(rule.dottedName) === ns)
/*********************************
Autres */
@ -186,7 +184,7 @@ export let nestedSituationToPathMap = situation => {
/* Traduction */
export let translateAll = (translations, flatRules) => {
let translationsOf = rule => translations[buildDottedName(rule)],
let translationsOf = rule => translations[rule.dottedName],
translateProp = (lang, translation) => (rule, prop) => {
let propTrans = translation[prop + '.' + lang]
if (prop === 'suggestions' && propTrans)
@ -223,12 +221,17 @@ export let translateAll = (translations, flatRules) => {
return map(translateRule('en', translations, targets), flatRules)
}
const rulesList = Object.entries(rawRules).map(([dottedName, rule]) => ({
dottedName,
...rule
}))
// On enrichit la base de règles avec des propriétés dérivées de celles du YAML
export let rules = translateAll(translations, rawRules).map(rule =>
export let rules = translateAll(translations, rulesList).map(rule =>
enrichRule(rule)
)
export let rulesFr = rawRules.map(rule => enrichRule(rule))
export let rulesFr = rulesList.map(rule => enrichRule(rule))
export let findParentDependency = (rules, rule) => {
// A parent dependency means that one of a rule's parents is not just a namespace holder, it is a boolean question. E.g. is it a fixed-term contract, yes / no

View File

@ -1,4 +1,5 @@
import { remove, isEmpty, unnest } from 'ramda'
import i18n from '../i18n'
//TODO this function does not handle complex units like passenger-kilometer/flight
export let parseUnit = string => {
@ -10,12 +11,21 @@ export let parseUnit = string => {
return result
}
let printUnits = units => units.filter(unit => unit !== '%').join('-')
let printUnits = (units, count) =>
units
.filter(unit => unit !== '%')
.map(unit => i18n.t(`units:${unit}`, { count }))
.join('-')
export let serialiseUnit = rawUnit => {
const plural = 2
export let serialiseUnit = (rawUnit, count = plural) => {
if (typeof rawUnit !== 'object') {
return typeof rawUnit === 'string'
? i18n.t(`units:${rawUnit}`, { count })
: 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 '%'
@ -26,10 +36,10 @@ export let serialiseUnit = rawUnit => {
!n && !d
? ''
: n && !d
? printUnits(numerators)
? printUnits(numerators, count)
: !n && d
? `/${printUnits(denominators)}`
: `${printUnits(numerators)} / ${printUnits(denominators)}`
? `/${printUnits(denominators, 1)}`
: `${printUnits(numerators, plural)} / ${printUnits(denominators, 1)}`
return string
}

View File

@ -1,34 +1,26 @@
import i18next from 'i18next'
import queryString from 'query-string'
import { initReactI18next } from 'react-i18next'
import enTranslations from './locales/en.yaml'
import {
getFromSessionStorage,
getIframeOption,
parseDataAttributes,
setToSessionStorage
} from './utils'
import unitsTranslations from './locales/units.yaml'
let lang =
getIframeOption('lang') ||
(typeof location !== 'undefined' &&
queryString.parse(location.search)['lang']) ||
parseDataAttributes(getFromSessionStorage('lang')) ||
new URLSearchParams(document.location.search.substring(1)).get('lang') ||
sessionStorage?.getItem('lang')?.match(/^(fr|en)$/)?.[0] ||
'fr'
setToSessionStorage('lang', lang)
i18next.use(initReactI18next).init(
{
sessionStorage?.setItem('lang', lang)
i18next
.use(initReactI18next)
.init({
lng: lang,
resources: {
fr: { units: unitsTranslations.fr },
en: {
translation: enTranslations
translation: enTranslations,
units: unitsTranslations.en
}
}
},
(err, t) => {
console && console.error('Error from i18n load', err, t) //eslint-disable-line no-console
}
)
})
.catch(err => console?.error('Error from i18n load', err))
export default i18next

20
source/locales/units.yaml Normal file
View File

@ -0,0 +1,20 @@
fr:
heure_plural: heures
jour_plural: jours
semaine_plural: semaines
trimestre_plural: trimestres
employé_plural: employés
points_plural: points
en:
heure: hour
heure_plural: hours
jour: day
jour_plural: days
semaine: week
semaine_plural: weeks
trimestre: quarter
trimestre_plural: quarters
repas: meal
repas_plural: meals
employé: employee
employé_plural: employees

File diff suppressed because it is too large Load Diff

View File

@ -4,18 +4,16 @@
# bloquant :
# - ?
- nom: douche
douche:
icônes: 🚿
- espace: douche
nom: impact
douche . impact:
icônes: 🍃
période: flexible
unité: kgCO2eq
formule: impact par douche * douche . nombre
- espace: douche
nom: nombre
douche . nombre:
période: flexible
question: Combien prenez-vous de douches ?
unité: _
@ -23,21 +21,17 @@
suggestions:
Une par jour: 30
- espace: douche
nom: impact par douche
douche . impact par douche:
formule: impact par litre * litres d'eau
- espace: douche
nom: impact par litre
douche . impact par litre:
formule: eau . impact par litre froid + chauffage . impact par litre
- espace: douche
douche . litres d'eau:
icônes: 🇱
nom: litres d'eau
formule: durée de la douche * litres par minute
- espace: douche
nom: litres par minute
douche . litres par minute:
formule:
variations:
- si: pomme de douche économe
@ -46,24 +40,21 @@
références:
économise l'eau: https://www.jeconomiseleau.org/index.php/particuliers/economies-par-usage/la-douche-et-le-bain
- espace: douche
nom: pomme de douche économe
douche . pomme de douche économe:
question: Utilisez-vous une pomme de douche économe ?
par défaut: non
- nom: eau
eau:
icônes: 💧
- espace: eau
nom: impact par litre froid
eau . impact par litre froid:
unité: kgCO2eq/l
formule: 0.000132
- nom: chauffage
chauffage:
icônes: 🔥
- espace: chauffage
nom: type
chauffage . type:
question: Comment est chauffée votre eau ?
formule:
une possibilité:
@ -74,20 +65,16 @@
- électricité
par défaut: gaz
- espace: chauffage . type
nom: gaz
chauffage . type . gaz:
icônes: 🔵
- espace: chauffage . type
nom: fioul
chauffage . type . fioul:
icônes: 🛢️
- espace: chauffage . type
nom: électricité
chauffage . type . électricité:
icônes:
# définir ces éléments un par un
- espace: chauffage
nom: impact par kWh
chauffage . impact par kWh:
unité: kgCO2eq/kWh PCI
formule:
variations:
@ -107,15 +94,13 @@
électricité: https://www.electricitymap.org/?page=country&solar=false&remote=true&wind=false&countryCode=FR
électricité sur Décrypter l'Energie: https://decrypterlenergie.org/decryptage-quel-est-le-contenu-en-co2-du-kwh-electrique
- espace: chauffage
nom: énergie consommée par litre
chauffage . énergie consommée par litre:
formule: 0.0325
unité: kWh
références:
analyse du prix d'une douche: https://www.econologie.com/forums/plomberie-et-sanitaire/prix-reel-d-un-bain-ou-d-une-douche-pour-l-eau-et-chauffage-t12727.html
- espace: chauffage
nom: impact par litre
chauffage . impact par litre:
formule: impact par kWh * énergie consommée par litre
# Meilleure syntaxe : nouveau mécanisme correspondance
@ -126,8 +111,7 @@
# fioul: 50
# électricité: 2
- espace: douche
nom: durée de la douche
douche . durée de la douche:
question: Combien de temps dure votre douche en général ?
unité: _
par défaut: 5

View File

@ -166,35 +166,6 @@ contrat salarié . indemnité kilométrique vélo . active:
revenu. Pour verser une prime de salaire équivalente à son salarié sans ce
dispositif, **l'employeur devrait débourser près de 500€ pour un salaire
médian**.
contrat salarié . CDD . CIF:
description.en: >-
Contribution to the financing of individual training leave, specific to
fixed-term contracts.
description.fr: >-
Contribution au financement du congé individuel de formation spécifique aux
CDD.
titre.en: CIF
titre.fr: CIF
contrat salarié . CDD . compensation pour congés non pris:
description.en: >-
The employee on a fixed-term contract has the same rights for paid leave as
the employee on a permanent contract. He acquires and takes his paid leave
under the same conditions.
It is however common that the employee can not take all his leave before the
end of his contract. In that case, he receives a compensatory alowance paid
by the employer.
description.fr: >
Le salarié en CDD bénéficie des mêmes droits à congés payés que le salarié
en CDI. Il acquiert et prend ses congés payés dans les mêmes conditions.
Il est cependant courant que le salarié ne puisse pas prendre tous ses
congés avant le terme de son contrat, il bénéficie alors d'une indemnité
compensatrice de congés payés versée par l'employeur.
titre.en: untaken vacation compensation
titre.fr: compensation pour congés non pris
? contrat salarié . CDD . compensation pour congés non pris . proportion congés non pris
: titre.en: proportion of untaken leave
titre.fr: proportion congés non pris

View File

@ -1,18 +1,18 @@
# Ce petit ensemble de règles a été historiquement utilisé pour tester l'externalisation du moteur, et est en train d'être réintégré progressivement dans la base centrale
- nom: chiffre affaires
chiffre affaires:
période: flexible
unité:
- nom: charges
charges:
période: flexible
par défaut: 0
unité:
- nom: répartition salaire sur dividendes
répartition salaire sur dividendes:
par défaut: 0.5
- nom: impôt sur les sociétés
impôt sur les sociétés:
période: année
formule:
barème:
@ -28,25 +28,22 @@
références:
fiche service-public.fr: https://www.service-public.fr/professionnels-entreprises/vosdroits/F23575
- nom: bénéfice
bénéfice:
période: flexible
formule: chiffre affaires - salaire total
- nom: dividendes
dividendes:
- espace: dividendes
dividendes . brut:
période: flexible
nom: brut
formule: bénéfice - impôt sur les sociétés
- espace: dividendes
dividendes . net:
période: flexible
nom: net
formule: brut - prélèvement forfaitaire unique
- nom: prélèvement forfaitaire unique
dividendes . prélèvement forfaitaire unique:
période: flexible
espace: dividendes
formule:
multiplication:
assiette: brut
@ -54,10 +51,10 @@
- taux: 17.2%
- taux: 12.8%
- nom: salaire total
salaire total:
période: flexible
formule: chiffre affaires * répartition salaire sur dividendes
- nom: revenu net après impôt
revenu net après impôt:
période: flexible
formule: contrat salarié . rémunération . net après impôt + dividendes . net

View File

@ -19,7 +19,7 @@ import {
retrievePersistedSimulation
} from '../../storage/persistSimulation'
import Tracker, { devTracker } from '../../Tracker'
import { inIframe, setToSessionStorage } from '../../utils'
import { inIframe } from '../../utils'
import './App.css'
import Footer from './layout/Footer/Footer'
import { PrivacyContent } from './layout/Footer/Privacy'
@ -59,7 +59,7 @@ const middlewares = [
function InFranceRoute({ basename, language }) {
useEffect(() => {
setToSessionStorage('lang', language)
sessionStorage?.setItem('lang', language)
}, [language])
const paths = constructLocalizedSitePath(language)
const rules = language === 'en' ? baseRulesEn : baseRulesFr

View File

@ -1,13 +1,13 @@
- nom: revenu imposable
revenu imposable:
format: euros
- nom: revenu abattu
revenu abattu:
formule:
allègement:
assiette: revenu imposable
abattement: 10%
- nom: impôt sur le revenu
impôt sur le revenu:
formule:
barème:
assiette: revenu abattu
@ -26,8 +26,7 @@
- au-dessus de: 153783
taux: 45%
- nom: impôt final
impôt final:
formule:
allègement:
assiette: impôt sur le revenu

View File

@ -1,19 +1,15 @@
- espace: douche
nom: impact par douche
douche . impact par douche:
titre: Une douche
icônes: 🚿
formule: impact par litre * litres d'eau
- espace: douche
nom: impact par litre
douche . impact par litre:
formule: eau . impact par litre froid + chauffage . impact par litre
- espace: douche
nom: litres d'eau
douche . litres d'eau:
formule: durée de la douche * litres par minute
- espace: douche
nom: litres par minute
douche . litres par minute:
unité: l/minute
formule:
variations:
@ -23,13 +19,11 @@
références:
- https://www.jeconomiseleau.org/index.php/particuliers/economies-par-usage/la-douche-et-le-bain
- espace: douche
nom: pomme de douche économe
douche . pomme de douche économe:
question: Utilisez-vous une pomme de douche économe ?
par défaut: non
- espace: douche
nom: durée de la douche
douche . durée de la douche:
question: Combien de temps dure votre douche en général (en minutes) ?
par défaut: 5
unité: minute
@ -38,15 +32,10 @@
moyenne: 10
lente: 20
- nom: chauffage
chauffage:
icônes: 🔥
- espace: chauffage
nom: type
chauffage . type:
question: Votre eau est chauffée comment ?
formule:
une possibilité:
@ -57,10 +46,7 @@
- électricité
par défaut: gaz
- espace: chauffage
nom: impact par kWh
chauffage . impact par kWh:
unité: kgCO₂e/kWh
formule:
variations:
@ -79,17 +65,12 @@
- https://www.electricitymap.org/?page=country&solar=false&remote=true&wind=false&countryCode=FR
- https://decrypterlenergie.org/decryptage-quel-est-le-contenu-en-co2-du-kwh-electrique
- espace: chauffage
nom: énergie consommée par litre
chauffage . énergie consommée par litre:
formule: 0.0325
unité: kWh
références:
- https://www.econologie.com/forums/plomberie-et-sanitaire/prix-reel-d-un-bain-ou-d-une-douche-pour-l-eau-et-chauffage-t12727.html
- espace: chauffage
nom: impact par litre
chauffage . impact par litre:
titre: impact par litre chauffé
formule: impact par kWh * énergie consommée par litre

View File

@ -4,31 +4,6 @@ import { map } from 'ramda'
export let capitalise0 = (name: string) =>
name && name[0].toUpperCase() + name.slice(1)
export let getUrl = () =>
typeof window !== 'undefined' ? window.location.href.toString() : null
export let parseDataAttributes = (value: any) =>
value === 'undefined'
? undefined
: value === null
? null
: !isNaN(value)
? +value
: /* value is a normal string */
value
export let getIframeOption = (optionName: string) => {
let url = getUrl(),
hasOption = url?.includes(optionName + '=')
return parseDataAttributes(
hasOption && url.split(optionName + '=')[1].split('&')[0]
)
}
export function isNumeric(val: number) {
return Number(parseFloat(val)) === val
}
export function debounce<ArgType: any>(
timeout: number,
fn: ArgType => void
@ -94,13 +69,3 @@ export const constructSitePaths = (
sitePaths
)
})
export const getFromSessionStorage = softCatch<string, any>(where => {
typeof sessionStorage !== 'undefined' ? sessionStorage[where] : null
})
export const setToSessionStorage = softCatch<string, void>((where, what) => {
if (typeof sessionStorage !== 'undefined') {
sessionStorage[where] = what
}
})

View File

@ -16,11 +16,11 @@ describe('conversation', function() {
it('should start with the first missing variable', function() {
let rawRules = [
// TODO - this won't work without the indirection, figure out why
{ nom: 'startHere', formule: { somme: ['a', 'b'] }, espace: 'top' },
{ nom: 'a', espace: 'top', formule: 'aa' },
{ nom: 'b', espace: 'top', formule: 'bb' },
{ nom: 'aa', question: '?', titre: 'a', espace: 'top' },
{ nom: 'bb', question: '?', titre: 'b', espace: 'top' }
{ nom: 'top . startHere', formule: { somme: ['a', 'b'] } },
{ nom: 'top . a', formule: 'aa' },
{ nom: 'top . b', formule: 'bb' },
{ nom: 'top . aa', question: '?', titre: 'a' },
{ nom: 'top . bb', question: '?', titre: 'b' }
],
rules = rawRules.map(enrichRule),
state = merge(baseState, {
@ -34,16 +34,15 @@ describe('conversation', function() {
let rawRules = [
// TODO - this won't work without the indirection, figure out why
{
nom: 'startHere',
formule: { somme: ['a', 'b', 'c'] },
espace: 'top'
nom: 'top . startHere',
formule: { somme: ['a', 'b', 'c'] }
},
{ nom: 'a', espace: 'top', formule: 'aa' },
{ nom: 'b', espace: 'top', formule: 'bb' },
{ nom: 'c', espace: 'top', formule: 'cc' },
{ nom: 'aa', question: '?', titre: 'a', espace: 'top' },
{ nom: 'bb', question: '?', titre: 'b', espace: 'top' },
{ nom: 'cc', question: '?', titre: 'c', espace: 'top' }
{ nom: 'top . a', formule: 'aa' },
{ nom: 'top . b', formule: 'bb' },
{ nom: 'top . c', formule: 'cc' },
{ nom: 'top . aa', question: '?', titre: 'a' },
{ nom: 'top . bb', question: '?', titre: 'b' },
{ nom: 'top . cc', question: '?', titre: 'c' }
],
rules = rawRules.map(enrichRule)

View File

@ -45,7 +45,7 @@ describe('pay slip selector', function() {
// $FlowFixMe
let cotisationsSanté = (cotisations.find(([branche]) =>
branche.includes('santé')
) || [])[1].map(cotisation => cotisation.nom)
) || [])[1].map(cotisation => cotisation.name)
expect(cotisationsSanté).to.have.lengthOf(2)
expect(cotisationsSanté).to.include('maladie')
expect(cotisationsSanté).to.include('complémentaire santé')

View File

@ -13,19 +13,17 @@ describe('collectMissingVariables', function() {
let rawRules = [
{ nom: 'sum' },
{
nom: 'startHere',
nom: 'sum . startHere',
formule: 2,
'non applicable si': 'sum . evt . ko',
espace: 'sum'
'non applicable si': 'sum . evt . ko'
},
{
nom: 'evt',
espace: 'sum',
nom: 'sum . evt',
formule: { 'une possibilité': ['ko'] },
titre: 'Truc',
question: '?'
},
{ nom: 'ko', espace: 'sum . evt' }
{ nom: 'sum . evt . ko' }
],
rules = parseAll(rawRules.map(enrichRule)),
analysis = analyse(rules, 'startHere')(stateSelector),
@ -37,15 +35,14 @@ describe('collectMissingVariables', function() {
it('should identify missing variables mentioned in expressions', function() {
let rawRules = [
{ nom: 'sum' },
{ nom: 'evt', espace: 'sum' },
{ nom: 'sum . evt' },
{
nom: 'startHere',
nom: 'sum . startHere',
formule: 2,
'non applicable si': 'evt . nyet > evt . nope',
espace: 'sum'
'non applicable si': 'evt . nyet > evt . nope'
},
{ nom: 'nope', espace: 'sum . evt' },
{ nom: 'nyet', espace: 'sum . evt' }
{ nom: 'sum . evt . nope' },
{ nom: 'sum . evt . nyet' }
],
rules = parseAll(rawRules.map(enrichRule)),
analysis = analyse(rules, 'startHere')(stateSelector),
@ -59,12 +56,11 @@ describe('collectMissingVariables', function() {
let rawRules = [
{ nom: 'sum' },
{
nom: 'startHere',
nom: 'sum . startHere',
formule: 'trois',
'non applicable si': '3 > 2',
espace: 'sum'
'non applicable si': '3 > 2'
},
{ nom: 'trois', espace: 'sum' }
{ nom: 'sum . trois' }
],
rules = parseAll(rawRules.map(enrichRule)),
analysis = analyse(rules, 'startHere')(stateSelector),
@ -77,14 +73,13 @@ describe('collectMissingVariables', function() {
let rawRules = [
{ nom: 'sum' },
{
nom: 'startHere',
nom: 'sum . startHere',
formule: 'trois',
'non applicable si': {
'une de ces conditions': ['3 > 2', 'trois']
},
espace: 'sum'
}
},
{ nom: 'trois', espace: 'sum' }
{ nom: 'sum . trois' }
],
rules = parseAll(rawRules.map(enrichRule)),
analysis = analyse(rules, 'startHere')(stateSelector),
@ -96,11 +91,10 @@ describe('collectMissingVariables', function() {
it('should report "une possibilité" as a missing variable even though it has a formula', function() {
let rawRules = [
{ nom: 'top' },
{ nom: 'startHere', formule: 'trois', espace: 'top' },
{ nom: 'top . startHere', formule: 'trois' },
{
nom: 'trois',
formule: { 'une possibilité': ['ko'] },
espace: 'top'
nom: 'top . trois',
formule: { 'une possibilité': ['ko'] }
}
],
rules = parseAll(rawRules.map(enrichRule)),
@ -113,12 +107,11 @@ describe('collectMissingVariables', function() {
it('should not report missing variables when "une possibilité" is inapplicable', function() {
let rawRules = [
{ nom: 'top' },
{ nom: 'startHere', formule: 'trois', espace: 'top' },
{ nom: 'top . startHere', formule: 'trois' },
{
nom: 'trois',
nom: 'top . trois',
formule: { 'une possibilité': ['ko'] },
'non applicable si': 1,
espace: 'top'
'non applicable si': 1
}
],
rules = parseAll(rawRules.map(enrichRule)),
@ -134,11 +127,10 @@ describe('collectMissingVariables', function() {
let rawRules = [
{ nom: 'top' },
{ nom: 'startHere', formule: 'trois', espace: 'top' },
{ nom: 'top . startHere', formule: 'trois' },
{
nom: 'trois',
formule: { 'une possibilité': ['ko'] },
espace: 'top'
nom: 'top . trois',
formule: { 'une possibilité': ['ko'] }
}
],
rules = parseAll(rawRules.map(enrichRule)),
@ -152,17 +144,16 @@ describe('collectMissingVariables', function() {
let rawRules = [
{ nom: 'top' },
{
nom: 'startHere',
nom: 'top . startHere',
formule: {
'aiguillage numérique': {
'11 > dix': '1000%',
'3 > dix': '1100%',
'1 > dix': '1200%'
}
},
espace: 'top'
}
},
{ nom: 'dix', espace: 'top' }
{ nom: 'top . dix' }
],
rules = parseAll(rawRules.map(enrichRule)),
analysis = analyse(rules, 'startHere')(stateSelector),
@ -176,13 +167,11 @@ describe('collectMissingVariables', function() {
let rawRules = [
{ nom: 'top' },
{
nom: 'startHere',
formule: { somme: ['variations'] },
espace: 'top'
nom: 'top . startHere',
formule: { somme: ['variations'] }
},
{
nom: 'variations',
espace: 'top',
nom: 'top . variations',
formule: {
barème: {
assiette: 2008,
@ -213,10 +202,10 @@ describe('collectMissingVariables', function() {
}
}
},
{ nom: 'dix', espace: 'top' },
{ nom: 'deux', espace: 'top' },
{ nom: 'trois', espace: 'top' },
{ nom: 'quatre', espace: 'top' }
{ nom: 'top . dix' },
{ nom: 'top . deux' },
{ nom: 'top . trois' },
{ nom: 'top . quatre' }
],
rules = parseAll(rawRules.map(enrichRule)),
analysis = analyse(rules, 'startHere')(stateSelector),
@ -233,16 +222,15 @@ describe('collectMissingVariables', function() {
let rawRules = [
{ nom: 'top' },
{
nom: 'startHere',
nom: 'top . startHere',
formule: {
'aiguillage numérique': {
'8 > 10': '1000%',
'1 > 2': 'dix'
}
},
espace: 'top'
}
},
{ nom: 'dix', espace: 'top' }
{ nom: 'top . dix' }
],
rules = parseAll(rawRules.map(enrichRule)),
analysis = analyse(rules, 'startHere')(stateSelector),
@ -255,7 +243,7 @@ describe('collectMissingVariables', function() {
let rawRules = [
{ nom: 'top' },
{
nom: 'startHere',
nom: 'top . startHere',
formule: {
'aiguillage numérique': {
'10 > 11': '1000%',
@ -264,11 +252,10 @@ describe('collectMissingVariables', function() {
'1 > 2': '75015%'
}
}
},
espace: 'top'
}
},
{ nom: 'douze', espace: 'top' },
{ nom: 'dix', espace: 'top' }
{ nom: 'top . douze' },
{ nom: 'top . dix' }
],
rules = parseAll(rawRules.map(enrichRule)),
analysis = analyse(rules, 'startHere')(stateSelector),
@ -282,17 +269,16 @@ describe('collectMissingVariables', function() {
let rawRules = [
{ nom: 'top' },
{
nom: 'startHere',
nom: 'top . startHere',
formule: {
'aiguillage numérique': {
'11 > 10': '1000%',
'3 > dix': '1100%',
'1 > dix': '1200%'
}
},
espace: 'top'
}
},
{ nom: 'dix', espace: 'top' }
{ nom: 'top . dix' }
],
rules = parseAll(rawRules.map(enrichRule)),
analysis = analyse(rules, 'startHere')(stateSelector),
@ -306,16 +292,14 @@ describe('nextSteps', function() {
it('should generate questions for simple situations', function() {
let rawRules = [
{ nom: 'top' },
{ nom: 'sum', formule: 'deux', espace: 'top' },
{ nom: 'top . sum', formule: 'deux' },
{
nom: 'deux',
nom: 'top . deux',
formule: 2,
'non applicable si': 'top . sum . evt',
espace: 'top'
'non applicable si': 'top . sum . evt'
},
{
nom: 'evt',
espace: 'top . sum',
nom: 'top . sum . evt',
titre: 'Truc',
question: '?'
}
@ -330,15 +314,13 @@ describe('nextSteps', function() {
it('should generate questions', function() {
let rawRules = [
{ nom: 'top' },
{ nom: 'sum', formule: 'deux', espace: 'top' },
{ nom: 'top . sum', formule: 'deux' },
{
nom: 'deux',
formule: 'sum . evt',
espace: 'top'
nom: 'top . deux',
formule: 'sum . evt'
},
{
nom: 'evt',
espace: 'top . sum',
nom: 'top . sum . evt',
question: '?'
}
],
@ -355,21 +337,19 @@ describe('nextSteps', function() {
it.skip('should generate questions with more intricate situation', function() {
let rawRules = [
{ nom: 'top' },
{ nom: 'sum', formule: { somme: [2, 'deux'] }, espace: 'top' },
{ nom: 'top . sum', formule: { somme: [2, 'deux'] } },
{
nom: 'deux',
nom: 'top . deux',
formule: 2,
'non applicable si': "top . sum . evt = 'ko'",
espace: 'top'
'non applicable si': "top . sum . evt = 'ko'"
},
{
nom: 'evt',
espace: 'top . sum',
nom: 'top . sum . evt',
formule: { 'une possibilité': ['ko'] },
titre: 'Truc',
question: '?'
},
{ nom: 'ko', espace: 'top . sum . evt' }
{ nom: 'top . sum . evt . ko' }
],
rules = parseAll(rawRules.map(enrichRule)),
analysis = analyse(rules, 'sum')(stateSelector),

View File

@ -10,7 +10,6 @@ import { analyse, parseAll } from '../source/engine/traverse'
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', () =>
@ -43,7 +42,7 @@ describe('Mécanismes', () =>
missing = collectMissingVariables(analysis.targets),
target = analysis.targets[0]
if (isNumeric(valeur)) {
if (typeof valeur === 'number') {
expect(target.nodeValue).to.be.closeTo(valeur, 0.001)
} else if (valeur !== undefined) {
expect(target).to.have.property('nodeValue', valeur)

View File

@ -140,8 +140,7 @@
- nom: CDD
- espace: CDD
nom: poursuivi en CDI
- nom: CDD . poursuivi en CDI
- test: variable booléene
formule: CDD . poursuivi en CDI
@ -188,10 +187,8 @@
- commerciale
- artisanale
- espace: catégorie d'activité
nom: artisanale
- espace: catégorie d'activité
nom: commerciale
- nom: catégorie d'activité . artisanale
- nom: catégorie d'activité . commerciale
- test: test de possibilités
formule: catégorie d'activité = 'artisanale'

View File

@ -16,7 +16,7 @@ describe('enrichRule', function() {
})
it('should extract the dotted name of the rule', function() {
let rule = { espace: 'contrat salarié', nom: 'CDD' }
let rule = { nom: 'contrat salarié . CDD' }
expect(enrichRule(rule)).to.have.property('name', 'CDD')
expect(enrichRule(rule)).to.have.property(
'dottedName',
@ -77,8 +77,7 @@ describe('translateAll', function() {
it('should translate flat rules', function() {
let rules = [
{
espace: 'foo',
nom: 'bar',
nom: 'foo . bar',
titre: 'Titre',
description: 'Description',
question: 'Question'
@ -115,16 +114,14 @@ describe('misc', function() {
it('should procude an array of the parents of a rule', function() {
let rawRules = [
{ nom: 'CDD', question: 'CDD ?' },
{ nom: 'taxe', formule: 'montant annuel / 12', espace: 'CDD' },
{ nom: 'CDD . taxe', formule: 'montant annuel / 12' },
{
nom: 'montant annuel',
formule: '20 - exonération annuelle',
espace: 'CDD . taxe'
nom: 'CDD . taxe . montant annuel',
formule: '20 - exonération annuelle'
},
{
nom: 'exonération annuelle',
formule: 20,
espace: 'CDD . taxe . montant annuel'
nom: 'CDD . taxe . montant annuel . exonération annuelle',
formule: 20
}
]
@ -138,16 +135,14 @@ describe('misc', function() {
it("should disambiguate a reference to another rule in a rule, given the latter's namespace", function() {
let rawRules = [
{ nom: 'CDD', question: 'CDD ?' },
{ nom: 'taxe', formule: 'montant annuel / 12', espace: 'CDD' },
{ nom: 'CDD . taxe', formule: 'montant annuel / 12' },
{
nom: 'montant annuel',
formule: '20 - exonération annuelle',
espace: 'CDD . taxe'
nom: 'CDD . taxe . montant annuel',
formule: '20 - exonération annuelle'
},
{
nom: 'exonération annuelle',
formule: 20,
espace: 'CDD . taxe . montant annuel'
nom: 'CDD . taxe . montant annuel . exonération annuelle',
formule: 20
}
]

View File

@ -26,8 +26,8 @@ describe('analyse on raw rules', function() {
it('should handle direct referencing of a variable', function() {
let rawRules = [
{ nom: 'top' },
{ nom: 'startHere', formule: 'dix', espace: 'top' },
{ nom: 'dix', formule: 10, espace: 'top' }
{ nom: 'top . startHere', formule: 'dix' },
{ nom: 'top . dix', formule: 10 }
],
rules = parseAll(rawRules.map(enrichRule))
expect(
@ -38,8 +38,8 @@ describe('analyse on raw rules', function() {
it('should handle expressions referencing other rules', function() {
let rawRules = [
{ nom: 'top' },
{ nom: 'startHere', formule: '3259 + dix', espace: 'top' },
{ nom: 'dix', formule: 10, espace: 'top' }
{ nom: 'top . startHere', formule: '3259 + dix' },
{ nom: 'top . dix', formule: 10 }
],
rules = parseAll(rawRules.map(enrichRule))
expect(
@ -50,9 +50,9 @@ describe('analyse on raw rules', function() {
it('should handle applicability conditions', function() {
let rawRules = [
{ nom: 'top' },
{ nom: 'startHere', formule: '3259 + dix', espace: 'top' },
{ nom: 'dix', formule: 10, espace: 'top', 'non applicable si': 'vrai' },
{ nom: 'vrai', formule: '2 > 1', espace: 'top' }
{ nom: 'top . startHere', formule: '3259 + dix' },
{ nom: 'top . dix', formule: 10, 'non applicable si': 'vrai' },
{ nom: 'top . vrai', formule: '2 > 1' }
],
rules = parseAll(rawRules.map(enrichRule))
expect(
@ -63,8 +63,8 @@ describe('analyse on raw rules', function() {
it('should handle comparisons', function() {
let rawRules = [
{ nom: 'top' },
{ nom: 'startHere', formule: '3259 > dix', espace: 'top' },
{ nom: 'dix', formule: 10, espace: 'top' }
{ nom: 'top . startHere', formule: '3259 > dix' },
{ nom: 'top . dix', formule: 10 }
],
rules = parseAll(rawRules.map(enrichRule))
expect(
@ -115,17 +115,16 @@ describe('analyse with mecanisms', function() {
let rawRules = [
{ nom: 'top' },
{
nom: 'startHere',
nom: 'top . startHere',
formule: {
'aiguillage numérique': {
'1 > dix': '1000%',
'3 < dix': '1100%',
'3 > dix': '1200%'
}
},
espace: 'top'
}
},
{ nom: 'dix', formule: 10, espace: 'top' }
{ nom: 'top . dix', formule: 10 }
],
rules = parseAll(rawRules.map(enrichRule))
expect(
@ -134,10 +133,7 @@ describe('analyse with mecanisms', function() {
})
it('should handle percentages', function() {
let rawRules = [
{ nom: 'top' },
{ nom: 'startHere', formule: '35%', espace: 'top' }
],
let rawRules = [{ nom: 'top' }, { nom: 'top . startHere', formule: '35%' }],
rules = parseAll(rawRules.map(enrichRule))
expect(
analyse(rules, 'startHere')(stateSelector).targets[0]
@ -323,11 +319,10 @@ describe('analyse with mecanisms', function() {
let rawRules = [
{ nom: 'top' },
{
nom: 'startHere',
formule: { complément: { cible: 'dix', montant: 93 } },
espace: 'top'
nom: 'top . startHere',
formule: { complément: { cible: 'dix', montant: 93 } }
},
{ nom: 'dix', formule: 17, espace: 'top' }
{ nom: 'top . dix', formule: 17 }
],
rules = parseAll(rawRules.map(enrichRule))
expect(
@ -339,16 +334,15 @@ describe('analyse with mecanisms', function() {
let rawRules = [
{ nom: 'top' },
{
nom: 'startHere',
nom: 'top . startHere',
formule: {
complément: {
cible: 'dix',
composantes: [{ montant: 93 }, { montant: 93 }]
}
},
espace: 'top'
}
},
{ nom: 'dix', formule: 17, espace: 'top' }
{ nom: 'top . dix', formule: 17 }
],
rules = parseAll(rawRules.map(enrichRule))
expect(
@ -359,10 +353,9 @@ describe('analyse with mecanisms', function() {
it('should handle filtering on components', function() {
let rawRules = [
{ nom: 'top' },
{ nom: 'startHere', espace: 'top', formule: 'composed [salarié]' },
{ nom: 'top . startHere', formule: 'composed [salarié]' },
{
nom: 'composed',
espace: 'top',
nom: 'top . composed',
formule: {
barème: {
assiette: 2008,
@ -399,14 +392,12 @@ describe('analyse with mecanisms', function() {
let rawRules = [
{ nom: 'top' },
{
nom: 'startHere',
espace: 'top',
nom: 'top . startHere',
formule: 'composed [salarié] + composed [employeur]'
},
{ nom: 'orHere', espace: 'top', formule: 'composed' },
{ nom: 'top . orHere', formule: 'composed' },
{
nom: 'composed',
espace: 'top',
nom: 'top . composed',
formule: {
barème: {
assiette: 2008,
@ -448,7 +439,7 @@ describe('Implicit parent applicability', function() {
it('should make a variable non applicable if one parent is input to false', function() {
let rawRules = [
{ nom: 'CDD', question: 'CDD ?' },
{ nom: 'surcoût', formule: 10, espace: 'CDD' }
{ nom: 'CDD . surcoût', formule: 10 }
],
rules = parseAll(rawRules.map(enrichRule))
expect(

View File

@ -1,5 +1,5 @@
import { expect } from 'chai'
import { evaluateBottomUp, getSituationValue } from '../source/engine/getSituationValue'
import { getSituationValue } from '../source/engine/getSituationValue'
describe('getSituationValue', function() {
it('should directly return the value of any rule that specifies a format (i.e currency, duration)', function() {
@ -66,8 +66,7 @@ describe('getSituationValue', function() {
it('should set the value of variants to false if one of them is true', function() {
let rule = {
nom: 'ici',
espace: 'univers',
nom: 'univers . ici',
formule: { 'une possibilité': ['noir', 'blanc'] }
},
state = { 'univers . ici': 'blanc' },