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: a680ad31c33b93e4f35171488cec1b6f9e08179apull/1817/head
parent
f4e3b93af2
commit
2e6ce3ab1a
|
@ -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
|
||||
|
||||
|
|
|
@ -1,3 +0,0 @@
|
|||
node_modules
|
||||
dist
|
||||
publicodes/example/
|
111
.eslintrc.js
111
.eslintrc.js
|
@ -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 +0,0 @@
|
|||
custom: ['https://mon-entreprise.fr/budget']
|
|
@ -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 }}
|
||||
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
||||
|
||||
|
||||
|
|
@ -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,2 +0,0 @@
|
|||
.eslintrc.js
|
||||
dist
|
|
@ -1,9 +0,0 @@
|
|||
{
|
||||
"recommendations": [
|
||||
"esbenp.prettier-vscode",
|
||||
"ban.spellright",
|
||||
"jpoissonnier.vscode-styled-components",
|
||||
"bungcip.better-toml",
|
||||
"mikestead.dotenv"
|
||||
]
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -1 +0,0 @@
|
|||
@types/react-native
|
|
@ -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
|
210
CONTRIBUTING.md
210
CONTRIBUTING.md
|
@ -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 l’hé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>
|
||||
```
|
||||
|
|
58
README.md
58
README.md
|
@ -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++)
|
||||
[![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 d’entreprise 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
|
||||
|
|
|
@ -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",
|
|
@ -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": {
|
|
@ -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),
|
||||
},
|
||||
})
|
|
@ -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>
|
|
@ -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>
|
||||
}
|
|
@ -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()
|
||||
}
|
|
@ -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 %}
|
|
@ -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)
|
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
|
@ -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 {
|
|
@ -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
|
|
@ -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)
|
|
@ -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 }),
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
|
@ -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,
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
)
|
|
@ -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)
|
||||
})
|
|
@ -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 }),
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
}
|
||||
}
|
|
@ -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,
|
|
@ -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,
|
|
@ -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', () => {
|
|
@ -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)
|
||||
})
|
||||
})
|
|
@ -16,7 +16,6 @@ prévoyance obligatoire cadre:
|
|||
statut cadre: non
|
||||
valeur attendue: false
|
||||
|
||||
|
||||
variable:
|
||||
par défaut: oui
|
||||
applicable comme mécanisme chainé:
|
|
@ -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
|
|
@ -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
|
|
@ -47,10 +47,9 @@ plancher:
|
|||
exemples:
|
||||
- valeur attendue: 2500
|
||||
|
||||
|
||||
encadrement inférieur et supérieur:
|
||||
formule:
|
||||
somme:
|
||||
somme:
|
||||
- 500
|
||||
- 400
|
||||
plafond: 800
|
|
@ -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
|
|
@ -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
Loading…
Reference in New Issue