Squashed 'publicodes/' changes from 10a30d32..a680ad31

a680ad31 🐛 Corrige un bug sur l'affichage des remplacements dans la doc publicodes
1e45d98d 🔥 Supprime la transformation d'emoji dans la doc publicodes
77973a9b 🐛 Répare l'affichage des règles remplacées
341b2e39 🎨🐛 corrige le style du remplacement dans les sommes
24dce683 🎨 Améliore l'explication des règles avec remplacement
6d086823 💚 fix lint
2d64d908 🏗Add export as Pdf button on simulators
163b766a  Prettier linting in publicodes subtree
e1507975  Reformat files
80161f95 🐛 Fix VAT example
79bde0be 🔥 Remove "classnames" dependency from publicodes-react
3e65e320 🔥 Remove ramda from publicodes-react
71b68707 📦 Publicodes v1.0.0-beta.16
bdc92216 Merge the tests and publish workflows
1c032ebc  Add test for a value with a percentage in its unit
d2865e8c Disable sum optimization inside comparisons
f4faa35d Ajout d'un test qui casse l'implé actuelle des missing parentes
f6105283 🖋 Document packages publication on NPM
a79eeb86 Better Github workflows
d0db4d09 Import publish action
c268cff5 Type checking in CI
a35403d7 Correction formattage
3022fd78 Add a separate cache for applicability
35095da9 Optimize the evaluation of applicability
7525446e Add a github action to run tests on push
39a12a13 Ajout d'un prettierrc / reformattage de quelques fichiers récents
c296a25e Ajout d'un deuxièmes test non fonctionnel sur le sujet #33
9f5afb4e Désactivation d'un nouveau test pas encore résolu
76d00085 Récupération de la complexité initiale du test missing variations
93210235 🐛 Meilleures missingVariables des variations
369abeae Simplification du test missingVariables qui ne marche pas
64217d3d Nouveau test missing variables éval paresseuse variations
d9c3e1f6 Conversion d'un gros test JS object en YAML
615ae5e5 Ajout d'un test râté pour #33
d290b46d Passage à mochapack pour webpack 5
5d7a5b31 Paquets NPM et conf babel manquants
1df9a8d4 Ajout d'un .gitignore et yarn.lock
6c2d0203 Uniformise l'unité des arrondis
2cbffe8a ⬆ MAJ Typescript vers 4.3
678403e4 Corrige le calcul des cotisations forfaitaires de début d'activité
8cdaac05 Simpler condition component (#1578)
b7459617 🔥 Supprime les variables temporelles
db62b57d 🔥 Supprime l'utilisation des temporals dans les mécanismes

git-subtree-dir: publicodes
git-subtree-split: a680ad31c33b93e4f35171488cec1b6f9e08179a
pull/1817/head
Maxime Quandalle 2021-11-05 15:52:37 +01:00
parent f4e3b93af2
commit 2e6ce3ab1a
666 changed files with 2544 additions and 81872 deletions

View File

@ -8,24 +8,18 @@ trim_trailing_whitespace = true
# tab_width doesn't make much sense as it can be left to the reader to decide.
indent_style = tab
insert_final_newline = true
max_line_length = 80
[**.{js,jsx,ts,tsx}]
indent_size = 2
max_line_length = 80
[**.{yml,yaml}]
# Spaces are mandatory for yaml files:
indent_style = space
indent_size = 2
# A high max_line_length is needed as prettier doesn't manage property-name
# line-wrapping correctly:
# See https://github.com/prettier/prettier/issues/5599
max_line_length = 1000
trim_trailing_whitespace = false
[*.md]
trim_trailing_whitespace = false
indent_style = space
indent_size = 4
trim_trailing_whitespace = false

View File

@ -1,3 +0,0 @@
node_modules
dist
publicodes/example/

View File

@ -1,111 +0,0 @@
module.exports = {
root: true,
parser: "babel-eslint",
parserOptions: {
"ecmaFeatures": {
"jsx": true
}
},
env: {
"browser": true,
"commonjs": true,
"es6": true,
},
globals: {
"process": false
},
plugins: [
"react",
"react-hooks",
"mocha"
],
rules: {
"quotes": [
1,
"single",
{
"avoidEscape": true
}
],
"no-console": 1,
"no-restricted-globals": [
2,
"length"
],
"no-global-assign": 0,
"no-unsafe-negation": 0,
"react/prop-types": 0,
"react/jsx-no-target-blank": 0,
"react/no-unescaped-entities": 0,
"react/display-name": 1,
"react-hooks/rules-of-hooks": "error",
"react-hooks/exhaustive-deps": "warn",
"react/jsx-uses-react": "off",
"react/react-in-jsx-scope": "off",
"mocha/no-skipped-tests": "warn",
"mocha/no-exclusive-tests": "error"
},
settings: {
"react": {
"version": "detect"
}
},
overrides: [
{
files: [ "**/*.{ts,tsx}" ],
parser: "@typescript-eslint/parser",
parserOptions: {
"ecmaFeatures": {
"jsx": true
},
"tsconfigRootDir": __dirname,
"project": [ "./mon-entreprise/tsconfig.json", "./publicodes/tsconfig.json" ]
},
plugins: [ "@typescript-eslint" ],
rules: {
"@typescript-eslint/no-empty-function": 0,
"@typescript-eslint/no-use-before-define": 0,
"@typescript-eslint/member-delimiter-style": [2, {
multiline: {
delimiter: "none"
}
}],
"@typescript-eslint/explicit-function-return-type": 0,
'@typescript-eslint/prefer-string-starts-ends-with': 1,
'@typescript-eslint/no-unnecessary-type-assertion': 1, // has false positives (Object.values result) v 2.29.0
'@typescript-eslint/no-inferrable-types': 1, // causes problems with unknown values v 2.29.0 typescript v 3.8.3
'@typescript-eslint/no-var-requires': 'off',
// TODO - enable these new recommended rules, a first step would be to switch from "off" to "warn"
'@typescript-eslint/explicit-module-boundary-types': 'off',
'@typescript-eslint/no-floating-promises': 'off',
'@typescript-eslint/no-extra-semi': 'off',
'@typescript-eslint/no-unsafe-assignment': 'off',
'@typescript-eslint/no-unsafe-call': 'off',
'@typescript-eslint/no-unsafe-member-access': 'off',
'@typescript-eslint/no-unsafe-return': 'off',
'@typescript-eslint/restrict-plus-operands': 'off',
'@typescript-eslint/restrict-template-expressions': 'off',
'@typescript-eslint/naming-convention': 'off',
'@typescript-eslint/prefer-regexp-exec': 'off',
'@typescript-eslint/no-explicit-any': 'off',
},
extends: [
"plugin:@typescript-eslint/recommended",
"plugin:@typescript-eslint/recommended-requiring-type-checking"
]
},
{
files: ["**/*.test.js"],
env: {
mocha: true
}
}
],
extends: [
"eslint:recommended",
"plugin:react/recommended",
"prettier",
"prettier/react",
"prettier/@typescript-eslint"
]
}

1
.github/FUNDING.yml vendored
View File

@ -1 +0,0 @@
custom: ['https://mon-entreprise.fr/budget']

View File

@ -1,222 +0,0 @@
name: Déploiement
on:
pull_request:
types: [opened, synchronize]
push:
branches: [master, demo, next]
# We display the release notes in the "news" section of mon-entreprise.fr so
# we want to re-deploy the site when a new release is published or edited on
# GitHub.
release:
types: [published, edited]
# The /stats data is generated during the build. To keep the daily data fresh,
# we relaunch a nightly full build of the app
schedule:
- cron: "0 4 * * *"
jobs:
deploy-context:
runs-on: ubuntu-18.04
outputs:
env-name: ${{ steps.deploy-env.outputs.name }}
fr_url: ${{ steps.base-urls.outputs.fr }}
en_url: ${{ steps.base-urls.outputs.en }}
publicodes_url: ${{ steps.base-urls.outputs.publicodes }}
steps:
- id: deploy-env
run:
echo "::set-output name=name::${{ github.event.number || '${GITHUB_REF#refs/*/}' }}"
- id: base-urls
run:
echo "::set-output name=fr::${{ steps.deploy-env.outputs.name == 'master' && 'https://mon-entreprise.fr' || format('https://{0}--mon-entreprise.netlify.app', steps.deploy-env.outputs.name) }}";
echo "::set-output name=en::${{ steps.deploy-env.outputs.name == 'master' && 'https://mycompanyinfrance.fr' || format('https://{0}-en--mon-entreprise.netlify.app', steps.deploy-env.outputs.name) }}";
echo "::set-output name=publicodes::${{ steps.deploy-env.outputs.name == 'master' && 'https://publi.codes' || format('https://{0}-publicodes--mon-entreprise.netlify.app', steps.deploy-env.outputs.name) }}";
build:
needs: deploy-context
env:
FR_BASE_URL: ${{ needs.deploy-context.outputs.fr_url }}
EN_BASE_URL: ${{ needs.deploy-context.outputs.en_url }}
PUBLICODES_BASE_URL: ${{ needs.deploy-context.outputs.publicodes_url }}
runs-on: ubuntu-18.04
steps:
- uses: actions/checkout@v2
- uses: actions/cache@v2
with:
path: '**/node_modules'
key: ${{ runner.os }}-modules-${{ hashFiles('**/yarn.lock') }}-v2
- run: yarn install --frozen-lockfile
env:
# Secrets of all kinds for fetching stats & releases
GITHUB_API_SECRET: ${{ secrets.GITHUB_TOKEN }}
ZAMMAD_API_SECRET_KEY: ${{ secrets.ZAMMAD_API_SECRET_KEY }}
ATINTERNET_API_SECRET_KEY: ${{ secrets.ATINTERNET_API_SECRET_KEY }}
ATINTERNET_API_ACCESS_KEY: ${{ secrets.ATINTERNET_API_ACCESS_KEY }}
- name: Build app
run: yarn workspace mon-entreprise build
env:
AT_INTERNET_SITE_ID: ${{ needs.deploy-context.outputs.env-name == 'master' && 617190 || 617189 }}
NODE_ENV: production
- name: Replace site placeholders in netlify.toml redirection file
run:
sed -i "s|:SITE_FR|$FR_BASE_URL|g" netlify.toml;
sed -i "s|:SITE_EN|$EN_BASE_URL|g" netlify.toml;
sed -i "s|:SITE_PUBLICODES|$PUBLICODES_BASE_URL|g" netlify.toml
- uses: actions/upload-artifact@v2
with:
name: static-site
path: |
mon-entreprise/dist/**
netlify.toml
if-no-files-found: error
deploy-preview:
needs: [build, deploy-context]
runs-on: ubuntu-18.04
if: needs.deploy-context.outputs.env-name != 'master'
strategy:
matrix:
site: ['', 'en', 'publicodes']
steps:
- uses: actions/checkout@v2
- uses: actions/download-artifact@v2
with:
name: static-site
- id: deploy-netlify
uses: nwtgck/actions-netlify@v1.1
with:
publish-dir: './mon-entreprise/dist'
netlify-config-path: ./netlify.toml
production-deploy: false
github-token: ${{ secrets.GITHUB_TOKEN }}
enable-commit-status: true
enable-commit-comment: false
github-deployment-environment: ${{ needs.deploy-context.outputs.env-name }}
alias: ${{ needs.deploy-context.outputs.env-name }}${{ matrix.site && format('-{0}', matrix.site) }}
deploy-message: ${{ github.event.pull_request.title || needs.deploy-context.outputs.env-name }} (${{ matrix.site || 'fr' }})
# Disabled because we create our own customized comment
enable-pull-request-comment: false
env:
NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }}
NETLIFY_SITE_ID: ${{ secrets.NETLIFY_SITE_ID }}
timeout-minutes: 1
deploy-prod:
needs: [build, deploy-context]
runs-on: ubuntu-18.04
if: needs.deploy-context.outputs.env-name == 'master'
steps:
- uses: actions/checkout@v2
- uses: actions/download-artifact@v2
with:
name: static-site
- id: deploy-netlify
uses: nwtgck/actions-netlify@v1.1
with:
publish-dir: './mon-entreprise/dist'
netlify-config-path: ./netlify.toml
production-deploy: true
github-token: ${{ secrets.GITHUB_TOKEN }}
enable-commit-status: true
enable-commit-comment: false
github-deployment-environment: master
deploy-message: Deploy production branch
env:
NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }}
NETLIFY_SITE_ID: ${{ secrets.NETLIFY_SITE_ID }}
timeout-minutes: 1
post-comment:
runs-on: ubuntu-18.04
if: github.event_name == 'pull_request'
needs: [deploy-preview, deploy-context]
steps:
- name: Find Comment
uses: peter-evans/find-comment@v1
id: find-comment
with:
issue-number: ${{ github.event.pull_request.number }} #e.g. 1
comment-author: 'github-actions[bot]'
body-includes: netlify
- name: Create comment
uses: peter-evans/create-or-update-comment@v1
with:
comment-id: ${{ steps.find-comment.outputs.comment-id }}
issue-number: ${{ github.event.pull_request.number }}
edit-mode: replace
body: |
🚀 La branche est déployée !
- mon-entreprise : ${{ needs.deploy-context.outputs.fr_url }}
- mycompanyinfrance : ${{ needs.deploy-context.outputs.en_url }}
- publicodes : ${{ needs.deploy-context.outputs.publicodes_url }}
end-to-end-test:
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }}
runs-on: ubuntu-16.04
# We need to specify always() https://github.com/actions/runner/issues/491
if: always() && (needs.deploy-prod.result == 'success' || needs.deploy-preview.result == 'success')
needs: [deploy-context, deploy-prod, deploy-preview]
strategy:
fail-fast: false
matrix:
site: ['fr', 'en', 'publicodes']
include:
- site: fr
integrationFolder: mon-entreprise
baseUrl: ${{ needs.deploy-context.outputs.fr_url }}
language: fr
test-external: ${{ needs.deploy-context.outputs.env-name == 'master' }}
- site: en
integrationFolder: mon-entreprise
baseUrl: ${{ needs.deploy-context.outputs.en_url }}
language: en
- site: publicodes
baseUrl: ${{ needs.deploy-context.outputs.publicodes_url }}
integrationFolder: publi.codes
language: fr
# TODO : activate parallelization https://github.com/cypress-io/github-action#parallel (missing https://github.com/cypress-io/github-action#custom-build-id)
# containers: [1, 2]
# TODO : browser: ['firefox', 'chrome']
steps:
- name: Checkout
uses: actions/checkout@v2
- uses: actions/cache@v2
# Custom cache as we do not care about installing all the other dependancies
with:
path: |
~/.cache/Cypress
node_modules
key: cypress-cache-${{ runner.os }}-${{ hashFiles('package-lock.json') }}
- run: npm i cypress cypress-plugin-tab
- name: Test mon-entreprise
uses: cypress-io/github-action@v2
with:
install: false
working-directory: mon-entreprise
record: true
tag: ${{ matrix.site }},${{ needs.deploy-context.outputs.env-name }}-deploy
config: integrationFolder=cypress/integration/${{ matrix.integrationFolder }},baseUrl=${{ matrix.baseUrl }}
env: language=${{ matrix.language }}
env:
COMMIT_INFO_MESSAGE: ${{ github.event.pull_request.title }}
- name: Test external integration
if: matrix.test-external
uses: cypress-io/github-action@v2
with:
install: false
working-directory: mon-entreprise
record: true
tag: external-integration
config: integrationFolder=cypress/integration/external,baseUrl=${{ matrix.baseUrl }}

View File

@ -1,45 +0,0 @@
name: Publication du paquet publicodes
on:
push:
paths:
- publicodes/**
- .github/workflows/publish-publicodes.yaml
jobs:
test:
if: contains(join(github.event.commits.*.message, ' | '), '📦 Publicodes v1.0.0-beta.')
runs-on: ubuntu-18.04
steps:
- uses: actions/checkout@v2
- uses: actions/cache@v2
with:
path: '**/node_modules'
key: ${{ runner.os }}-modules-${{ hashFiles('**/yarn.lock') }}-v2
- run: yarn install --frozen-lockfile
- working-directory: ./publicodes/example/publicodes-react
run: |
yarn install
yarn test
publish:
needs: test
runs-on: ubuntu-18.04
steps:
- uses: actions/checkout@v2
- uses: actions/cache@v2
with:
path: '**/node_modules'
key: ${{ runner.os }}-modules-${{ hashFiles('**/yarn.lock') }}-v2
- run: yarn install --frozen-lockfile
- uses: JS-DevTools/npm-publish@v1
with:
token: ${{ secrets.NPM_PUBLISH_SECRET }}
dry-run: ${{ github.ref != 'refs/heads/master' }}
package: ./publicodes/core/package.json
tag: next
- uses: JS-DevTools/npm-publish@v1
with:
token: ${{ secrets.NPM_PUBLISH_SECRET }}
dry-run: ${{ github.ref != 'refs/heads/master' }}
package: ./publicodes/ui-react/package.json
tag: next

76
.github/workflows/test-publish.yaml vendored Normal file
View File

@ -0,0 +1,76 @@
name: Test and Publish
on: [push]
jobs:
lint:
runs-on: ubuntu-20.04
steps:
- uses: actions/checkout@v2
- uses: actions/cache@v2
with:
path: '**/node_modules'
key: ${{ runner.os }}-modules-${{ hashFiles('**/yarn.lock') }}-v2
- run: yarn install --frozen-lockfile
- run: yarn lint:prettier
test:
name: Unit tests
runs-on: ubuntu-20.04
steps:
- uses: actions/checkout@v1
- uses: actions/setup-node@v1
with:
node-version: 14
- run: yarn install
- run: yarn test
test-type:
name: Type checking
runs-on: ubuntu-20.04
steps:
- uses: actions/checkout@v1
- uses: actions/setup-node@v1
with:
node-version: 14
- run: yarn install
- run: yarn test:type
test-example-app:
name: Test example app
runs-on: ubuntu-20.04
steps:
- uses: actions/checkout@v2
- uses: actions/cache@v2
with:
path: '**/node_modules'
key: ${{ runner.os }}-modules-${{ hashFiles('**/yarn.lock') }}-v2
- run: yarn install --frozen-lockfile
- working-directory: ./example/publicodes-react
run: |
yarn install
yarn test
# This job could be in a separate workflow triggered when all the tests passes
# using the `workflow_run` event, but it makes it difficult to retrieve the
# commit message.
publish:
if: contains(join(github.event.commits.*.message, ' | '), '📦 Publicodes v1.0.0-beta.')
needs: [test, test-type, test-example-app]
runs-on: ubuntu-20.04
steps:
- uses: actions/checkout@v2
- uses: actions/cache@v2
with:
path: '**/node_modules'
key: ${{ runner.os }}-modules-${{ hashFiles('**/yarn.lock') }}-v2
- run: yarn install --frozen-lockfile
- uses: JS-DevTools/npm-publish@v1
with:
token: ${{ secrets.NPM_PUBLISH_SECRET }}
dry-run: ${{ github.ref != 'refs/heads/master' }}
package: ./core/package.json
- uses: JS-DevTools/npm-publish@v1
with:
token: ${{ secrets.NPM_PUBLISH_SECRET }}
dry-run: ${{ github.ref != 'refs/heads/master' }}
package: ./ui-react/package.json

View File

@ -1,18 +0,0 @@
name: Règles (non-regression)
on:
pull_request:
paths:
- modele-social/règles/**
- publicodes/core/**
- mon-entreprise/test/regressions/**
jobs:
test:
runs-on: ubuntu-18.04
steps:
- uses: actions/checkout@v2
- uses: actions/cache@v2
with:
path: '**/node_modules'
key: ${{ runner.os }}-modules-${{ hashFiles('**/yarn.lock') }}-v2
- run: yarn install --frozen-lockfile
- run: yarn test:regressions

View File

@ -1,59 +0,0 @@
name: Tests
on: push
jobs:
lint:
runs-on: ubuntu-18.04
steps:
- uses: actions/checkout@v2
- uses: actions/cache@v2
with:
path: '**/node_modules'
key: ${{ runner.os }}-modules-${{ hashFiles('**/yarn.lock') }}-v2
- run: yarn install --frozen-lockfile
- run: yarn lint
typecheck:
runs-on: ubuntu-18.04
steps:
- uses: actions/checkout@v2
- uses: actions/cache@v2
with:
path: '**/node_modules'
key: ${{ runner.os }}-modules-${{ hashFiles('**/yarn.lock') }}-v2
- run: yarn install --frozen-lockfile
env:
# Secrets of all kinds for fetching stats & releases
GITHUB_API_SECRET: ${{ secrets.GITHUB_TOKEN }}
ZAMMAD_API_SECRET_KEY: ${{ secrets.ZAMMAD_API_SECRET_KEY }}
ATINTERNET_API_SECRET_KEY: ${{ secrets.ATINTERNET_API_SECRET_KEY }}
ATINTERNET_API_ACCESS_KEY: ${{ secrets.ATINTERNET_API_ACCESS_KEY }}
- run: yarn test:type
unit:
runs-on: ubuntu-18.04
steps:
- uses: actions/checkout@v2
- uses: actions/cache@v2
with:
path: '**/node_modules'
key: ${{ runner.os }}-modules-${{ hashFiles('**/yarn.lock') }}-v2
- run: yarn install --frozen-lockfile
- run: yarn test
i18n:
runs-on: ubuntu-18.04
steps:
- uses: actions/checkout@v2
- uses: actions/cache@v2
with:
path: '**/node_modules'
key: ${{ runner.os }}-modules-${{ hashFiles('**/yarn.lock') }}-v2
- run: yarn install --frozen-lockfile
- working-directory: mon-entreprise
run:
yarn run i18n:rules:check;
yarn run i18n:ui:check

View File

@ -1,24 +0,0 @@
# This bot post a comment when issues with a given label are closed.
name: Message du robot Zammad
on:
issues:
types: [closed]
jobs:
comment-when-issue-close:
if: contains(github.event.issue.labels.*.name, '🏓 retour utilisateur')
runs-on: ubuntu-latest
steps:
# Note: we could detect if the comment was already posted in the issue to
# avoid posting it multiple times in case the issue was re-opened and
# re-closed. https://github.com/peter-evans/create-or-update-comment
- uses: peter-evans/create-or-update-comment@v1
with:
issue-number: ${{ github.event.issue.number }}
body: |
Ce ticket vient d'être fermé 🎉
Il est temps de prévenir les utilisateurs qui nous ont fait ce retour :
https://mon-entreprise.zammad.com/#search/tags%3A%23${{ github.event.issue.number }}
Laissez un 👍 quand c'est fait !

1
.nvmrc
View File

@ -1 +0,0 @@
16.0.0

View File

@ -1,2 +0,0 @@
.eslintrc.js
dist

View File

@ -1,9 +0,0 @@
{
"recommendations": [
"esbenp.prettier-vscode",
"ban.spellright",
"jpoissonnier.vscode-styled-components",
"bungcip.better-toml",
"mikestead.dotenv"
]
}

15
.vscode/settings.json vendored
View File

@ -1,15 +0,0 @@
{
"editor.formatOnSave": true,
"spellright.language": ["fr", "en"],
"spellright.documentTypes": ["yaml", "git-commit", "markdown"],
"typescript.tsdk": "node_modules/typescript/lib",
"editor.tabSize": 2,
"eslint.enable": true,
"cSpell.words": [
"mycompanyinfrance",
"smarttag"
],
"search.exclude": {
"**/dist": true
}
}

View File

@ -1 +0,0 @@
@types/react-native

View File

@ -26,8 +26,11 @@
- Fix bug sur le mécanisme minimum, une valeur non applicable n'est plus considérée comme valant "0" (#1493)
## 1.0.0-beta.16 (release candidate)
## 1.0.0-beta.16
**core**
- Répare un bug dans le mécanisme résoudre le cycle
- Suppression des variables temporelles
- Optimisation de la désactivation de branches
- Meilleures performances

View File

@ -6,217 +6,15 @@ Voici quelques informations pour démarrer :
## Rapport de bug, nouvelles fonctionnalités
Nous utilisons GitHub pour suivre tous les bugs et discussions sur les nouvelles fonctionnalités. Pour rapporter un bug ou proposer une évolution vous pouvez [ouvrir une nouvelle discussion](https://github.com/betagouv/mon-entreprise/issues/new). N'hésitez pas à utiliser la recherche pour vérifier si le sujet n'est pas déjà traité dans une discussion ouverte.
Nous utilisons GitHub pour suivre tous les bugs et discussions sur les nouvelles fonctionnalités. Pour rapporter un bug ou proposer une évolution vous pouvez [ouvrir une nouvelle discussion](https://github.com/betagouv/publicodes/discussions). N'hésitez pas à utiliser la recherche pour vérifier si le sujet n'est pas déjà traité dans une discussion ouverte.
## Développement
Si vous voulez participer au développement de nouvelles fonctionnalités, vous pouvez consulter la liste des «[good first issue](https://github.com/betagouv/mon-entreprise/issues?q=is%3Aopen+is%3Aissue+label%3A%22%3Anew%3A+good+first+issue%22) ». Ce sont des fonctionnalités intéressantes qui ne sont normalement pas trop complexe à implémenter. N'hésitez pas à poser toutes vos questions sur ces issues !
### Technologies
L'application est écrite en JavaScript, elle est exécuté uniquement côté client — il n'y a pas de serveur applicatif, nous générons des fichiers `.html` statiques
Nous utilisons :
- [TypeScript](https://www.typescriptlang.org) pour ajouter un système de typage à notre code JavaScript. Le typage n'est pas utilisé partout et il n'est pas obligatoire de le prendre en compte pour contribuer.
- [Yarn](https://yarnpkg.com/fr) pour la gestion des dépendances (à la place de NPM qui est souvent utilisé dans les applications JavaScript)
- [React](https://reactjs.org) pour la gestion de l'interface utilisateur
- [Redux](https://redux.js.org) pour gérer le “state” de l'application côté client
- [Prettier](https://prettier.io/) pour formater le code source, l'idéal est de configurer votre éditeur de texte pour que les fichiers soit formatés automatiquement quand vous sauvegardez un fichier. Si vous utilisez [VS Code](https://code.visualstudio.com/) cette configuration est automatique.
- [Webpack](https://webpack.js.org) pour le “bundling”
- [Eslint](http://eslint.org) qui permet par exemple d'éviter de garder des variables inutilisées
- [Ramda](https://ramdajs.com) comme libraire d'utilitaires pour manipuler les listes/objects/etc (c'est une alternative à lodash ou underscore)
- [Mocha](https://mochajs.org), [Jest](https://jestjs.io) et [Cypress](https://www.cypress.io) pour les l'execution des tests. Plus d'informations dans la section consacrée aux tests.
### Démarrage
Si l'historique des commits est trop volumineux, vous pouvez utiliser le paramètre `depth` de git pour ne télécharger que les derniers commits.
```
# Clone this repo on your computer
git clone --depth 100 git@github.com:betagouv/mon-entreprise.git && cd mon-entreprise
# Install the Javascript dependencies through Yarn
yarn install
# Watch changes in publicodes and run the server for mon-entreprise
yarn start
```
L'application est exécuté sur https://localhost:8080/mon-entreprise pour la version française et http://localhost:8080/infrance pour la version anglaise.
Pour activer le tracing Redux:
```
REDUX_TRACE=true yarn start
```
### Messages de commit
A mettre sans retenue dans les messages de commit :
https://github.com/atom/atom/blob/master/CONTRIBUTING.md#git-commit-messages
- 🎨 `:art:` when working on the app's visual style
- 🐎 `:racehorse:` when improving performance
- 📝 `:memo:` when writing docs
- 🐛 `:bug:` when fixing a bug
- 🔥 `:fire:` when removing code or files
- 💚 `:green_heart:` when fixing the CI build
- ✅ `:white_check_mark:` when adding tests
- ⬆️ `:arrow_up:` when upgrading dependencies
- :sparkles: `:sparkles:` when formatting, renaming, reorganizing files
Et ceux spécifiques au projet :
- :gear: `:gear:` pour une contribution au moteur qui traite les YAML
- :hammer: `:hammer:` pour une contribution à la base de règles
- :calendar: `:calendar:` pour un changement de règle du à une évolution temporelle (en attendant mieux)
- :chart_with_upwards_trend: `:chart_with_upwards_trend:` pour une amélioration du tracking
- :alien: `:alien:` pour ajouter des traductions
- :wheelchair: `:wheelchair:` pour corriger les problèmes liés à l'accessibilité
- :fountain_pen: `:fountain_pen:` pour séparer les commits liés à la modification du contenu
- :mag: `:mag:` pour les modifications liées au référencement naturel
### Tests
Pour executer les tests unitaires :
```sh
$ yarn run test-common
```
Pour le snapshot testing :
```sh
$ yarn run test:regressions
```
Si vous souhaitez mettre à jour les snapshots vous pouvez utiliser le paramètre `--updateSnapshot`, son raccourci `-u`, ou encore le [mode interactif](https://jestjs.io/docs/en/snapshot-testing#interactive-snapshot-mode).
Enfin pour les tests d'intégration :
```sh
$ yarn run cypress run
```
### Traduction 👽
Le site est disponible en français, et en anglais sur https://mycompanyinfrance.com
Les traductions se trouvent dans le répertoire `source/locales`.
La librairie utilisée pour la traduction de l'UI est
[react-i18next](https://react.i18next.com/).
Lorsque l'on introduit une nouvelle chaîne de caractère dans l'UI il faut
systématiquement penser à gérer sa traduction, via un composant `<Trans>`, ou
via la fonction `t`
Le circle-ci fait une analyse statique du code pour repérer les chaînes non
traduites, dans le moteur et l'UI :
```sh
$ yarn run i18n:rules:check
$ yarn run i18n:ui:check
```
Pour traduire automatiquement les chaînes manquantes via l'api Deepl :
```sh
$ yarn run i18n:rules:translate
$ yarn run i18n:ui:translate
```
N'oubliez pas de vérifier sur le diff que rien n'est choquant.
### CI/CD
- Nous utilisons des [Github actions](https://github.com/features/actions) pour faire tourner les builds et
tests.
- [Netlify](https://www.netlify.com/), s'occupe de lhébergement du site sur Internet avec gestion des DNS.
### Analyse des bundles
La commande `yarn run build:analyse-bundle` gènere une visualisation interactive du
contenu packagé, cf.
[webpack-bundle-analyzer](https://github.com/webpack-contrib/webpack-bundle-analyzer)
### Modifier publicodes
Publicodes dispose désormais de son propre dépôt GitHub https://github.com/betagouv/publicodes
Néanmoins pour certaines nouvelles fonctionnalités de mon-entreprise nous concervons le besoin de modifier publicodes avec le moins de frictions possible. Pour tester une évolution du moteur il serait en effet trop lourd d'avoir à ouvrir d'abord une PR côté publicodes, la merger, publier une nouvelle version du paquet, puis ré-intégrer cette nouvelle version sur mon-entreprise.
C'est pourquoi nous intégrons le code source du publicode dans le sous-répertoire `publicodes/`. La commande `git subtree` nous permet de synchroniser les changements effectués dans l'un ou l'autre des dépôts.
La première chose à faire est d'ajouter une nouvelle `remote` pour `betagouv/publicodes`, ici nous l'appelons simplement `publicodes` :
```sh
git remote add publicodes git@github.com:betagouv/publicodes.git
```
Ensuite il est possible de remonter les changements effectués dans le sous-repertoire `publicodes/` vers la branche master de la remote `publicodes`.
```sh
$ git subtree push --prefix=publicodes publicodes master
```
Dans l'autre sens il est possible de rapatrier les changements avec la commande
```sh
$ git subtree pull --prefix=publicodes publicodes master --squash
```
## Développement de modèles Publicodes
### Traduction des normes (lois) en règles Publicodes
Checklist:
- [ ] Lire les articles de vulgarisation (sur le site de l'URSSAF, des impôts, etc.).
- [ ] Utiliser un moteur de recherche spécialisé, comme [RFPaye](https://rfpaye.grouperf.com/).
- [ ] [Lire les normes][wiki normes] et noter leurs référence dans les règles Publicodes.
[wiki normes]: https://github.com/betagouv/mon-entreprise/wiki/Comment-lire-les-normes-(la-loi)-efficacement-pour-r%C3%A9diger-des-r%C3%A8gles-Publicodes%3F
### Tests
Pour tester les règles, il est recommandé de:
- faire tourner un simulateur et vérifier à la main l'adéquation des règles avec les normes
traduites ;
- créer des cas de tests de non-régression sous la forme de nouveaux snapshots (cf.
`mon-entreprise/test/regressions`).
## Documentation
### Publicodes
Un tutoriel sur publicodes est disponible sur https://publi.codes.
Un wiki contenant des informations intéressantes sur publicodes et le
raisonnement ayant abouti à ce langage sont dispos sur le repository
[betagouv/publicodes](https://github.com/betagouv/publicodes/wiki), qui est par
ailleurs inutilisé.
Pour se familiariser avec les règles, vous pouvez jeter un œil aux fichiers
contenant les règles elles-mêmes (dans le dossier `rules`) mais cela peut
s'avérer assez abrupt.
Essayez plutôt de jeter un oeil [aux tests](./publicodes/test/mécanismes/expressions.yaml)
dans un premier temps, puis au [mécanismes en
place](./publicodes/source/mecanisms).
## Publier une nouvelle version des paquets publicodes
<!-- TODO: action à déplacer dans le dépot betagouv/publicodes -->
## Publier une nouvelle version sur NPM
Voici la marche à suivre pour publier une nouvelle version :
1. Renseigner les modifications dans publicodes/CHANGELOG.md
1. Renseigner les modifications dans `CHANGELOG.md`
2. Remplacer les références à la précédente version par la nouvelle version dans les packages.json
3. Ajouter tous les changement dans un commit avec le message suivant :
3. Ajouter tous les changements dans un commit avec le message suivant :
```
📦 Publicodes v1.0.0-beta.<n>
```

View File

@ -1,39 +1,41 @@
Ce dépôt contient :
> 🇬🇧 Most of the documentation (including issues and commit messages) is written in French, please raise an [issue](https://github.com/betagouv/publicodes/issues/new) if you are interested and do not speak French. We intend to translate the language and the documentation in the coming weeks.
- Le code source du site [mon-entreprise.fr](https://mon-entreprise.fr)
- Les [règles publicodes](https://github.com/betagouv/mon-entreprise/tree/master/modele-social) pour le calcul des cotisations sociales, des impôts et des droits sociaux.
## <a href="https://publi.codes"><img src="https://mon-entreprise.fr/images/logo-publicodes.png" alt="Publicodes" width="200"/></a>
## <a href="https://mon-entreprise.fr"><img src="https://mon-entreprise.fr/images/logo.svg" alt="mon-entreprise.fr" width="200"/></a>
[![Npm version](https://img.shields.io/npm/v/publicodes)](https://www.npmjs.com/package/publicodes)
[![Gitter chat](https://badges.gitter.im/publicodes/publicodes.png)](https://gitter.im/publicodes/community)
[![Statut déploiement](https://github.com/betagouv/mon-entreprise/actions/workflows/deploy.yaml/badge.svg?branch=master)](https://github.com/betagouv/mon-entreprise/actions/workflows/deploy.yaml?query=branch%3Amaster++)&nbsp;
[![Statut test](https://github.com/betagouv/mon-entreprise/actions/workflows/test.yaml/badge.svg?branch=master)](https://github.com/betagouv/mon-entreprise/actions/workflows/test.yaml?query=branch%3Amaster++)
Publicodes est un langage déclaratif pour encoder les algorithmes d'intérêt
public. Il permet de réaliser des calculs généraux tout en fournissant une
explication permettant de comprendre et de documenter ces calculs.
Site développé en partenariat avec l'Urssaf, qui a pour mission d'accompagner des créateurs dentreprise dans le développement de leur activité.
Publicodes est adapté pour modéliser des domaines métiers complexes pouvant être
décomposés en règles élémentaires simples (comme la [législation socio-fiscale](https://github.com/betagouv/mon-entreprise/tree/master/publicodes),
[un bilan carbone](https://github.com/laem/futureco-data/blob/master/co2.yaml),
un estimateur de rendement locatif, etc.).
Il propose notamment des simulateurs de cotisations sociales très complets, basés sur le language déclaratif [publicodes](https://publi.codes). On peut ainsi calculer le coût d'une embauche, un salaire net après impôt, ses revenus d'auto-entrepreneur ou encore ceux d'un dirigeant de SASU ou d'indépendant
Il permet de générer facilement des simulateurs web interactifs où l'on peut affiner
progressivement le résultat affiché, et d'exposer une documentation du calcul explorable.
> 🧮 [Voir la liste des simulateurs](https://mon-entreprise.fr/simulateurs)
## Installation
Les développeurs ont la possibilité d'intégrer ces simulateurs sur d'autres sites, ou de réutiliser les règles pour effectuer leur propre calculs.
```
npm install publicodes
```
> 🧰 [Voir les outils à disposition des développeurs](https://mon-entreprise.fr/int%C3%A9gration)
## Documentation
Tous les outils proposés sur mon-entreprise.fr sont propulsés par [publicodes](https://publi.codes), un nouveau langage pour les algorithmes d'intérêt public.
- [Se lancer](https://publi.codes/langage/se-lancer)
- [Principes de base](https://publi.codes/langage/principes-de-base)
- [Bac à sable](https://publi.codes/studio)
## Contribuer
## Projets phares
Si vous souhaitez contribuer à l'un des deux projets, rendez-vous sur [CONTRIBUTING.md](./CONTRIBUTING.md).
## 🇬🇧 English users
This repository powers [mycompanyinfrance.fr](https://mycompanyinfrance.fr) and [mon-entreprise.fr](https://mon-entreprise.fr)
Most of the documentation (including issues and commit message) is written in french, please raise an [issue](https://github.com/betagouv/mon-entreprise/issues/new) if you are interested and do not speak French.
## 🗜️ Compatibility
The website will run well on modern browsers. Internet Explorer is not supported anymore (it should work but with visual glitches and performance issues).
This compatibility is tested thanks to [BrowserStack](http://browserstack.com/)'s free open source program.
![Logo de Browserstack, notre solution de tests manuels](https://i.imgur.com/dQwLjXA.png)
- **[mon-entreprise.fr](https://mon-entreprise.fr/simulateurs)** utilise publicodes
pour spécifier l'ensemble des calculs relatifs à la législation socio-fiscale
en France. Le site permet entre autre de simuler une fiche de paie complète,
de calculer les cotisations sociales pour un indépendant ou encore connaître
le montant du chômage partiel.
- **[futur.eco](https://futur.eco/)** utilise publicodes pour calculer les bilans
carbone d'un grand nombre d'activités, plats, transports ou biens.
- **[Nos Gestes Climat](https://ecolab.ademe.fr/apps/climat)** utilise publicodes pour proposer un calculateur d'empreinte climat personnel de référence complètement ouvert

View File

@ -8,16 +8,9 @@
}
}
],
[
"@babel/preset-react",
{
"runtime": "automatic"
}
],
"@babel/preset-typescript"
],
"plugins": [
"babel-plugin-styled-components",
"@babel/plugin-proposal-class-properties",
"@babel/plugin-proposal-optional-chaining",
"@babel/plugin-proposal-nullish-coalescing-operator",

View File

@ -1,6 +1,6 @@
{
"name": "publicodes",
"version": "1.0.0-beta.15",
"version": "1.0.0-beta.16",
"description": "A declarative language for encoding public algorithm",
"main": "dist/index.js",
"types": "dist/types/index.d.ts",
@ -25,14 +25,27 @@
],
"private": false,
"devDependencies": {
"@babel/preset-typescript": "^7.14.5",
"babel-loader": "^8.2.2",
"chai": "^4.2.0",
"dedent-js": "1.0.1",
"intl": "^1.2.5",
"typescript": "^4.2.4",
"dedent-js": "1.0.1"
"json-loader": "^0.5.7",
"mocha": "^9.0.1",
"mochapack": "^2.1.2",
"nearley-loader": "^2.0.0",
"rimraf": "^3.0.2",
"typescript": "^4.3.2",
"webpack-cli": "^4.7.2",
"yaml-loader": "^0.6.0"
},
"dependencies": {
"@babel/core": "^7.14.6",
"@babel/preset-env": "^7.14.5",
"@types/webpack-env": "^1.16.0",
"moo": "^0.5.1",
"nearley": "^2.19.2",
"webpack": "^5.39.1",
"yaml": "^1.9.2"
},
"scripts": {
@ -41,7 +54,7 @@
"prepare": "yarn run rimraf dist && yarn run build",
"build": "yarn run webpack --config webpack.config.js && yarn run tsc",
"build:watch": "concurrently \"yarn run webpack --watch --config webpack.config.js\" \"yarn run tsc -w\"",
"test:file": "yarn mocha-webpack --include test/setupIntl.js --webpack-config ./webpack.test.js ",
"test:file": "yarn mochapack --include test/setupIntl.js --webpack-config ./webpack.test.js ",
"test": "yarn test:file \"./{,!(node_modules)/**/}!(webpack).test.js\""
},
"engines": {

View File

@ -174,8 +174,6 @@ export const traverseASTNode: TraverseFunction<NodeKind> = (fn, node) => {
return traverseUnitéNode(fn, node)
case 'variations':
return traverseVariationNode(fn, node)
case 'variable temporelle':
return traverseVariableTemporelle(fn, node)
case 'replacementRule':
return traverseReplacementNode(fn, node)
default:
@ -297,16 +295,14 @@ const traversePlancherNode: TraverseFunction<'plancher'> = (fn, node) => ({
},
})
const traverseRésoudreRéférenceCirculaireNode: TraverseFunction<'résoudre référence circulaire'> = (
fn,
node
) => ({
...node,
explanation: {
...node.explanation,
valeur: fn(node.explanation.valeur),
},
})
const traverseRésoudreRéférenceCirculaireNode: TraverseFunction<'résoudre référence circulaire'> =
(fn, node) => ({
...node,
explanation: {
...node.explanation,
valeur: fn(node.explanation.valeur),
},
})
const traversePlafondNode: TraverseFunction<'plafond'> = (fn, node) => ({
...node,
@ -382,17 +378,3 @@ const traverseVariationNode: TraverseFunction<'variations'> = (fn, node) => ({
consequence: fn(consequence),
})),
})
const traverseVariableTemporelle: TraverseFunction<'variable temporelle'> = (
fn,
node
) => ({
...node,
explanation: {
period: {
end: node.explanation.period.end && fn(node.explanation.period.end),
start: node.explanation.period.start && fn(node.explanation.period.start),
},
value: fn(node.explanation.value),
},
})

View File

@ -23,12 +23,10 @@ import { SommeNode } from '../mecanisms/sum'
import { SynchronisationNode } from '../mecanisms/synchronisation'
import { TauxProgressifNode } from '../mecanisms/tauxProgressif'
import { UnitéNode } from '../mecanisms/unité'
import { VariableTemporelleNode } from '../mecanisms/variableTemporelle'
import { VariationNode } from '../mecanisms/variations'
import { ReferenceNode } from '../reference'
import { ReplacementRule } from '../replacement'
import { RuleNode } from '../rule'
import { Temporal } from '../temporal'
export type ConstantNode = {
type: 'boolean' | 'objet' | 'number' | 'string'
@ -64,7 +62,6 @@ export type ASTNode = (
| SynchronisationNode
| TauxProgressifNode
| UnitéNode
| VariableTemporelleNode
| VariationNode
| ConstantNode
| ReplacementRule
@ -109,14 +106,24 @@ export type Unit = {
}
// Idée : une évaluation est un n-uple : (value, unit, missingVariable, isApplicable)
// Une temporalEvaluation est une liste d'evaluation sur chaque période. : [(Evaluation, Period)]
type EvaluationDecoration<T extends Types> = {
nodeValue: Evaluation<T>
missingVariables: Record<string, number>
unit?: Unit
temporalValue?: Temporal<Evaluation>
}
export type Types = number | boolean | string | Record<string, unknown>
export type Evaluation<T extends Types = Types> = T | false | null
// TODO: type NotYetDefined & NotApplicable properly (see #14) then refactor any code depending on these:
export type NotYetDefined = null
export function isNotYetDefined(value): value is NotYetDefined {
return value === null
}
export type NotApplicable = false
export function isNotApplicable(value): value is NotApplicable {
return typeof value === 'boolean' && value === false
}
export type Evaluation<T extends Types = Types> =
| T
| NotApplicable
| NotYetDefined
export type EvaluatedNode<T extends Types = Types> = ASTNode &
EvaluationDecoration<T>

View File

@ -7,17 +7,8 @@ import {
NodeKind,
} from './AST/types'
import { warning } from './error'
import { convertNodeToUnit, simplifyNodeUnit } from './nodeUnits'
import { convertNodeToUnit } from './nodeUnits'
import parse from './parse'
import {
concatTemporals,
liftTemporalNode,
mapTemporal,
pureTemporal,
Temporal,
temporalAverage,
zipTemporals,
} from './temporal'
export const collectNodeMissing = (
node: EvaluatedNode | ASTNode
@ -75,37 +66,17 @@ export const evaluateArray: <NodeName extends NodeKind>(
node.explanation.map(evaluate),
node.name
)
const values = evaluatedNodes.map(({ nodeValue }) => nodeValue)
const nodeValue = values.some((value) => value === null)
? null
: values.reduce(reducer, start)
const temporalValues = concatTemporals(
evaluatedNodes.map(
({ temporalValue, nodeValue }) =>
temporalValue ?? pureTemporal(nodeValue)
)
)
const temporalValue = mapTemporal((values) => {
if (values.some((value) => value === null)) {
return null
}
return values.reduce(reducer, start)
}, temporalValues)
const baseEvaluation = {
return {
...node,
missingVariables: mergeAllMissing(evaluatedNodes),
explanation: evaluatedNodes,
...(evaluatedNodes[0] && { unit: evaluatedNodes[0].unit }),
}
if (temporalValue.length === 1) {
return {
...baseEvaluation,
nodeValue: temporalValue[0].value,
}
}
return {
...baseEvaluation,
temporalValue,
nodeValue: temporalAverage(temporalValue as any),
nodeValue,
}
}
@ -132,71 +103,3 @@ export const parseObject = (objectShape, value, context) => {
})
)
}
export function evaluateObject<NodeName extends NodeKind>(
effet: (this: Engine, explanations: any) => any
) {
return function (node) {
const evaluations = Object.fromEntries(
Object.entries((node as any).explanation).map(([key, value]) => [
key,
this.evaluate(value as any),
])
)
const temporalExplanations = mapTemporal(
Object.fromEntries,
concatTemporals(
Object.entries(evaluations).map(([key, node]) =>
zipTemporals(pureTemporal(key), liftTemporalNode(node as ASTNode))
)
)
)
const temporalExplanation = mapTemporal((explanations) => {
const evaluation = effet.call(this, explanations)
return {
...evaluation,
explanation: {
...explanations,
...evaluation.explanation,
},
}
}, temporalExplanations)
const sameUnitTemporalExplanation: Temporal<
ASTNode & EvaluatedNode & { nodeValue: number }
> = convertNodesToSameUnit
.call(
this,
temporalExplanation.map((x) => x.value),
node.nodeKind
)
.map((node, i) => ({
...temporalExplanation[i],
value: simplifyNodeUnit(node),
}))
const temporalValue = mapTemporal(
({ nodeValue }) => nodeValue,
sameUnitTemporalExplanation
)
const nodeValue = temporalAverage(temporalValue)
const baseEvaluation = {
...node,
nodeValue,
unit: sameUnitTemporalExplanation[0].value.unit,
explanation: evaluations,
missingVariables: mergeAllMissing(Object.values(evaluations)),
}
if (sameUnitTemporalExplanation.length === 1) {
return {
...baseEvaluation,
explanation: (sameUnitTemporalExplanation[0] as any).value.explanation,
}
}
return {
...baseEvaluation,
temporalValue,
temporalExplanation,
}
} as EvaluationFunction<NodeName>
}

View File

@ -2,33 +2,35 @@ import { Evaluation, Unit } from './AST/types'
import { simplifyNodeUnit } from './nodeUnits'
import { formatUnit, serializeUnit } from './units'
export const numberFormatter = ({
style,
maximumFractionDigits = 2,
minimumFractionDigits = 0,
language,
}: {
style?: string
maximumFractionDigits?: number
minimumFractionDigits?: number
language?: string
}) => (value: number) => {
// When we format currency we don't want to display a single decimal digit
// ie 8,1€ but we want to display 8,10€
const adaptedMinimumFractionDigits =
style === 'currency' &&
maximumFractionDigits >= 2 &&
minimumFractionDigits === 0 &&
!Number.isInteger(value)
? 2
: minimumFractionDigits
return Intl.NumberFormat(language, {
export const numberFormatter =
({
style,
currency: 'EUR',
maximumFractionDigits,
minimumFractionDigits: adaptedMinimumFractionDigits,
}).format(value)
}
maximumFractionDigits = 2,
minimumFractionDigits = 0,
language,
}: {
style?: string
maximumFractionDigits?: number
minimumFractionDigits?: number
language?: string
}) =>
(value: number) => {
// When we format currency we don't want to display a single decimal digit
// ie 8,1€ but we want to display 8,10€
const adaptedMinimumFractionDigits =
style === 'currency' &&
maximumFractionDigits >= 2 &&
minimumFractionDigits === 0 &&
!Number.isInteger(value)
? 2
: minimumFractionDigits
return Intl.NumberFormat(language, {
style,
currency: 'EUR',
maximumFractionDigits,
minimumFractionDigits: adaptedMinimumFractionDigits,
}).format(value)
}
export const formatCurrency = (
nodeValue: number | undefined,
@ -168,9 +170,10 @@ export function serializeValue(
{ nodeValue, unit }: { nodeValue: Evaluation; unit?: Unit },
{ format }: { format: formatUnit }
) {
const serializedUnit = (unit && typeof nodeValue === 'number'
? serializeUnit(unit, nodeValue, format)
: ''
const serializedUnit = (
unit && typeof nodeValue === 'number'
? serializeUnit(unit, nodeValue, format)
: ''
)?.replace(/\s*\/\s*/g, '/')
return `${nodeValue} ${serializedUnit}`.trim()
}

View File

@ -7,8 +7,7 @@
@{%
const {
string, date, variable, temporalNumericValue, binaryOperation,
unaryOperation, boolean, number, numberWithUnit, JSONObject
string, date, variable, binaryOperation, unaryOperation, boolean, number, numberWithUnit, JSONObject
} = require('./grammarFunctions')
const moo = require("moo");
@ -61,11 +60,6 @@ main ->
NumericValue ->
AdditionSubstraction {% id %}
| Negation {% id %}
| TemporalNumericValue {% id %}
TemporalNumericValue ->
NumericValue %space %periodWord %space %date {% ([value,,word,,dateString]) => temporalNumericValue(value, word, date([dateString])) %}
| NumericValue %space %periodWord %colon Date {% ([value,,word,,date]) => temporalNumericValue(value, word, date) %}
NumericTerminal ->
Variable {% id %}

View File

@ -1,28 +1,24 @@
/* 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 { normalizeDateString } from './date'
import { parsePeriod } from './temporal'
export let binaryOperation = (operationType) => ([A, , operator, , B]) => ({
[operator]: {
operationType,
explanation: [A, B],
},
})
export let binaryOperation =
(operationType) =>
([A, , operator, , B]) => ({
[operator]: {
operationType,
explanation: [A, B],
},
})
export let unaryOperation = (operationType) => ([operator, , A]) => ({
[operator]: {
operationType,
explanation: [number([{ value: '0' }]), A],
},
})
export let temporalNumericValue = (variable, word, date) => ({
temporalValue: {
explanation: variable,
period: parsePeriod(word.value.slice(2), date),
},
})
export let unaryOperation =
(operationType) =>
([operator, , A]) => ({
[operator]: {
operationType,
explanation: [number([{ value: '0' }]), A],
},
})
export let variable = ([firstFragment, nextFragment], _, reject) => {
const fragments = [firstFragment, ...nextFragment].map(({ value }) => value)

View File

@ -16,14 +16,17 @@ const emptyCache = (): Cache => ({
_meta: {
parentRuleStack: [],
evaluationRuleStack: [],
disableApplicabilityContextCounter: 0,
},
nodes: new Map(),
nodesApplicability: new Map(),
})
type Cache = {
_meta: {
parentRuleStack: Array<string>
evaluationRuleStack: Array<string>
disableApplicabilityContextCounter: number
inversionFail?:
| {
given: string
@ -34,6 +37,7 @@ type Cache = {
filter?: string
}
nodes: Map<PublicodesExpression | ASTNode, EvaluatedNode>
nodesApplicability: Map<PublicodesExpression | ASTNode, EvaluatedNode>
}
export type EvaluationOptions = Partial<{
@ -41,7 +45,14 @@ export type EvaluationOptions = Partial<{
}>
export { reduceAST, makeASTTransformer as transformAST } from './AST/index'
export { Evaluation, Unit } from './AST/types'
export {
Evaluation,
Unit,
NotYetDefined,
isNotYetDefined,
NotApplicable,
isNotApplicable,
} from './AST/types'
export { capitalise0, formatValue } from './format'
export { simplifyNodeUnit } from './nodeUnits'
export { default as serializeEvaluation } from './serializeEvaluation'
@ -49,7 +60,7 @@ export { parseUnit, serializeUnit } from './units'
export { parsePublicodes, utils }
export { Rule, RuleNode, ASTNode, EvaluatedNode }
type PublicodesExpression = string | Record<string, unknown> | number
export type PublicodesExpression = string | Record<string, unknown> | number
export type Logger = {
log(message: string): void
@ -150,8 +161,17 @@ export default class Engine<Name extends string = string> {
evaluate(value: PublicodesExpression): EvaluatedNode
evaluate(value: PublicodesExpression | ASTNode): EvaluatedNode {
const cachedNode = this.cache.nodes.get(value)
// The evaluation of parent applicabilty is slightly different from
// regular rules since we cut some of the paths (sums) for optimization.
// That's why we need to have a separate cache for this evaluation.
if (cachedNode !== undefined) {
return cachedNode
} else if (this.inApplicabilityEvaluationContext) {
const cachedNodeApplicability = this.cache.nodesApplicability.get(value)
if (cachedNodeApplicability) {
return cachedNodeApplicability
}
}
let parsedNode: ASTNode
@ -173,7 +193,16 @@ export default class Engine<Name extends string = string> {
this,
parsedNode
)
this.cache.nodes.set(value, evaluatedNode)
// TODO: In most cases the two evaluation provide the same result, this
// could be optimized. The idea would be to use the “nodesApplicability”
// cache iff the rule uses a sum mechanism (ie, some paths are cut from
// the full evaluaiton).
if (!this.inApplicabilityEvaluationContext) {
this.cache.nodes.set(value, evaluatedNode)
} else {
this.cache.nodesApplicability.set(value, evaluatedNode)
}
return evaluatedNode
}
@ -189,6 +218,13 @@ export default class Engine<Name extends string = string> {
newEngine.cache = this.cache
return newEngine
}
get inApplicabilityEvaluationContext(): boolean {
return (
this.cache._meta.parentRuleStack.length > 0 &&
this.cache._meta.disableApplicabilityContextCounter === 0
)
}
}
/**

View File

@ -2,7 +2,9 @@ import { EvaluationFunction, simplifyNodeUnit } from '..'
import { mergeAllMissing } from '../evaluation'
import { registerEvaluationFunction } from '../evaluationFunctions'
import parse from '../parse'
import { ASTNode } from '../AST/types'
import { ASTNode, EvaluatedNode } from '../AST/types'
import { serializeUnit } from '../units'
import { evaluationError } from '../error'
export type ArrondiNode = {
explanation: {
@ -24,6 +26,19 @@ const evaluate: EvaluationFunction<'arrondi'> = function (node) {
let arrondi = node.explanation.arrondi
if (nodeValue !== false) {
arrondi = this.evaluate(arrondi)
if (
typeof (arrondi as EvaluatedNode).nodeValue === 'number' &&
!serializeUnit((arrondi as EvaluatedNode).unit)?.match(/décimales?/)
) {
evaluationError(
this.options.logger,
this.cache._meta.evaluationRuleStack[0],
`L'unité ${serializeUnit(
(arrondi as EvaluatedNode).unit
)} de l'arrondi est inconnu. Vous devez utiliser l'unité décimales`
)
}
}
return {

View File

@ -3,12 +3,6 @@ import { ASTNode } from '../AST/types'
import { defaultNode, mergeAllMissing } from '../evaluation'
import { registerEvaluationFunction } from '../evaluationFunctions'
import parse from '../parse'
import {
liftTemporal2,
liftTemporalNode,
mapTemporal,
temporalAverage,
} from '../temporal'
import { convertUnit, parseUnit } from '../units'
import {
evaluatePlafondUntilActiveTranche,
@ -78,44 +72,28 @@ const evaluate: EvaluationFunction<'barème'> = function (node) {
const evaluate = this.evaluate.bind(this)
const assiette = this.evaluate(node.explanation.assiette)
const multiplicateur = this.evaluate(node.explanation.multiplicateur)
const temporalTranchesPlafond = liftTemporal2(
(assiette, multiplicateur) =>
evaluatePlafondUntilActiveTranche.call(this, {
parsedTranches: node.explanation.tranches,
assiette,
multiplicateur,
}),
liftTemporalNode(assiette as any),
liftTemporalNode(multiplicateur as any)
const tranches = evaluateBarème(
evaluatePlafondUntilActiveTranche.call(this, {
parsedTranches: node.explanation.tranches,
assiette,
multiplicateur,
}),
assiette,
evaluate
)
const temporalTranches = liftTemporal2(
(tranches, assiette) => evaluateBarème(tranches, assiette, evaluate),
temporalTranchesPlafond,
liftTemporalNode(assiette as any)
)
const temporalValue = mapTemporal(
(tranches) =>
tranches.reduce(
(value, { nodeValue }) =>
nodeValue == null ? null : value + nodeValue,
0
),
temporalTranches
const nodeValue = tranches.reduce(
(value, { nodeValue }) => (nodeValue == null ? null : value + nodeValue),
0
)
return {
...node,
nodeValue: temporalAverage(temporalValue),
...(temporalValue.length > 1
? {
temporalValue,
}
: { missingVariables: mergeAllMissing(temporalTranches[0].value) }),
nodeValue,
missingVariables: mergeAllMissing(tranches),
explanation: {
assiette,
multiplicateur,
...(temporalTranches.length > 1
? { temporalTranches }
: { tranches: temporalTranches[0].value }),
tranches,
},
unit: assiette.unit,
} as any

View File

@ -0,0 +1,89 @@
import { EvaluationFunction } from '..'
import { ASTNode } from '../AST/types'
import { defaultNode, mergeAllMissing } from '../evaluation'
import { registerEvaluationFunction } from '../evaluationFunctions'
import parse from '../parse'
import {
evaluatePlafondUntilActiveTranche,
parseTranches,
TrancheNodes,
} from './trancheUtils'
export type GrilleNode = {
explanation: {
assiette: ASTNode
multiplicateur: ASTNode
tranches: TrancheNodes
}
nodeKind: 'grille'
}
export default function parseGrille(v, context): GrilleNode {
const explanation = {
assiette: parse(v.assiette, context),
multiplicateur: v.multiplicateur
? parse(v.multiplicateur, context)
: defaultNode(1),
tranches: parseTranches(v.tranches, context),
}
return {
explanation,
nodeKind: 'grille',
}
}
const evaluate: EvaluationFunction<'grille'> = function (node) {
const evaluate = this.evaluate.bind(this)
const assiette = this.evaluate(node.explanation.assiette)
const multiplicateur = this.evaluate(node.explanation.multiplicateur)
const tranches = evaluatePlafondUntilActiveTranche
.call(this, {
parsedTranches: node.explanation.tranches,
assiette,
multiplicateur,
})
.map((tranche) => {
if (tranche.isActive === false) {
return tranche
}
const montant = evaluate(tranche.montant)
return {
...tranche,
montant,
nodeValue: montant.nodeValue,
unit: montant.unit,
missingVariables: mergeAllMissing([montant, tranche]),
}
})
let activeTranches
const activeTranche = tranches.find((tranche) => tranche.isActive)
if (activeTranche) {
activeTranches = [activeTranche]
} else if (tranches[tranches.length - 1].isAfterActive === false) {
activeTranches = [{ nodeValue: false }]
} else {
activeTranches = tranches.filter((tranche) => tranche.isActive === null)
}
const nodeValue = !activeTranches[0]
? false
: activeTranches[0].isActive === null
? null
: activeTranches[0].nodeValue
return {
...node,
nodeValue,
missingVariables: mergeAllMissing(activeTranches),
explanation: {
...node.explanation,
assiette,
multiplicateur,
tranches,
},
unit: activeTranches[0]?.unit ?? undefined,
} as any
}
registerEvaluationFunction('grille', evaluate)

View File

@ -1,13 +1,11 @@
import { EvaluationFunction } from '..'
import { ASTNode } from '../AST/types'
import { ASTNode, EvaluatedNode } from '../AST/types'
import { convertToDate } from '../date'
import { warning } from '../error'
import { mergeAllMissing } from '../evaluation'
import { registerEvaluationFunction } from '../evaluationFunctions'
import { convertNodeToUnit } from '../nodeUnits'
import parse from '../parse'
import { liftTemporal2, pureTemporal, temporalAverage } from '../temporal'
import { EvaluatedNode } from '../AST/types'
import { inferUnit, serializeUnit } from '../units'
const knownOperations = {
@ -43,10 +41,24 @@ const parseOperation = (k, symbol) => (v, context) => {
}
const evaluate: EvaluationFunction<'operation'> = function (node) {
// When we only need to evaluate the applicability of a rule, we don't enter
// inside “sum terms” since we know that the sum will always be applicable.
// However, if somewhere in the evaluation stack we do a comparison, we need
// to disable this optimization since in this case we'll need the exact value
// of sums in the evaluation subtree.
const disableApplicabilityContext = ['≠', '=', '<', '>', '≤', '≥'].includes(
node.operator
)
if (disableApplicabilityContext && this.inApplicabilityEvaluationContext) {
this.cache._meta.disableApplicabilityContextCounter += 1
}
const explanation = node.explanation.map((node) => this.evaluate(node)) as [
EvaluatedNode,
EvaluatedNode
]
if (disableApplicabilityContext && this.inApplicabilityEvaluationContext) {
this.cache._meta.disableApplicabilityContextCounter -= 1
}
let [node1, node2] = explanation
const missingVariables = mergeAllMissing([node1, node2])
@ -74,7 +86,26 @@ const evaluate: EvaluationFunction<'operation'> = function (node) {
)
}
}
const baseNode = {
const operatorFunction = knownOperations[node.operationKind][0]
const a = node1.nodeValue as string | false
const b = node2.nodeValue as string | false
const nodeValue =
!['≠', '='].includes(node.operator) && a === false && b === false
? false
: ['<', '>', '≤', '≥', '', '×'].includes(node.operator) &&
(a === false || b === false)
? false
: a !== false &&
b !== false &&
['≠', '=', '<', '>', '≤', '≥'].includes(node.operator) &&
[a, b].every((value) => value.match?.(/[\d]{2}\/[\d]{2}\/[\d]{4}/))
? operatorFunction(convertToDate(a), convertToDate(b))
: operatorFunction(a, b)
return {
...node,
explanation,
...((node.operationKind === '*' ||
@ -84,40 +115,7 @@ const evaluate: EvaluationFunction<'operation'> = function (node) {
unit: inferUnit(node.operationKind, [node1.unit, node2.unit]),
}),
missingVariables,
}
const operatorFunction = knownOperations[node.operationKind][0]
const temporalValue = liftTemporal2(
(a: string | false, b: string | false) => {
if (!['≠', '='].includes(node.operator) && a === false && b === false) {
return false
}
if (
['<', '>', '≤', '≥', '', '×'].includes(node.operator) &&
(a === false || b === false)
) {
return false
}
if (
a !== false &&
b !== false &&
['≠', '=', '<', '>', '≤', '≥'].includes(node.operator) &&
[a, b].every((value) => value.match?.(/[\d]{2}\/[\d]{2}\/[\d]{4}/))
) {
return operatorFunction(convertToDate(a), convertToDate(b))
}
return operatorFunction(a, b)
},
node1.temporalValue ?? (pureTemporal(node1.nodeValue) as any),
node2.temporalValue ?? (pureTemporal(node2.nodeValue) as any)
)
const nodeValue = temporalAverage(temporalValue, baseNode.unit)
return {
...baseNode,
nodeValue,
...(temporalValue.length > 1 && { temporalValue }),
}
}

View File

@ -1,7 +1,7 @@
import { EvaluationFunction } from '..'
import { ASTNode } from '../AST/types'
import { warning } from '../error'
import { defaultNode, evaluateObject, parseObject } from '../evaluation'
import { defaultNode, mergeAllMissing, parseObject } from '../evaluation'
import { registerEvaluationFunction } from '../evaluationFunctions'
import { convertNodeToUnit, simplifyNodeUnit } from '../nodeUnits'
import { areUnitConvertible, convertUnit, inferUnit } from '../units'
@ -32,12 +32,12 @@ export const mecanismProduct = (v, context) => {
} as ProductNode
}
const productEffect: EvaluationFunction = function ({
assiette,
taux,
facteur,
plafond,
}: any) {
const evaluateProduit: EvaluationFunction<'produit'> = function (node) {
const assiette = this.evaluate(node.explanation.assiette)
const taux = this.evaluate(node.explanation.taux)
const facteur = this.evaluate(node.explanation.facteur)
let plafond = this.evaluate(node.explanation.plafond)
if (assiette.unit) {
try {
plafond = convertNodeToUnit(assiette.unit, plafond)
@ -72,16 +72,20 @@ const productEffect: EvaluationFunction = function ({
nodeValue = convertUnit(unit, assiette.unit, nodeValue)
unit = assiette.unit
}
return simplifyNodeUnit({
...node,
missingVariables: mergeAllMissing([assiette, taux, facteur, plafond]),
nodeValue,
unit,
explanation: {
plafondActif: assiette.nodeValue > plafond.nodeValue,
assiette,
taux,
facteur,
plafond,
plafondActif: (assiette.nodeValue as any) > (plafond as any).nodeValue,
},
})
}
const evaluate = evaluateObject<'produit'>(productEffect)
registerEvaluationFunction('produit', evaluate)
registerEvaluationFunction('produit', evaluateProduit)

View File

@ -18,7 +18,7 @@ export type RecalculNode = {
const evaluateRecalcul: EvaluationFunction<'recalcul'> = function (node) {
if (this.cache._meta.inRecalcul) {
return (defaultNode(false) as any) as RecalculNode & EvaluatedNode
return defaultNode(null) as any as RecalculNode & EvaluatedNode
}
const amendedSituation = node.explanation.amendedSituation
@ -62,9 +62,6 @@ const evaluateRecalcul: EvaluationFunction<'recalcul'> = function (node) {
},
missingVariables: evaluatedNode.missingVariables,
...('unit' in evaluatedNode && { unit: evaluatedNode.unit }),
...(evaluatedNode.temporalValue && {
temporalValue: evaluatedNode.temporalValue,
}),
}
}

View File

@ -0,0 +1,109 @@
import { EvaluationFunction } from '..'
import { ASTNode, ConstantNode, Unit } from '../AST/types'
import { registerEvaluationFunction } from '../evaluationFunctions'
import parse from '../parse'
import { Context } from '../parsePublicodes'
import uniroot from '../uniroot'
import { UnitéNode } from './unité'
export type RésoudreRéférenceCirculaireNode = {
explanation: {
ruleToSolve: string
valeur: ASTNode
}
nodeKind: 'résoudre référence circulaire'
}
export const evaluateRésoudreRéférenceCirculaire: EvaluationFunction<'résoudre référence circulaire'> =
function (node) {
const originalCache = this.cache
let inversionNumberOfIterations = 0
const evaluateWithValue = (
n: number,
unit: Unit = { numerators: [], denominators: [] }
) => {
inversionNumberOfIterations++
this.resetCache()
this.parsedSituation[node.explanation.ruleToSolve] = {
unit: unit,
nodeKind: 'unité',
explanation: {
nodeKind: 'constant',
nodeValue: n,
type: 'number',
} as ConstantNode,
} as UnitéNode
return this.evaluate(node.explanation.valeur)
}
let nodeValue: number | null | undefined = null
const x0 = 0
let valeur = evaluateWithValue(x0)
const y0 = valeur.nodeValue as number
const unit = valeur.unit
const missingVariables = valeur.missingVariables
let i = 0
if (y0 !== null) {
// The `uniroot` function parameter. It will be called with its `min` and
// `max` arguments, so we can use our cached nodes if the function is called
// with the already computed x1 or x2.
const test = (x: number): number => {
if (x === x0) {
return y0 - x0
}
valeur = evaluateWithValue(x, unit)
const y = valeur.nodeValue
i++
return (y as number) - x
}
const defaultMin = -1_000_000
const defaultMax = 100_000_000
nodeValue = uniroot(test, defaultMin, defaultMax, 0.5, 30, 2)
}
this.cache = originalCache
if (nodeValue === undefined) {
nodeValue = null
this.cache._meta.inversionFail = true
}
if (nodeValue !== null) {
valeur = evaluateWithValue(nodeValue, unit)
}
delete this.parsedSituation[node.explanation.ruleToSolve]
return {
...node,
unit,
nodeValue,
explanation: {
...node.explanation,
valeur,
inversionNumberOfIterations,
},
missingVariables,
}
}
export default function parseRésoudreRéférenceCirculaire(v, context: Context) {
return {
explanation: {
ruleToSolve: context.dottedName,
valeur: parse(v.valeur, context),
},
nodeKind: 'résoudre référence circulaire',
} as RésoudreRéférenceCirculaireNode
}
parseRésoudreRéférenceCirculaire.nom = 'résoudre la référence circulaire'
registerEvaluationFunction(
'résoudre référence circulaire',
evaluateRésoudreRéférenceCirculaire
)

View File

@ -18,4 +18,17 @@ export const mecanismSum = (v, context) => {
} as SommeNode
}
registerEvaluationFunction('somme', evaluate)
registerEvaluationFunction('somme', function (node) {
if (this.inApplicabilityEvaluationContext) {
return {
// With a clearer distinction between `getApplicability` and
// `getValue` we could avoid faking a `nodeValue: true` and instead
// simply return `isApplicable: true, nodeValue: undefined`
nodeValue: true,
nodeKind: 'somme',
missingVariables: {},
explanation: [],
}
}
return evaluate.call(this, node)
})

View File

@ -1,17 +1,10 @@
import { EvaluationFunction } from '..'
import { ASTNode, Unit } from '../AST/types'
import { ASTNode, EvaluatedNode, Unit } from '../AST/types'
import { warning } from '../error'
import { bonus, defaultNode, mergeAllMissing } from '../evaluation'
import { registerEvaluationFunction } from '../evaluationFunctions'
import { convertNodeToUnit } from '../nodeUnits'
import parse from '../parse'
import {
liftTemporal2,
pureTemporal,
sometime,
Temporal,
temporalAverage,
} from '../temporal'
export type VariationNode = {
explanation: Array<{
@ -62,12 +55,12 @@ export default function parseVariations(v, context): VariationNode {
}
const evaluate: EvaluationFunction<'variations'> = function (node) {
const [temporalValue, explanation, unit] = node.explanation.reduce<
const [nodeValue, explanation, unit] = node.explanation.reduce<
[
Temporal<any>,
EvaluatedNode['nodeValue'],
VariationNode['explanation'],
Unit | undefined,
Temporal<any>
boolean | null
]
>(
(
@ -75,11 +68,7 @@ const evaluate: EvaluationFunction<'variations'> = function (node) {
{ condition, consequence },
i: number
) => {
const previousConditionsAlwaysTrue = !sometime(
(value) => value !== true,
previousConditions
)
if (previousConditionsAlwaysTrue) {
if (previousConditions === true) {
return [
evaluation,
[...explanations, { condition, consequence }],
@ -88,24 +77,19 @@ const evaluate: EvaluationFunction<'variations'> = function (node) {
]
}
const evaluatedCondition = this.evaluate(condition)
const currentCondition = liftTemporal2(
(previousCond, currentCond) =>
previousCond === null
? previousCond
: !previousCond &&
(currentCond === null ? null : currentCond !== false),
previousConditions,
evaluatedCondition.temporalValue ??
pureTemporal(evaluatedCondition.nodeValue)
)
const currentCondition =
previousConditions === null
? previousConditions
: !previousConditions &&
(evaluatedCondition.nodeValue === null
? null
: evaluatedCondition.nodeValue !== false)
evaluatedCondition.missingVariables = bonus(
evaluatedCondition.missingVariables
)
const currentConditionAlwaysFalse = !sometime(
(x) => x !== false,
currentCondition
)
if (currentConditionAlwaysFalse) {
if (currentCondition === false) {
return [
evaluation,
[...explanations, { condition: evaluatedCondition, consequence }],
@ -128,15 +112,8 @@ const evaluate: EvaluationFunction<'variations'> = function (node) {
)
}
}
const currentValue = liftTemporal2(
(cond, value) => cond && value,
currentCondition,
evaluatedConsequence.temporalValue ??
pureTemporal(evaluatedConsequence.nodeValue)
)
const or = (a, b) => a || b
return [
liftTemporal2(or, evaluation, currentValue),
currentCondition && evaluatedConsequence.nodeValue,
[
...explanations,
{
@ -146,19 +123,18 @@ const evaluate: EvaluationFunction<'variations'> = function (node) {
},
],
unit || evaluatedConsequence.unit,
liftTemporal2(or, previousConditions, currentCondition),
previousConditions || currentCondition,
]
},
[pureTemporal(false), [], undefined, pureTemporal(false)]
[false, [], undefined, false]
)
const nodeValue = temporalAverage(temporalValue, unit)
const missingVariables = mergeAllMissing(
explanation.reduce<ASTNode[]>(
(values, { condition, consequence }) => [
(values, { condition, satisfied, consequence }) => [
...values,
condition,
consequence,
...(satisfied ? [consequence] : []),
],
[]
)
@ -170,7 +146,6 @@ const evaluate: EvaluationFunction<'variations'> = function (node) {
...(unit !== undefined && { unit }),
explanation,
missingVariables,
...(temporalValue.length > 1 && { temporalValue }),
}
}

View File

@ -1,5 +1,4 @@
import { EvaluatedNode, Unit } from './AST/types'
import { mapTemporal } from './temporal'
import { convertUnit, simplifyUnit } from './units'
export function simplifyNodeUnit(node) {
@ -15,20 +14,12 @@ export function convertNodeToUnit<Node extends EvaluatedNode = EvaluatedNode>(
to: Unit | undefined,
node: Node
): Node {
const temporalValue =
node.temporalValue && node.unit
? mapTemporal(
(value) => convertUnit(node.unit, to, value as number),
node.temporalValue
)
: node.temporalValue
return {
...node,
nodeValue:
node.unit && typeof node.nodeValue === 'number'
? convertUnit(node.unit, to, node.nodeValue)
: node.nodeValue,
...(temporalValue && { temporalValue }),
unit: to,
}
}

View File

@ -28,7 +28,6 @@ import { mecanismSum } from './mecanisms/sum'
import { mecanismSynchronisation } from './mecanisms/synchronisation'
import tauxProgressif from './mecanisms/tauxProgressif'
import unité from './mecanisms/unité'
import variableTemporelle from './mecanisms/variableTemporelle'
import variations, { devariate } from './mecanisms/variations'
import { Context } from './parsePublicodes'
import parseReference from './reference'
@ -190,7 +189,6 @@ const parseFunctions = {
somme: mecanismSum,
multiplication: mecanismProduct,
produit: mecanismProduct,
temporalValue: variableTemporelle,
barème,
grille,
'taux progressif': tauxProgressif,

View File

@ -101,13 +101,12 @@ const equals = <T>(a: T, b: T) => {
}
}
export const removeOnce = <T>(
element: T,
eqFn: (a: T, b: T) => boolean = equals
) => (list: Array<T>): Array<T> => {
const index = list.findIndex((e) => eqFn(e, element))
return list.filter((_, i) => i !== index)
}
export const removeOnce =
<T>(element: T, eqFn: (a: T, b: T) => boolean = equals) =>
(list: Array<T>): Array<T> => {
const index = list.findIndex((e) => eqFn(e, element))
return list.filter((_, i) => i !== index)
}
const simplify = (
unit: Unit,

View File

@ -21,6 +21,12 @@ describe('format engine values', () => {
expect(formatValue(10, { displayedUnit: '%' })).to.equal('10 %')
expect(formatValue(100, { displayedUnit: '%' })).to.equal('100 %')
expect(formatValue(10.2, { displayedUnit: '%' })).to.equal('10,2 %')
expect(
formatValue({
nodeValue: 441,
unit: parseUnit('%.kgCO2e'),
})
).to.equal('4,41 kgCO2e')
})
it('format values', () => {

View File

@ -1,22 +1,25 @@
import { expect } from 'chai'
import Engine from '../source/index'
import { parse } from 'yaml'
describe('Missing variables', function () {
it('should identify missing variables', function () {
const rawRules = {
ko: 'oui',
sum: 'oui',
'sum . startHere': {
formule: 2,
'non applicable si': 'sum . evt . ko',
},
'sum . evt': {
formule: { 'une possibilité': ['ko'] },
titre: 'Truc',
question: '?',
},
'sum . evt . ko': {},
}
// Rules in tests can be expressed in YAML like to for more clarity than JS objects
const rawRules = parse(`
ko: oui
sum: oui
sum . startHere:
formule: 2
non applicable si: sum . evt . ko
sum . evt:
formule:
une possibilité:
- ko
titre: Truc
question: '?'
sum . evt . ko:
`)
const result = Object.keys(
new Engine(rawRules).evaluate('sum . startHere').missingVariables
)
@ -129,60 +132,74 @@ describe('Missing variables', function () {
expect(result).to.be.empty
})
// TODO : réparer ce test
it.skip('should report missing variables in variations', function () {
const rawRules = {
top: 'oui',
'top . startHere': {
formule: { somme: ['variations'] },
},
'top . variations': {
formule: {
variations: [
{
si: 'dix',
alors: {
barème: {
assiette: 2008,
multiplicateur: 'deux',
tranches: [
{ plafond: 1, taux: 0.1 },
{ plafond: 2, taux: 'trois' },
{ taux: 10 },
],
},
},
},
{
si: '3 > 4',
alors: {
barème: {
assiette: 2008,
multiplicateur: 'quatre',
tranches: [
{ plafond: 1, taux: 0.1 },
{ plafond: 2, taux: 1.8 },
{ 'au-dessus de': 2, taux: 10 },
],
},
},
},
],
},
},
'top . dix': {},
'top . deux': {},
'top . trois': {},
'top . quatre': {},
}
it('should report missing variables in simple variations', function () {
const rawRules = parse(`
somme: a + b
a: 10
b:
formule:
variations:
- si: a > 100
alors: c
- sinon: 0
c:
question: Alors ?`)
const result = Object.keys(
new Engine(rawRules).evaluate('top . startHere').missingVariables
new Engine(rawRules).evaluate('somme').missingVariables
)
expect(result).to.include('top . dix')
expect(result).to.include('top . deux')
expect(result).to.include('top . trois')
expect(result).not.to.include('top . quatre')
expect(result).to.have.lengthOf(0)
})
// TODO : réparer ce test
it('should report missing variables in variations', function () {
const rawRules = parse(`
startHere:
formule:
somme:
- variations
variations:
formule:
variations:
- si: dix
alors:
barème:
assiette: 2008
multiplicateur: deux
tranches:
- plafond: 1
taux: 0.1
- plafond: 2
taux: trois
- taux: 10
- si: 3 > 4
alors:
barème:
assiette: 2008
multiplicateur: quatre
tranches:
- plafond: 1
taux: 0.1
- plafond: 2
taux: 1.8
- au-dessus de: 2
taux: 10
dix: {}
deux: {}
trois: {}
quatre: {}
`)
const result = Object.keys(
new Engine(rawRules).evaluate('startHere').missingVariables
)
expect(result).to.include('dix')
expect(result).to.include('deux')
expect(result).to.include('trois')
expect(result).not.to.include('quatre')
})
})
@ -249,4 +266,98 @@ describe('nextSteps', function () {
expect(result).to.eql(['top . sum . evt'])
})
it("Parent's other descendands in sums should not be included as missing variables", function () {
// See https://github.com/betagouv/publicodes/issues/33
const rawRules = parse(`
transport:
somme:
- voiture
- avion
transport . voiture:
formule: empreinte * km
transport . voiture . empreinte: 0.12
transport . voiture . km:
question: COMBIENKM
par défaut: 1000
transport . avion:
applicable si: usager
formule: empreinte * km
transport . avion . km:
question: COMBIENKM
par défaut: 10000
transport . avion . empreinte: 0.300
transport . avion . usager:
question: Prenez-vous l'avion ?
par défaut: oui
`)
const result = Object.keys(
new Engine(rawRules).evaluate('transport . avion').missingVariables
)
expect(result).deep.to.equal([
'transport . avion . km',
'transport . avion . usager',
])
expect(result).to.have.lengthOf(2)
})
it("Parent's other descendands in sums should not be included as missing variables - 2", function () {
// See https://github.com/betagouv/publicodes/issues/33
const rawRules = parse(`
avion:
question: prenez-vous l'avion ?
par défaut: oui
avion . impact:
formule:
somme:
- au sol
- en vol
avion . impact . en vol:
question: Combien de temps passé en vol ?
par défaut: 10
avion . impact . au sol: 5
`)
const result = Object.keys(
new Engine(rawRules).evaluate('avion . impact . au sol').missingVariables
)
expect(result).deep.to.equal(['avion'])
expect(result).to.have.lengthOf(1)
})
it("Parent's other descendands in sums in applicability should be included as missing variables", function () {
// See https://github.com/betagouv/publicodes/issues/33
const rawRules = parse(`
a:
applicable si: d > 3
valeur: oui
d:
formule:
somme:
- e
- 8
e:
question: Vous venez à combien à la soirée ?
par défaut: 3
a . b: 20 + 9
`)
const result = Object.keys(
new Engine(rawRules).evaluate('a . b').missingVariables
)
expect(result).deep.to.equal(['e'])
expect(result).to.have.lengthOf(1)
})
})

View File

@ -16,7 +16,6 @@ prévoyance obligatoire cadre:
statut cadre: non
valeur attendue: false
variable:
par défaut: oui
applicable comme mécanisme chainé:

View File

@ -1,4 +1,3 @@
arrondi oui:
formule:
valeur: 30.4167 jours
@ -24,7 +23,7 @@ arrondi décimales:
demie part:
formule:
valeur: 0.5 * 100.2
arrondi: oui
arrondi: oui
exemples:
- valeur attendue: 50
@ -48,7 +47,7 @@ cotisation retraite:
Arrondi:
formule:
valeur: cotisation retraite
arrondi: oui
arrondi: oui
exemples:
- nom: arrondi en dessous

View File

@ -27,7 +27,6 @@ Conversion de variable:
douches par mois: 30
valeur attendue: 45
unité attendue: kCo2/mois
Conversion de variable et expressions:
unité: kCo2/an

View File

@ -47,10 +47,9 @@ plancher:
exemples:
- valeur attendue: 2500
encadrement inférieur et supérieur:
formule:
somme:
somme:
- 500
- 400
plafond: 800

View File

@ -306,17 +306,16 @@ chaine de charactère:
chaine de charactère: "'je t'y vois'"
valeur attendue: je t'y vois
a: oui
b: 5
a . b: b + 5
a . c: b + 5
désambiguation du nom de règle 1:
formule: a . b
exemples:
exemples:
- valeur attendue: 10
désambiguation du nom de règle 2:
formule: a . c
exemples:
- valeur attendue: 15
exemples:
- valeur attendue: 15

View File

@ -59,7 +59,7 @@ Grille avec valeur manquante:
situation:
assiette: 3000
valeur attendue: 300
- nom: 'assiette au delà du plagond'
- nom: 'assiette au delà du plafond'
situation:
assiette: 5000
valeur attendue: false

Some files were not shown because too many files have changed in this diff Show More