From 7f08675e1ae9c47dc1bb9463322eaa385271f0a3 Mon Sep 17 00:00:00 2001 From: Maxime Quandalle Date: Thu, 26 May 2022 18:44:01 +0200 Subject: [PATCH] =?UTF-8?q?=F0=9F=94=A5=20Supprime=20Ramda?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- site/package.json | 2 - site/scripts/i18n/translate-ui.js | 13 +- site/scripts/i18n/utils.js | 132 ++++++++--------- site/source/components/QuickLinks.tsx | 11 +- .../components/search/SimulatorHits.tsx | 13 +- .../components/utils/useNextQuestion.tsx | 138 ++++++++++-------- site/source/pages/Stats/SatisfactionChart.tsx | 49 +++---- site/source/pages/Stats/Stats.tsx | 43 ++++-- .../source/pages/gerer/_components/Fields.tsx | 3 +- .../_components/ModeAccompagnement.tsx | 3 +- .../_components/hooks.ts | 5 +- .../selectors/companyStatusSelectors.ts | 113 +++++++------- site/source/utils.ts | 18 +++ yarn.lock | 20 +-- 14 files changed, 300 insertions(+), 263 deletions(-) diff --git a/site/package.json b/site/package.json index a4aad3d3c..87e2cceec 100644 --- a/site/package.json +++ b/site/package.json @@ -83,7 +83,6 @@ "modele-social": "workspace:^", "publicodes": "=1.0.0-beta.40", "publicodes-react": "=1.0.0-beta.40", - "ramda": "^0.27.0", "react": "^17.0.0", "react-color": "^2.14.0", "react-dom": "^17.0.0", @@ -118,7 +117,6 @@ "@storybook/builder-vite": "^0.1.23", "@storybook/react": "^6.5.0-alpha.49", "@storybook/testing-library": "^0.0.9", - "@types/ramda": "^0.26.43", "@types/react": "^17.0.0", "@types/react-color": "^3.0.1", "@types/react-dom": "^17.0.9", diff --git a/site/scripts/i18n/translate-ui.js b/site/scripts/i18n/translate-ui.js index b1276f84d..89c88743a 100644 --- a/site/scripts/i18n/translate-ui.js +++ b/site/scripts/i18n/translate-ui.js @@ -1,5 +1,4 @@ import { readFileSync, writeFileSync } from 'fs' -import { assocPath } from 'ramda' import yaml from 'yaml' import { fetchTranslation, @@ -36,3 +35,15 @@ import { yaml.stringify(originalKeys, { sortMapEntries: true }) ) })() + +function assocPath(path, val, obj) { + if (path.length === 0) return val + + const key = path[0] + + if (path.length >= 2) { + val = assocPath(path.slice(1), val, obj?.[key] ?? {}) + } + + return { ...obj, [key]: val } +} diff --git a/site/scripts/i18n/utils.js b/site/scripts/i18n/utils.js index 7a18901cc..24692bab3 100644 --- a/site/scripts/i18n/utils.js +++ b/site/scripts/i18n/utils.js @@ -1,8 +1,6 @@ import 'dotenv/config.js' import { readFileSync } from 'fs' import 'isomorphic-fetch' -import { stringify } from 'querystring' -import { equals, mergeAll, path as _path, pick } from 'ramda' import yaml from 'yaml' import rules from '../../../modele-social/dist/index.js' @@ -27,68 +25,66 @@ export function getRulesMissingTranslations() { ) let missingTranslations = [] - let resolved = Object.entries(rules) - .map(([dottedName, rule]) => [ - dottedName, - !rule || !rule.titre // && utils.ruleWithDedicatedDocumentationPage(rule)) - ? { ...rule, titre: dottedName.split(' . ').slice(-1)[0] } - : rule, - ]) - .map(([dottedName, rule]) => ({ - [dottedName]: mergeAll( - Object.entries(rule) - .filter(([, v]) => !!v) - .map(([k, v]) => { - let attrToTranslate = attributesToTranslate.find(equals(k)) - if (!attrToTranslate) return {} - let enTrad = attrToTranslate + '.en', - frTrad = attrToTranslate + '.fr' + let resolved = Object.fromEntries( + Object.entries(rules) + .map(([dottedName, rule]) => [ + dottedName, + !rule || !rule.titre // && utils.ruleWithDedicatedDocumentationPage(rule)) + ? { ...rule, titre: dottedName.split(' . ').slice(-1)[0] } + : rule, + ]) + .map(([dottedName, rule]) => [ + dottedName, + Object.fromEntries( + Object.entries(rule) + .filter(([, v]) => !!v) + .map(([k, v]) => { + let attrToTranslate = attributesToTranslate.find( + (attr) => attr === k + ) + if (!attrToTranslate) return [] + let enTrad = attrToTranslate + '.en' + let frTrad = attrToTranslate + '.fr' - let currentTranslation = currentExternalization[dottedName] + let currentTranslation = currentExternalization[dottedName] - if ('suggestions' === attrToTranslate) { - return Object.keys(v).reduce((acc, suggestion) => { - const enTrad = `suggestions.${suggestion}.en` - const frTrad = `suggestions.${suggestion}.fr` - if ( - currentTranslation && - currentTranslation[enTrad] && - currentTranslation[frTrad] === suggestion - ) { - return { - ...acc, - [frTrad]: currentTranslation[frTrad], - [enTrad]: currentTranslation[enTrad], + if ('suggestions' === attrToTranslate) { + return Object.keys(v).reduce((acc, suggestion) => { + const enTrad = `suggestions.${suggestion}.en` + const frTrad = `suggestions.${suggestion}.fr` + if ( + currentTranslation?.[enTrad] && + currentTranslation?.[frTrad] === suggestion + ) { + return [ + ...acc, + [frTrad, currentTranslation[frTrad]], + [enTrad, currentTranslation[enTrad]], + ] } - } - missingTranslations.push([dottedName, enTrad, suggestion]) - return { - ...acc, - [frTrad]: suggestion, - } - }, {}) - } - - // Check if a human traduction exists already for this attribute and if - // it does need to be updated - if ( - currentTranslation && - currentTranslation[enTrad] && - currentTranslation[frTrad] === v - ) - return { - [enTrad]: currentTranslation[enTrad], - [frTrad]: v, + missingTranslations.push([dottedName, enTrad, suggestion]) + return [...acc, [frTrad, suggestion]] + }, []) } - missingTranslations.push([dottedName, enTrad, v]) - return { - [frTrad]: v, - } - }) - ), - })) - resolved = mergeAll(resolved) + // Check if a human traduction exists already for this attribute and if + // it does need to be updated + if ( + currentTranslation && + currentTranslation[enTrad] && + currentTranslation[frTrad] === v + ) + return [ + [enTrad, currentTranslation[enTrad]], + [frTrad, v], + ] + missingTranslations.push([dottedName, enTrad, v]) + return [[frTrad, v]] + }) + .flat() + ), + ]) + ) return [missingTranslations, resolved] } @@ -105,26 +101,32 @@ export const getUiMissingTranslations = () => { return false } const keys = key.split(/(?<=[A-zÀ-ü0-9])\.(?=[A-zÀ-ü0-9])/) - - const isNewKey = !_path(keys, translatedKeys) - const isInvalidatedKey = _path(keys, originalKeys) !== valueInSource + const pathReducer = (currentSelection, subPath) => + currentSelection?.[subPath] + const isNewKey = !keys.reduce(pathReducer, translatedKeys) + const isInvalidatedKey = + keys.reduce(pathReducer, originalKeys) !== valueInSource return isNewKey || isInvalidatedKey }, staticKeys) .map(([key]) => key) - return pick(missingTranslations, staticKeys) + return Object.fromEntries( + Object.entries(staticKeys).filter(([key]) => + missingTranslations.includes(key) + ) + ) } export const fetchTranslation = async (text) => { const response = await fetch( - `https://api.deepl.com/v2/translate?${stringify({ + `https://api.deepl.com/v2/translate?${new URLSearchParams({ text, auth_key: process.env.DEEPL_API_SECRET, tag_handling: 'xml', source_lang: 'FR', target_lang: 'EN', - })}` + }).toString()}` ) if (response.status !== 200) { console.error(`❌ Deepl return status ${response.status} for:\n\t${text}\n`) diff --git a/site/source/components/QuickLinks.tsx b/site/source/components/QuickLinks.tsx index 57230b843..17e4d11d2 100644 --- a/site/source/components/QuickLinks.tsx +++ b/site/source/components/QuickLinks.tsx @@ -2,8 +2,6 @@ import { goToQuestion } from '@/actions/actions' import { Spacing } from '@/design-system/layout' import { Link } from '@/design-system/typography/link' import { SmallBody } from '@/design-system/typography/paragraphs' -import { DottedName } from 'modele-social' -import { contains, filter, pipe, reject, toPairs } from 'ramda' import { Trans } from 'react-i18next' import { useDispatch, useSelector } from 'react-redux' import { RootState } from '@/reducers/rootReducer' @@ -26,11 +24,10 @@ export default function QuickLinks() { if (!quickLinks) { return } - const links = pipe( - reject((dottedName: DottedName) => contains(dottedName, quickLinksToHide)), - filter((dottedName: DottedName) => contains(dottedName, nextSteps)), - toPairs - )(quickLinks) + const links = Object.entries(quickLinks).filter( + ([, dottedName]) => + nextSteps.includes(dottedName) && !quickLinksToHide.includes(dottedName) + ) if (links.length < 1) { return diff --git a/site/source/components/search/SimulatorHits.tsx b/site/source/components/search/SimulatorHits.tsx index 5e1e99f12..d83e9dd18 100644 --- a/site/source/components/search/SimulatorHits.tsx +++ b/site/source/components/search/SimulatorHits.tsx @@ -6,7 +6,6 @@ import InfoBulle from '@/design-system/InfoBulle' import { H3 } from '@/design-system/typography/heading' import { ExtractFromSimuData } from '@/pages/Simulateurs/metadata' import { MetadataSrc } from '@/pages/Simulateurs/metadata-src' -import { path } from 'ramda' import { useContext } from 'react' import { Trans } from 'react-i18next' import { Hit } from 'react-instantsearch-core' @@ -69,12 +68,12 @@ export const SimulatorHits = connectHits< - } + path={hit.pathId + .split('.') + .reduce>( + (acc, curr) => acc[curr as any], + sitePaths as any + )} /> ) diff --git a/site/source/components/utils/useNextQuestion.tsx b/site/source/components/utils/useNextQuestion.tsx index 110b6a4da..74d3c9278 100644 --- a/site/source/components/utils/useNextQuestion.tsx +++ b/site/source/components/utils/useNextQuestion.tsx @@ -1,29 +1,3 @@ -import { DottedName } from 'modele-social' -import { - add, - countBy, - descend, - difference, - equals, - flatten, - head, - identity, - keys, - last, - length, - map, - mergeWith, - pair, - pipe, - reduce, - sortBy, - sortWith, - takeWhile, - toPairs, - zipWith, -} from 'ramda' -import { useContext, useMemo } from 'react' -import { useSelector } from 'react-redux' import { Simulation, SimulationConfig } from '@/reducers/rootReducer' import { answeredQuestionsSelector, @@ -32,6 +6,9 @@ import { objectifsSelector, situationSelector, } from '@/selectors/simulationSelectors' +import { DottedName } from 'modele-social' +import { useContext, useMemo } from 'react' +import { useSelector } from 'react-redux' import { EngineContext } from './EngineContext' type MissingVariables = Partial> @@ -39,46 +16,72 @@ type MissingVariables = Partial> export function getNextSteps( missingVariables: Array ): Array { - const byCount = ([, [count]]: [unknown, [number]]) => count - const byScore = ([, [, score]]: [unknown, [unknown, number]]) => score - const missingByTotalScore = reduce( - mergeWith(add), - {}, - missingVariables + const missingByTotalScore = missingVariables.reduce>( + (acc, mv) => ({ + ...acc, + ...Object.fromEntries( + Object.entries(mv).map(([name, score]) => [ + name, + (acc[name] || 0) + score, + ]) + ), + }), + {} ) - const innerKeys = flatten(map(keys, missingVariables)) + const innerKeys = missingVariables.map((mv) => Object.keys(mv)).flat() const missingByTargetsAdvanced = Object.fromEntries( - Object.entries(countBy(identity, innerKeys)).map( + Object.entries( + innerKeys.reduce>( + (counters, key) => ({ + ...counters, + [key]: (counters[key] ?? 0) + 1, + }), + {} + ) + ).map( // Give higher score to top level questions ([name, score]) => [name, score + Math.max(0, 4 - name.split('.').length)] ) ) - const missingByCompound = mergeWith( - pair, - missingByTargetsAdvanced, - missingByTotalScore - ) - const pairs = toPairs(missingByCompound) - const sortedPairs = sortWith( - [descend(byCount), descend(byScore) as any], - pairs + const missingByCompoundEntries = [ + ...new Set([ + ...Object.keys(missingByTargetsAdvanced), + ...Object.keys(missingByTotalScore), + ]), + ].map((name): [string, { score: number; count: number }] => [ + name, + { + count: missingByTargetsAdvanced[name] ?? 0, + score: missingByTotalScore[name] ?? 0, + }, + ]) + + const sortedEntries = missingByCompoundEntries.sort( + ([, scoresA], [, scoresB]) => { + if (scoresA.count === scoresB.count) { + return scoresB.score - scoresA.score + } else { + return scoresB.count - scoresA.count + } + } ) - return map(head, sortedPairs) as any + return sortedEntries.map(([name]) => name) as Array } // Max : 1 // Min -> 0 -const questionDifference = (rule1 = '', rule2 = '') => - 1 / - (1 + - pipe( - zipWith(equals), - takeWhile(Boolean), - length - )(rule1.split(' . '), rule2.split(' . '))) +const questionDifference = (ruleA = '', ruleB = '') => { + if (ruleA === ruleB) { + return 0 + } + const partsA = ruleA.split(' . ') + const partsB = ruleB.split(' . ') + + return 1 / (1 + partsA.findIndex((val, i) => partsB?.[i] !== val)) +} export function getNextQuestions( missingVariables: Array, @@ -93,17 +96,22 @@ export function getNextQuestions( "à l'affiche": displayed = {}, } = questionConfig - let nextSteps = difference( - [...Object.values(displayed), ...getNextSteps(missingVariables)], - answeredQuestions - ) - nextSteps = nextSteps.filter( - (step) => - (!whitelist.length || whitelist.some((name) => step.startsWith(name))) && - (!blacklist.length || !blacklist.some((name) => step === name)) - ) + const nextSteps = [ + ...new Set([ + ...Object.values(displayed), + ...getNextSteps(missingVariables), + ]), + ] + .filter((name) => !answeredQuestions.includes(name)) + .filter( + (step) => + (!whitelist.length || + whitelist.some((name) => step.startsWith(name))) && + (!blacklist.length || !blacklist.some((name) => step === name)) + ) + + const lastStep = answeredQuestions[answeredQuestions.length - 1] - const lastStep = last(answeredQuestions) // L'ajout de la réponse permet de traiter les questions dont la réponse est // "une possibilité", exemple "contrat salarié . cdd" const lastStepWithAnswer = @@ -114,7 +122,7 @@ export function getNextQuestions( .trim() as DottedName) : lastStep - return sortBy((question) => { + const score = (question: string) => { const indexList = whitelist.findIndex((name) => question.startsWith(name)) + 1 const indexNotPriority = @@ -122,7 +130,9 @@ export function getNextQuestions( const differenceCoeff = questionDifference(question, lastStepWithAnswer) return indexList + indexNotPriority + differenceCoeff - }, nextSteps) + } + + return nextSteps.sort((a, b) => score(a) - score(b)) } export const useNextQuestions = function (): Array { diff --git a/site/source/pages/Stats/SatisfactionChart.tsx b/site/source/pages/Stats/SatisfactionChart.tsx index 4202b7876..8f3d50440 100644 --- a/site/source/pages/Stats/SatisfactionChart.tsx +++ b/site/source/pages/Stats/SatisfactionChart.tsx @@ -26,9 +26,12 @@ export const SatisfactionStyle: [ function toPercentage(data: Record): Record { const total = Object.values(data).reduce((a, b: number) => a + b, 0) - return Object.fromEntries( - Object.entries(data).map(([key, value]) => [key, (100 * value) / total]) - ) + return { + ...Object.fromEntries( + Object.entries(data).map(([key, value]) => [key, (100 * value) / total]) + ), + total, + } } type SatisfactionChartProps = { @@ -47,29 +50,23 @@ export default function SatisfactionChart({ data }: SatisfactionChartProps) { .filter((d) => Object.values(d.nombre).reduce((a, b) => a + b, 0)) return ( - <> - - - - - {SatisfactionStyle.map(([level, { emoji, color }]) => ( - - emoji} - position="left" - /> - - ))} - - - + + + + + {SatisfactionStyle.map(([level, { emoji, color }]) => ( + + emoji} position="left" /> + + ))} + + ) } diff --git a/site/source/pages/Stats/Stats.tsx b/site/source/pages/Stats/Stats.tsx index b321975ae..fe5d417af 100644 --- a/site/source/pages/Stats/Stats.tsx +++ b/site/source/pages/Stats/Stats.tsx @@ -8,13 +8,12 @@ import { Item, Select } from '@/design-system/field/Select' import { Spacing } from '@/design-system/layout' import { H2, H3 } from '@/design-system/typography/heading' import { formatValue } from 'publicodes' -import { add, groupBy, mapObjIndexed, mergeWith } from 'ramda' import { useCallback, useEffect, useMemo, useState } from 'react' import { Trans } from 'react-i18next' import { useHistory, useLocation } from 'react-router-dom' import { toAtString } from '../../ATInternetTracking' import statsJson from '@/data/stats.json' -import { debounce } from '../../utils' +import { debounce, groupBy } from '../../utils' import { SimulateurCard } from '../Simulateurs/Home' import useSimulatorsData, { SimulatorData } from '../Simulateurs/metadata' import Chart, { Data, isDataStacked } from './Chart' @@ -48,20 +47,24 @@ const isPAM = (name: string | undefined) => const filterByChapter2 = (pages: Pageish[], chapter2: Chapter2 | '') => { return Object.entries( groupBy( - (p) => ('date' in p ? p.date : p.month), pages.filter( (p) => !chapter2 || ((!('page' in p) || p.page !== 'accueil_pamc') && (p.page_chapter2 === chapter2 || (chapter2 === 'PAM' && isPAM(p.page_chapter3)))) - ) + ), + (p) => ('date' in p ? p.date : p.month) ) ).map(([date, values]) => ({ date, - nombre: mapObjIndexed( - (v: Array<{ nombre: number }>) => v.map((v) => v.nombre).reduce(add), - groupBy((x) => ('page' in x ? x.page : x.click), values) + nombre: Object.fromEntries( + Object.entries( + groupBy(values, (x) => ('page' in x ? x.page : x.click)) + ).map(([key, values]) => [ + key, + values.reduce((sum, value) => sum + value.nombre, 0), + ]) ), })) } @@ -69,16 +72,19 @@ const filterByChapter2 = (pages: Pageish[], chapter2: Chapter2 | '') => { function groupByDate(data: Pageish[]) { return Object.entries( groupBy( - (p) => ('date' in p ? p.date : p.month), - data.filter((d) => 'page' in d && d.page === 'accueil') + data.filter((d) => 'page' in d && d.page === 'accueil'), + (p) => ('date' in p ? p.date : p.month) ) ).map(([date, values]) => ({ date, nombre: Object.fromEntries( Object.entries( - groupBy((x) => x.page_chapter1 + ' / ' + x.page_chapter2, values) + groupBy(values, (x) => x.page_chapter1 + ' / ' + x.page_chapter2) ) - .map(([k, v]) => [k, v.map((v) => v.nombre).reduce(add, 0)] as const) + .map( + ([k, v]) => + [k, v.map((v) => v.nombre).reduce((a, b) => a + b, 0)] as const + ) .sort((a, b) => b[1] - a[1]) .slice(0, 7) ), @@ -89,8 +95,19 @@ const computeTotals = ( data: Data | Data> ): number | Record => { return isDataStacked(data) - ? data.map((d) => d.nombre).reduce(mergeWith(add), {}) - : data.map((d) => d.nombre).reduce(add, 0) + ? data + .map((d) => d.nombre) + .reduce( + (acc, record) => + [...Object.entries(acc), ...Object.entries(record)].reduce( + (merge, [key, value]) => { + return { ...merge, [key]: (acc[key] ?? 0) + value } + }, + {} + ), + {} + ) + : data.map((d) => d.nombre).reduce((a, b) => a + b, 0) } interface BrushStartEndIndex { diff --git a/site/source/pages/gerer/_components/Fields.tsx b/site/source/pages/gerer/_components/Fields.tsx index c47700831..eb220e4d6 100644 --- a/site/source/pages/gerer/_components/Fields.tsx +++ b/site/source/pages/gerer/_components/Fields.tsx @@ -16,7 +16,6 @@ import { evaluateQuestion, getMeta } from '@/utils' import { useSSRSafeId } from '@react-aria/ssr' import { DottedName } from 'modele-social' import { RuleNode } from 'publicodes' -import { isEmpty } from 'ramda' import { useCallback, useContext } from 'react' import { useDispatch, useSelector } from 'react-redux' import styled from 'styled-components' @@ -135,7 +134,7 @@ export function SimpleField({ required={meta.requis === 'oui'} missing={ evaluation.nodeValue === undefined || - !isEmpty(evaluation.missingVariables) + Object.keys(evaluation.missingVariables).length > 0 } onChange={dispatchValue} showSuggestions={showSuggestions} diff --git a/site/source/pages/gerer/declaration-revenu-independants/_components/ModeAccompagnement.tsx b/site/source/pages/gerer/declaration-revenu-independants/_components/ModeAccompagnement.tsx index d69a1c7ec..809d1f708 100644 --- a/site/source/pages/gerer/declaration-revenu-independants/_components/ModeAccompagnement.tsx +++ b/site/source/pages/gerer/declaration-revenu-independants/_components/ModeAccompagnement.tsx @@ -7,7 +7,6 @@ import { Strong } from '@/design-system/typography' import { H3 } from '@/design-system/typography/heading' import { Body, SmallBody } from '@/design-system/typography/paragraphs' import { useOrdinal } from '@/hooks/useOrdinal' -import { isEmpty } from 'ramda' import { useCallback } from 'react' import { Trans } from 'react-i18next' import { useDispatch } from 'react-redux' @@ -28,7 +27,7 @@ export default function ModeAccompagnement() { const dispatch = useDispatch() const imposition = engine.evaluate('entreprise . imposition') - if (isSelected && !isEmpty(imposition.missingVariables)) { + if (isSelected && Object.keys(imposition.missingVariables).length > 0) { dispatch( updateSituation( 'entreprise . imposition', diff --git a/site/source/pages/gerer/declaration-revenu-independants/_components/hooks.ts b/site/source/pages/gerer/declaration-revenu-independants/_components/hooks.ts index 86c7353ba..4fd3d56ac 100644 --- a/site/source/pages/gerer/declaration-revenu-independants/_components/hooks.ts +++ b/site/source/pages/gerer/declaration-revenu-independants/_components/hooks.ts @@ -1,7 +1,6 @@ import { useEngine } from '@/components/utils/EngineContext' import { DottedName } from 'modele-social' import { RuleNode } from 'publicodes' -import { isEmpty } from 'ramda' import { useMemo } from 'react' export function useProgress(objectifs: DottedName[]): number { @@ -13,8 +12,8 @@ export function useProgress(objectifs: DottedName[]): number { const objectifsApplicables = evaluatedObjectifs.filter( (objectif) => objectif.nodeValue !== null ) - const objectifsRemplis = objectifsApplicables.filter((objectif) => - isEmpty(objectif.missingVariables) + const objectifsRemplis = objectifsApplicables.filter( + (objectif) => Object.keys(objectif.missingVariables).length === 0 ) if (!objectifsApplicables.length) { diff --git a/site/source/selectors/companyStatusSelectors.ts b/site/source/selectors/companyStatusSelectors.ts index 8a1839886..d672bfe78 100644 --- a/site/source/selectors/companyStatusSelectors.ts +++ b/site/source/selectors/companyStatusSelectors.ts @@ -1,17 +1,4 @@ import { SitePathsContext } from '@/components/utils/SitePathsContext' -import { - add, - any, - countBy, - difference, - flatten, - isNil, - keys, - map, - mergeAll, - mergeWith, - sortBy, -} from 'ramda' import { useContext } from 'react' import { useSelector } from 'react-redux' import { RootState } from '@/reducers/rootReducer' @@ -92,39 +79,49 @@ const LEGAL_STATUS_DETAILS = { export type LegalStatus = keyof typeof LEGAL_STATUS_DETAILS type Question = keyof LegalStatusRequirements +type Answers = LegalStatusRequirements -const QUESTION_LIST: Array = keys( - mergeAll(flatten(Object.values(LEGAL_STATUS_DETAILS))) -) +const QUESTION_LIST: Array = [ + 'soleProprietorship', + 'directorStatus', + 'minorityDirector', + 'multipleAssociates', + 'autoEntrepreneur', +] -const isCompatibleStatusWith = - (answers: any) => - (statusRequirements: LegalStatusRequirements): boolean => { - const stringify = map((x) => (!isNil(x) ? JSON.stringify(x) : x)) - const answerCompatibility = Object.values( - mergeWith( - (answer, statusValue) => - isNil(answer) || isNil(statusValue) || answer === statusValue, - stringify(statusRequirements as any), - stringify(answers) +function isCompatibleStatusWith( + answers: Answers, + statusRequirements: LegalStatusRequirements +): boolean { + return Object.entries(statusRequirements).reduce( + (isCompatible, [question, statusValue]) => { + const answer = answers[question as Question] + + return ( + isCompatible && + (answer == null || + statusValue == null || + JSON.stringify(answer) === JSON.stringify(statusValue)) ) - ) - const isCompatibleStatus = answerCompatibility.every((x) => x !== false) + }, + true + ) +} - return isCompatibleStatus - } -const possibleStatus = ( - answers: Array | LegalStatusRequirements -): Record => - map( - (statusRequirements) => +const possibleStatus = (answers: Answers): Record => + Object.fromEntries( + Object.entries(LEGAL_STATUS_DETAILS).map(([key, statusRequirements]) => [ + key, Array.isArray(statusRequirements) - ? any(isCompatibleStatusWith(answers as any), statusRequirements) - : isCompatibleStatusWith(answers as any)( + ? !!statusRequirements.some((requirement) => + isCompatibleStatusWith(answers, requirement) + ) + : isCompatibleStatusWith( + answers, statusRequirements as LegalStatusRequirements ), - LEGAL_STATUS_DETAILS - ) + ]) + ) as Record export const possibleStatusSelector = (state: { choixStatutJuridique: State @@ -136,9 +133,14 @@ export const nextQuestionSelector = (state: RootState): Question | null => { const questionAnswered = Object.keys( legalStatusRequirements ) as Array - const possibleStatusList = flatten( - Object.values(LEGAL_STATUS_DETAILS) - ).filter(isCompatibleStatusWith(legalStatusRequirements) as any) + const possibleStatusList = Object.values(LEGAL_STATUS_DETAILS) + .flat() + .filter((requirement) => + isCompatibleStatusWith(legalStatusRequirements, requirement as any) + ) + + const difference = (l1: Array, l2: Array): Array => + l1.filter((x) => !l2.includes(x)) const unansweredQuestions = difference(QUESTION_LIST, questionAnswered) const shannonEntropyByQuestion = unansweredQuestions.map( @@ -146,24 +148,31 @@ export const nextQuestionSelector = (state: RootState): Question | null => { const answerPopulation = Object.values(possibleStatusList).map( (status: any) => status[question] ) - const frequencyOfAnswers = Object.values( - countBy( - (x) => x, - answerPopulation.filter((x) => x !== undefined) - ) + + const frequencyOfAnswers = Object.values( + answerPopulation + .filter((x) => x !== undefined) + .reduce( + (counters: Record, i) => ({ + ...counters, + [i]: (counters?.[i] ?? 0) + 1, + }), + {} + ) ).map((numOccurrence) => numOccurrence / answerPopulation.length) const shannonEntropy = -frequencyOfAnswers .map((p) => p * Math.log2(p)) - .reduce(add, 0) + .reduce((a, b) => a + b, 0) return [question, shannonEntropy] } ) - const sortedPossibleNextQuestions = sortBy( - ([, entropy]) => -entropy, - shannonEntropyByQuestion.filter(([, entropy]) => entropy !== 0) - ).map(([question]) => question) + const sortedPossibleNextQuestions = shannonEntropyByQuestion + .filter(([, entropy]) => entropy !== 0) + .sort(([, entropy1], [, entropy2]) => entropy2 - entropy1) + .map(([question]) => question) + if (sortedPossibleNextQuestions.length === 0) { return null } diff --git a/site/source/utils.ts b/site/source/utils.ts index 52511a6e5..cbefceec9 100644 --- a/site/source/utils.ts +++ b/site/source/utils.ts @@ -82,6 +82,24 @@ export function omit(obj: T, key: K): Omit { return returnObject } +// TODO: This is will be included in the ES spec soon. Remove our custom +// implementation and rely on browser native support and polyfill when it is +// available. +// https://developer.mozilla.org/fr/docs/Web/JavaScript/Reference/Global_Objects/Array/groupBy +// https://caniuse.com/?search=groupby +export function groupBy( + arr: Array, + callback: (elm: E, index: number, array: Array) => G +): Record> { + return arr.reduce((result, item, currentIndex) => { + const key = callback(item, currentIndex, arr) + result[key] = result[key] || [] + result[key].push(item) + + return result + }, {} as Record>) +} + export function isIterable(obj: unknown): obj is Iterable { return Symbol.iterator in Object(obj) } diff --git a/yarn.lock b/yarn.lock index 1ad505be2..d8a9916a2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6065,15 +6065,6 @@ __metadata: languageName: node linkType: hard -"@types/ramda@npm:^0.26.43": - version: 0.26.44 - resolution: "@types/ramda@npm:0.26.44" - dependencies: - ts-toolbelt: ^6.3.3 - checksum: 5dbb5dfb0311d4c777ee4d65df4c553a5af4bde06148c149d70fffdfe2498fe3709d892e681f4bdba907662e74ed7d7a09359d13b489f0797edc20719de45a10 - languageName: node - linkType: hard - "@types/react-color@npm:^3.0.1": version: 3.0.6 resolution: "@types/react-color@npm:3.0.6" @@ -16803,7 +16794,7 @@ __metadata: languageName: node linkType: hard -"ramda@npm:^0.27.0, ramda@npm:^0.27.1": +"ramda@npm:^0.27.1": version: 0.27.2 resolution: "ramda@npm:0.27.2" checksum: 28d6735dd1eea1a796c56cf6111f3673c6105bbd736e521cdd7826c46a18eeff337c2dba4668f6eed990d539b9961fd6db19aa46ccc1530ba67a396c0a9f580d @@ -18473,7 +18464,6 @@ __metadata: "@storybook/builder-vite": ^0.1.23 "@storybook/react": ^6.5.0-alpha.49 "@storybook/testing-library": ^0.0.9 - "@types/ramda": ^0.26.43 "@types/react": ^17.0.0 "@types/react-color": ^3.0.1 "@types/react-dom": ^17.0.9 @@ -18500,7 +18490,6 @@ __metadata: modele-social: "workspace:^" publicodes: =1.0.0-beta.40 publicodes-react: =1.0.0-beta.40 - ramda: ^0.27.0 react: ^17.0.0 react-color: ^2.14.0 react-dom: ^17.0.0 @@ -19691,13 +19680,6 @@ __metadata: languageName: node linkType: hard -"ts-toolbelt@npm:^6.3.3": - version: 6.15.5 - resolution: "ts-toolbelt@npm:6.15.5" - checksum: 24ad00cfd9ce735c76c873a9b1347eac475b94e39ebbdf100c9019dce88dd5f4babed52884cf82bb456a38c28edd0099ab6f704b84b2e5e034852b618472c1f3 - languageName: node - linkType: hard - "tsconfig-paths@npm:^3.14.1": version: 3.14.1 resolution: "tsconfig-paths@npm:3.14.1"