🔥 Supprime Ramda

pull/2110/head
Maxime Quandalle 2022-05-26 18:44:01 +02:00 committed by Maxime Quandalle
parent 3d2021ca21
commit 7f08675e1a
14 changed files with 300 additions and 263 deletions

View File

@ -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",

View File

@ -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 }
}

View File

@ -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`)

View File

@ -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 <Spacing sm />
}
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 <Spacing lg />

View File

@ -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<
<Grid item key={hit.objectID} xs={12} lg={6}>
<SimulateurCardHit
hit={hit}
path={
path(
hit.pathId.split('.'),
sitePaths
) as ExtractFromSimuData<'path'>
}
path={hit.pathId
.split('.')
.reduce<ExtractFromSimuData<'path'>>(
(acc, curr) => acc[curr as any],
sitePaths as any
)}
/>
</Grid>
)

View File

@ -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<Record<DottedName, number>>
@ -39,46 +16,72 @@ type MissingVariables = Partial<Record<DottedName, number>>
export function getNextSteps(
missingVariables: Array<MissingVariables>
): Array<DottedName> {
const byCount = ([, [count]]: [unknown, [number]]) => count
const byScore = ([, [, score]]: [unknown, [unknown, number]]) => score
const missingByTotalScore = reduce<MissingVariables, MissingVariables>(
mergeWith(add),
{},
missingVariables
const missingByTotalScore = missingVariables.reduce<Record<string, number>>(
(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<Record<string, number>>(
(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<number>(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<DottedName>
}
// 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<MissingVariables>,
@ -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<DottedName> {

View File

@ -26,9 +26,12 @@ export const SatisfactionStyle: [
function toPercentage(data: Record<string, number>): Record<string, number> {
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 (
<>
<ResponsiveContainer width="100%" height={400}>
<BarChart data={flattenData}>
<XAxis dataKey="date" tickFormatter={formatMonth} />
<Tooltip content={CustomTooltip} />
{SatisfactionStyle.map(([level, { emoji, color }]) => (
<Bar
key={level}
dataKey={level}
stackId="1"
fill={color}
maxBarSize={50}
>
<LabelList
dataKey={level}
content={() => emoji}
position="left"
/>
</Bar>
))}
</BarChart>
</ResponsiveContainer>
</>
<ResponsiveContainer width="100%" height={400}>
<BarChart data={flattenData}>
<XAxis dataKey="date" tickFormatter={formatMonth} />
<Tooltip content={CustomTooltip} />
{SatisfactionStyle.map(([level, { emoji, color }]) => (
<Bar
key={level}
dataKey={level}
stackId="1"
fill={color}
maxBarSize={50}
>
<LabelList dataKey={level} content={() => emoji} position="left" />
</Bar>
))}
</BarChart>
</ResponsiveContainer>
)
}

View File

@ -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<number> | Data<Record<string, number>>
): number | Record<string, number> => {
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 {

View File

@ -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}

View File

@ -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',

View File

@ -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) {

View File

@ -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<Question> = keys(
mergeAll(flatten(Object.values(LEGAL_STATUS_DETAILS)))
)
const QUESTION_LIST: Array<Question> = [
'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<boolean>(
(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> | LegalStatusRequirements
): Record<LegalStatus, boolean> =>
map(
(statusRequirements) =>
const possibleStatus = (answers: Answers): Record<LegalStatus, boolean> =>
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<LegalStatus, boolean>
export const possibleStatusSelector = (state: {
choixStatutJuridique: State
@ -136,9 +133,14 @@ export const nextQuestionSelector = (state: RootState): Question | null => {
const questionAnswered = Object.keys(
legalStatusRequirements
) as Array<Question>
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 = <T>(l1: Array<T>, l2: Array<T>): Array<T> =>
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<number>(
answerPopulation
.filter((x) => x !== undefined)
.reduce(
(counters: Record<string, number>, 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
}

View File

@ -82,6 +82,24 @@ export function omit<T, K extends keyof T>(obj: T, key: K): Omit<T, K> {
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<E, G extends string>(
arr: Array<E>,
callback: (elm: E, index: number, array: Array<E>) => G
): Record<G, Array<E>> {
return arr.reduce((result, item, currentIndex) => {
const key = callback(item, currentIndex, arr)
result[key] = result[key] || []
result[key].push(item)
return result
}, {} as Record<G, Array<E>>)
}
export function isIterable<T>(obj: unknown): obj is Iterable<T> {
return Symbol.iterator in Object(obj)
}

View File

@ -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"