diff --git a/modele-social/règles/salarié.yaml b/modele-social/règles/salarié.yaml index 459853889..84afbe862 100644 --- a/modele-social/règles/salarié.yaml +++ b/modele-social/règles/salarié.yaml @@ -1194,7 +1194,7 @@ contrat salarié . rémunération . brut de base: unité: €/mois suggestions: salaire médian: 2300 €/mois - SMIC: contrat salarié . SMIC contractuel + SMIC: SMIC contractuel formule: inversion numérique: question: Quel est le salaire ? diff --git a/publicodes/core/package.json b/publicodes/core/package.json index 5b7f575be..1f14689cc 100644 --- a/publicodes/core/package.json +++ b/publicodes/core/package.json @@ -31,7 +31,6 @@ "dependencies": { "moo": "^0.5.1", "nearley": "^2.19.2", - "ramda": "^0.27.0", "yaml": "^1.9.2" }, "scripts": { diff --git a/publicodes/core/source/AST/graph.ts b/publicodes/core/source/AST/graph.ts index 36cca58bb..3d072fa66 100644 --- a/publicodes/core/source/AST/graph.ts +++ b/publicodes/core/source/AST/graph.ts @@ -1,5 +1,4 @@ import graphlib from '@dagrejs/graphlib' -import * as R from 'ramda' import parsePublicodes from '../parsePublicodes' import { RuleNode } from '../rule' import { reduceAST } from './index' @@ -10,9 +9,10 @@ type GraphCyclesWithDependencies = Array function buildRulesDependencies( parsedRules: Record ): RulesDependencies { + const uniq = (arr: Array): Array => [...new Set(arr)] return Object.entries(parsedRules).map(([name, node]) => [ name, - R.uniq(buildRuleDependancies(node)), + uniq(buildRuleDependancies(node)), ]) } @@ -79,7 +79,7 @@ export function cyclicDependencies( const rulesDependencies = buildRulesDependencies(parsedRules) const dependenciesGraph = buildDependenciesGraph(rulesDependencies) const cycles = (graphlib as any).alg.findCycles(dependenciesGraph) - const rulesDependenciesObject = R.fromPairs(rulesDependencies) + const rulesDependenciesObject = Object.fromEntries(rulesDependencies) return cycles.map((cycle) => { const c = cycle.reverse() diff --git a/publicodes/core/source/AST/index.ts b/publicodes/core/source/AST/index.ts index 6205401ee..bb6ca5484 100644 --- a/publicodes/core/source/AST/index.ts +++ b/publicodes/core/source/AST/index.ts @@ -1,4 +1,3 @@ -import { mapObjIndexed } from 'ramda' import { InternalError } from '../error' import { TrancheNodes } from '../mecanisms/trancheUtils' import { ReplacementRule } from '../replacement' @@ -151,7 +150,9 @@ const traverseASTNode: TraverseFunction = (fn, node) => { const traverseRuleNode: TraverseFunction<'rule'> = (fn, node) => ({ ...node, replacements: node.replacements.map(fn) as Array, - suggestions: mapObjIndexed(fn, node.suggestions), + suggestions: Object.fromEntries( + Object.entries(node.suggestions).map(([key, value]) => [key, fn(value)]) + ), explanation: { parent: node.explanation.parent && fn(node.explanation.parent), valeur: fn(node.explanation.valeur), diff --git a/publicodes/core/source/evaluation.ts b/publicodes/core/source/evaluation.ts index d51965d50..7e651ef4f 100644 --- a/publicodes/core/source/evaluation.ts +++ b/publicodes/core/source/evaluation.ts @@ -1,13 +1,3 @@ -import { - add, - evolve, - fromPairs, - keys, - map, - mapObjIndexed, - mergeWith, - reduce, -} from 'ramda' import Engine, { EvaluationFunction } from '.' import { ASTNode, @@ -34,12 +24,20 @@ export const collectNodeMissing = ( ): Record => 'missingVariables' in node ? node.missingVariables : {} -export const bonus = (missings, hasCondition = true) => - hasCondition ? map((x) => x + 0.0001, missings || {}) : missings +export const bonus = (missings: Record = {}) => + Object.fromEntries( + Object.entries(missings).map(([key, value]) => [key, value + 0.0001]) + ) export const mergeMissing = ( - left: Record | undefined, - right: Record | undefined -): Record => mergeWith(add, left || {}, right || {}) + left: Record | undefined = {}, + right: Record | undefined = {} +): Record => + Object.fromEntries( + [...Object.keys(left), ...Object.keys(right)].map((key) => [ + key, + (left[key] ?? 0) + (right[key] ?? 0), + ]) + ) export const mergeAllMissing = (missings: Array) => missings.map(collectNodeMissing).reduce(mergeMissing, {}) @@ -66,8 +64,8 @@ function convertNodesToSameUnit(nodes, contextRule, mecanismName) { } export const evaluateArray: ( - reducer: Parameters[0], - start: Parameters[1] + reducer, + start ) => EvaluationFunction = (reducer, start) => function (node: any) { const evaluate = this.evaluateNode.bind(this) @@ -87,7 +85,7 @@ export const evaluateArray: ( if (values.some((value) => value === null)) { return null } - return reduce(reducer, start, values) + return values.reduce(reducer, start) }, temporalValues) const baseEvaluation = { @@ -119,27 +117,30 @@ export const defaultNode = (nodeValue: Evaluation) => } as ConstantNode) export const parseObject = (objectShape, value, context) => { - const recurseOne = (key) => (defaultValue) => { - if (value[key] == null && !defaultValue) - throw new Error( - `Il manque une clé '${key}' dans ${JSON.stringify(value)} ` - ) - return value[key] != null ? parse(value[key], context) : defaultValue - } - const transforms = fromPairs( - map((k) => [k, recurseOne(k)], keys(objectShape)) as any + return Object.fromEntries( + Object.entries(objectShape).map(([key, defaultValue]) => { + if (value[key] == null && !defaultValue) { + throw new Error( + `Il manque une clé '${key}' dans ${JSON.stringify(value)} ` + ) + } + + const parsedValue = + value[key] != null ? parse(value[key], context) : defaultValue + return [key, parsedValue] + }) ) - return evolve(transforms as any, objectShape) } export function evaluateObject( effet: (this: Engine, explanations: any) => any ) { return function (node) { - const evaluate = this.evaluateNode.bind(this) - const evaluations: Record = mapObjIndexed( - evaluate as any, - (node as any).explanation + const evaluations = Object.fromEntries( + Object.entries((node as any).explanation).map(([key, value]) => [ + key, + this.evaluateNode(value as any), + ]) ) const temporalExplanations = mapTemporal( Object.fromEntries, diff --git a/publicodes/core/source/format.ts b/publicodes/core/source/format.ts index 8b5f7fe2f..53646c6dd 100644 --- a/publicodes/core/source/format.ts +++ b/publicodes/core/source/format.ts @@ -1,12 +1,6 @@ -import { memoizeWith } from 'ramda' import { Evaluation, Unit } from './AST/types' import { formatUnit, serializeUnit } from './units' -const NumberFormat = memoizeWith( - (...args) => JSON.stringify(args), - Intl.NumberFormat -) - export const numberFormatter = ({ style, maximumFractionDigits = 2, @@ -27,7 +21,7 @@ export const numberFormatter = ({ !Number.isInteger(value) ? 2 : minimumFractionDigits - return NumberFormat(language, { + return Intl.NumberFormat(language, { style, currency: 'EUR', maximumFractionDigits, diff --git a/publicodes/core/source/index.ts b/publicodes/core/source/index.ts index 5a683676d..5f8f2981b 100644 --- a/publicodes/core/source/index.ts +++ b/publicodes/core/source/index.ts @@ -1,5 +1,4 @@ /* eslint-disable @typescript-eslint/ban-types */ -import { compose, mapObjIndexed } from 'ramda' import { ASTNode, EvaluatedNode, NodeKind } from './AST/types' import { evaluationFunctions } from './evaluationFunctions' import { simplifyNodeUnit } from './nodeUnits' @@ -104,24 +103,28 @@ export default class Engine { situation: Partial> = {} ) { this.resetCache() - this.parsedSituation = mapObjIndexed((value, key) => { - if (value && typeof value === 'object' && 'nodeKind' in value) { - return value as ASTNode - } - return compose( - inlineReplacements(this.replacements), - disambiguateReference(this.parsedRules) - )( - parse(value, { - dottedName: `situation [${key}]`, - parsedRules: {}, - options: this.options, - }) - ) - }, situation) + this.parsedSituation = Object.fromEntries( + Object.entries(situation).map(([key, value]) => { + const parsedValue = + value && typeof value === 'object' && 'nodeKind' in value + ? (value as ASTNode) + : this.parse(value, { + dottedName: `situation [${key}]`, + parsedRules: {}, + options: this.options, + }) + return [key, parsedValue] + }) + ) return this } + private parse(...args: Parameters) { + return inlineReplacements(this.replacements)( + disambiguateReference(this.parsedRules)(parse(...args)) + ) + } + evaluate(expression: string | Object): EvaluatedNode { /* TODO @@ -134,16 +137,11 @@ export default class Engine { originalWarn(warning) } const result = this.evaluateNode( - compose( - inlineReplacements(this.replacements), - disambiguateReference(this.parsedRules) - )( - parse(expression, { - dottedName: "evaluation'''", - parsedRules: {}, - options: this.options, - }) - ) + this.parse(expression, { + dottedName: "evaluation'''", + parsedRules: {}, + options: this.options, + }) ) console.warn = originalWarn return result diff --git a/publicodes/core/source/mecanisms/condition-allof.ts b/publicodes/core/source/mecanisms/condition-allof.ts index 47de822b3..ed5d92d8a 100644 --- a/publicodes/core/source/mecanisms/condition-allof.ts +++ b/publicodes/core/source/mecanisms/condition-allof.ts @@ -1,4 +1,3 @@ -import { is } from 'ramda' import { EvaluationFunction } from '..' import { ASTNode } from '../AST/types' import { mergeAllMissing } from '../evaluation' @@ -38,7 +37,7 @@ const evaluate: EvaluationFunction<'toutes ces conditions'> = function (node) { } export const mecanismAllOf = (v, context) => { - if (!is(Array, v)) throw new Error('should be array') + if (!Array.isArray(v)) throw new Error('should be array') const explanation = v.map((node) => parse(node, context)) return { diff --git a/publicodes/core/source/mecanisms/condition-oneof.ts b/publicodes/core/source/mecanisms/condition-oneof.ts index a2970aa13..414835dfa 100644 --- a/publicodes/core/source/mecanisms/condition-oneof.ts +++ b/publicodes/core/source/mecanisms/condition-oneof.ts @@ -1,4 +1,3 @@ -import { is } from 'ramda' import { EvaluationFunction } from '..' import { ASTNode, EvaluatedNode, Evaluation } from '../AST/types' import { mergeMissing } from '../evaluation' @@ -57,7 +56,7 @@ const evaluate: EvaluationFunction<'une de ces conditions'> = function (node) { } export const mecanismOneOf = (v, context) => { - if (!is(Array, v)) throw new Error('should be array') + if (!Array.isArray(v)) throw new Error('should be array') const explanation = v.map((node) => parse(node, context)) return { diff --git a/publicodes/core/source/mecanisms/min.ts b/publicodes/core/source/mecanisms/min.ts index acbf074a7..77b7d78fc 100644 --- a/publicodes/core/source/mecanisms/min.ts +++ b/publicodes/core/source/mecanisms/min.ts @@ -1,4 +1,3 @@ -import { min } from 'ramda' import { ASTNode } from '../AST/types' import { evaluateArray } from '../evaluation' import { registerEvaluationFunction } from '../evaluationFunctions' @@ -17,6 +16,6 @@ export const mecanismMin = (v, context) => { } as MinNode } -const evaluate = evaluateArray<'minimum'>(min, Infinity) +const evaluate = evaluateArray<'minimum'>((a, b) => Math.min(a, b), Infinity) registerEvaluationFunction('minimum', evaluate) diff --git a/publicodes/core/source/mecanisms/operation.ts b/publicodes/core/source/mecanisms/operation.ts index f199ce63f..b73e99c5c 100644 --- a/publicodes/core/source/mecanisms/operation.ts +++ b/publicodes/core/source/mecanisms/operation.ts @@ -1,4 +1,3 @@ -import { equals, fromPairs } from 'ramda' import { EvaluationFunction } from '..' import { ASTNode } from '../AST/types' import { convertToDate } from '../date' @@ -20,8 +19,8 @@ const knownOperations = { '<=': [(a, b) => a <= b, '≤'], '>': [(a, b) => a > b], '>=': [(a, b) => a >= b, '≥'], - '=': [(a, b) => equals(a, b)], - '!=': [(a, b) => !equals(a, b), '≠'], + '=': [(a, b) => a === b], + '!=': [(a, b) => a !== b, '≠'], } as const export type OperationNode = { @@ -123,7 +122,7 @@ const evaluate: EvaluationFunction<'operation'> = function (node) { registerEvaluationFunction('operation', evaluate) -const operationDispatch = fromPairs( +const operationDispatch = Object.fromEntries( Object.entries(knownOperations).map(([k, [f, symbol]]) => [ k, parseOperation(k, symbol), diff --git a/publicodes/core/source/mecanisms/reduction.ts b/publicodes/core/source/mecanisms/reduction.ts index 92f41f3b3..888cab965 100644 --- a/publicodes/core/source/mecanisms/reduction.ts +++ b/publicodes/core/source/mecanisms/reduction.ts @@ -1,4 +1,3 @@ -import { max, min } from 'ramda' import { typeWarning } from '../error' import { defaultNode, evaluateObject, parseObject } from '../evaluation' import { registerEvaluationFunction } from '../evaluationFunctions' @@ -48,12 +47,18 @@ const evaluate = evaluateObject<'allègement'>(function ({ ? 0 : null : serializeUnit(abattement.unit) === '%' - ? max( + ? Math.max( 0, assietteValue - - min(plafond.nodeValue, (abattement.nodeValue / 100) * assietteValue) + Math.min( + plafond.nodeValue, + (abattement.nodeValue / 100) * assietteValue + ) + ) + : Math.max( + 0, + assietteValue - Math.min(plafond.nodeValue, abattement.nodeValue) ) - : max(0, assietteValue - min(plafond.nodeValue, abattement.nodeValue)) : assietteValue return { nodeValue, diff --git a/publicodes/core/source/mecanisms/situation.ts b/publicodes/core/source/mecanisms/situation.ts index ab1e7791e..f18db2fc8 100644 --- a/publicodes/core/source/mecanisms/situation.ts +++ b/publicodes/core/source/mecanisms/situation.ts @@ -1,4 +1,3 @@ -import { isEmpty } from 'ramda' import { ASTNode, EvaluatedNode } from '../AST/types' import { mergeAllMissing } from '../evaluation' import { registerEvaluationFunction } from '../evaluationFunctions' @@ -50,7 +49,7 @@ registerEvaluationFunction(parseSituation.nom, function evaluate(node) { ...node, nodeValue: valeur.nodeValue, missingVariables: - isEmpty(missingVariables) && valeur.nodeValue === null + Object.keys(missingVariables).length === 0 && valeur.nodeValue === null ? { [situationKey]: 1 } : missingVariables, ...(unit !== undefined && { unit }), diff --git a/publicodes/core/source/mecanisms/synchronisation.ts b/publicodes/core/source/mecanisms/synchronisation.ts index 13738ffa4..96820f377 100644 --- a/publicodes/core/source/mecanisms/synchronisation.ts +++ b/publicodes/core/source/mecanisms/synchronisation.ts @@ -1,4 +1,3 @@ -import { path } from 'ramda' import { EvaluationFunction } from '..' import { ASTNode } from '../AST/types' import { registerEvaluationFunction } from '../evaluationFunctions' @@ -15,14 +14,14 @@ export type SynchronisationNode = { const evaluate: EvaluationFunction<'synchronisation'> = function (node: any) { const data = this.evaluateNode(node.explanation.data) const valuePath = node.explanation.chemin.split(' . ') - const nodeValue = - data.nodeValue == null ? null : path(valuePath, data.nodeValue) + const path = (obj) => valuePath.reduce((res, prop) => res[prop], obj) + const nodeValue = data.nodeValue == null ? null : path(data.nodeValue) // If the API gave a non null value, then some of its props may be null (the // API can be composed of multiple API, some failing). Then this prop will be // set to the default value defined in the API's rule const safeNodeValue = nodeValue == null && data.nodeValue != null - ? path(valuePath, data.explanation.defaultValue) + ? path(data.explanation.defaultValue) : nodeValue const missingVariables = { ...data.missingVariables, diff --git a/publicodes/core/source/mecanisms/trancheUtils.ts b/publicodes/core/source/mecanisms/trancheUtils.ts index 82eeab727..6b1575fa1 100644 --- a/publicodes/core/source/mecanisms/trancheUtils.ts +++ b/publicodes/core/source/mecanisms/trancheUtils.ts @@ -1,4 +1,3 @@ -import { evolve } from 'ramda' import { ASTNode, Evaluation } from '../AST/types' import { evaluationError, typeWarning } from '../error' import { mergeAllMissing } from '../evaluation' @@ -18,13 +17,14 @@ export const parseTranches = (tranches, context): TrancheNodes => { } return { ...t, plafond: t.plafond ?? Infinity } }) - .map( - evolve({ - taux: (node) => parse(node, context), - montant: (node) => parse(node, context), - plafond: (node) => parse(node, context), - }) - ) + .map((node) => ({ + ...node, + ...(node.taux !== undefined ? { taux: parse(node.taux, context) } : {}), + ...(node.montant !== undefined + ? { montant: parse(node.montant, context) } + : {}), + plafond: parse(node.plafond, context), + })) } export function evaluatePlafondUntilActiveTranche( diff --git a/publicodes/core/source/mecanisms/variations.ts b/publicodes/core/source/mecanisms/variations.ts index cb281ed43..0e00170e4 100644 --- a/publicodes/core/source/mecanisms/variations.ts +++ b/publicodes/core/source/mecanisms/variations.ts @@ -1,4 +1,3 @@ -import { or } from 'ramda' import { EvaluationFunction } from '..' import { ASTNode, Unit } from '../AST/types' import { typeWarning } from '../error' @@ -101,8 +100,7 @@ const evaluate: EvaluationFunction<'variations'> = function (node) { pureTemporal(evaluatedCondition.nodeValue) ) evaluatedCondition.missingVariables = bonus( - evaluatedCondition.missingVariables, - true + evaluatedCondition.missingVariables ) const currentConditionAlwaysFalse = !sometime( (x) => x !== false, @@ -136,6 +134,7 @@ const evaluate: EvaluationFunction<'variations'> = function (node) { evaluatedConsequence.temporalValue ?? pureTemporal(evaluatedConsequence.nodeValue) ) + const or = (a, b) => a || b return [ liftTemporal2(or, evaluation, currentValue), [ diff --git a/publicodes/core/source/parse.ts b/publicodes/core/source/parse.ts index 7f940c73c..f51b1018a 100644 --- a/publicodes/core/source/parse.ts +++ b/publicodes/core/source/parse.ts @@ -1,5 +1,4 @@ import { Grammar, Parser } from 'nearley' -import { isEmpty } from 'ramda' import { ASTNode } from './AST/types' import { EngineError, syntaxError } from './error' import grammar from './grammar.ne' @@ -109,7 +108,7 @@ Cela vient probablement d'une erreur dans l'indentation ` ) } - if (isEmpty(rawNode)) { + if (keys.length === 0) { return { nodeKind: 'constant', nodeValue: null } } diff --git a/publicodes/core/source/parsePublicodes.ts b/publicodes/core/source/parsePublicodes.ts index 200e2aa64..18d5058b3 100644 --- a/publicodes/core/source/parsePublicodes.ts +++ b/publicodes/core/source/parsePublicodes.ts @@ -1,4 +1,3 @@ -import { partial } from 'ramda' import yaml from 'yaml' import { ParsedRules } from '.' import { transformAST, traverseParsedRules } from './AST' diff --git a/publicodes/core/source/replacement.tsx b/publicodes/core/source/replacement.tsx index 020d7f5ef..2569d5b97 100644 --- a/publicodes/core/source/replacement.tsx +++ b/publicodes/core/source/replacement.tsx @@ -1,4 +1,3 @@ -import { groupBy } from 'ramda' import { transformAST } from './AST' import { ASTNode } from './AST/types' import { InternalError, warning } from './error' @@ -89,15 +88,15 @@ export function parseRendNonApplicable( export function getReplacements( parsedRules: Record ): Record> { - return groupBy( - (r: ReplacementRule) => { + return Object.values(parsedRules) + .flatMap((rule) => rule.replacements) + .reduce((acc, r: ReplacementRule) => { if (!r.replacedReference.dottedName) { throw new InternalError(r) } - return r.replacedReference.dottedName - }, - Object.values(parsedRules).flatMap((rule) => rule.replacements) - ) + const key = r.replacedReference.dottedName + return { ...acc, [key]: [...(acc[key] ?? []), r] } + }, {}) } export function inlineReplacements( diff --git a/publicodes/core/source/rule.ts b/publicodes/core/source/rule.ts index 7f9c488c6..a0592eb34 100644 --- a/publicodes/core/source/rule.ts +++ b/publicodes/core/source/rule.ts @@ -1,4 +1,3 @@ -import { filter, mapObjIndexed, pick } from 'ramda' import { ASTNode, EvaluatedNode } from './AST/types' import { bonus, mergeMissing } from './evaluation' import { registerEvaluationFunction } from './evaluationFunctions' @@ -79,7 +78,9 @@ export default function parseRule( } const ruleValue = { - ...pick(mecanismKeys, rawRule), + ...Object.fromEntries( + Object.entries(rawRule).filter(([key]) => mecanismKeys.includes(key)) + ), ...('formule' in rawRule && { valeur: rawRule.formule }), 'nom dans la situation': dottedName, } @@ -91,22 +92,24 @@ export default function parseRule( valeur: parse(ruleValue, ruleContext), parent: !!parent && parse(parent, context), } - context.parsedRules[dottedName] = filter(Boolean, { + context.parsedRules[dottedName] = { dottedName, replacements: [ ...parseRendNonApplicable(rawRule['rend non applicable'], ruleContext), ...parseReplacements(rawRule.remplace, ruleContext), ], title: ruleTitle, - suggestions: mapObjIndexed( - (node) => parse(node, ruleContext), - rawRule.suggestions ?? {} + suggestions: Object.fromEntries( + Object.entries(rawRule.suggestions ?? {}).map(([name, node]) => [ + name, + parse(node, ruleContext), + ]) ), nodeKind: 'rule', explanation, rawNode: rawRule, virtualRule: !!context.dottedName, - }) as RuleNode + } as RuleNode // We return the parsedReference return parse(rawRule.nom, context) as ReferenceNode diff --git a/publicodes/core/source/ruleUtils.ts b/publicodes/core/source/ruleUtils.ts index 8049e661c..5e8ba61ae 100644 --- a/publicodes/core/source/ruleUtils.ts +++ b/publicodes/core/source/ruleUtils.ts @@ -1,10 +1,9 @@ -import { last, pipe, range, take } from 'ramda' import { syntaxError } from './error' import { RuleNode } from './rule' const splitName = (str: string) => str.split(' . ') -const joinName = (strs) => strs.join(' . ') -export const nameLeaf = pipe(splitName, last) +const joinName = (strs: Array) => strs.join(' . ') +export const nameLeaf = (name: string) => splitName(name).slice(-1)?.[0] export const encodeRuleName = (name) => name ?.replace(/\s\.\s/g, '/') @@ -15,13 +14,12 @@ export const decodeRuleName = (name) => .replace(/\//g, ' . ') .replace(/-/g, ' ') .replace(/\u2011/g, '-') -export function ruleParents( - dottedName: Names -): Array { +export function ruleParents(dottedName: string): Array { const fragments = splitName(dottedName) // dottedName ex. [CDD . événements . rupture] - return range(1, fragments.length) - .map((nbEl) => take(nbEl, fragments)) - .map(joinName) // -> [ [CDD . événements . rupture], [CDD . événements], [CDD + return Array(fragments.length - 1) + .fill(0) + .map((f, i) => fragments.slice(0, i + 1)) + .map(joinName) .reverse() } diff --git a/publicodes/core/source/units.ts b/publicodes/core/source/units.ts index f5c28b349..5989a9f53 100644 --- a/publicodes/core/source/units.ts +++ b/publicodes/core/source/units.ts @@ -1,16 +1,3 @@ -import { - countBy, - equals, - flatten, - isEmpty, - keys, - map, - pipe, - remove, - uniq, - unnest, - without, -} from 'ramda' import { Evaluation, Unit } from './AST/types' export type getUnitKey = (writtenUnit: string) => string @@ -53,8 +40,8 @@ export const serializeUnit = ( const unit = simplify(rawUnit), { numerators = [], denominators = [] } = unit - const n = !isEmpty(numerators) - const d = !isEmpty(denominators) + const n = numerators.length > 0 + const d = denominators.length > 0 const string = !n && !d ? '' @@ -95,8 +82,8 @@ export const inferUnit = ( } if (operator === '*') return simplify({ - numerators: unnest(units.map((u) => u?.numerators ?? [])), - denominators: unnest(units.map((u) => u?.denominators ?? [])), + numerators: units.flatMap((u) => u?.numerators ?? []), + denominators: units.flatMap((u) => u?.denominators ?? []), }) if (operator === '-' || operator === '+') { @@ -106,13 +93,20 @@ export const inferUnit = ( return undefined } +const equals = (a: T, b: T) => { + if (Array.isArray(a) && Array.isArray(b)) { + return a.length === b.length && a.every((_, i) => a[i] === b[i]) + } else { + return a === b + } +} + export const removeOnce = ( element: T, eqFn: (a: T, b: T) => boolean = equals ) => (list: Array): Array => { const index = list.findIndex((e) => eqFn(e, element)) - if (index > -1) return remove(index, 1)(list) - else return list + return list.filter((_, i) => i !== index) } const simplify = ( @@ -262,8 +256,8 @@ export function simplifyUnit(unit: Unit): Unit { return { numerators: ['%'], denominators } } return { - numerators: without(['%'], numerators), - denominators: without(['%'], denominators), + numerators: removePercentages(numerators), + denominators: removePercentages(denominators), } } function simplifyUnitWithValue(unit: Unit, value = 1): [Unit, number] { @@ -273,24 +267,31 @@ function simplifyUnitWithValue(unit: Unit, value = 1): [Unit, number] { return [ simplify( { - numerators: without(['%'], numerators), - denominators: without(['%'], denominators), + numerators: removePercentages(numerators), + denominators: removePercentages(denominators), }, areSameClass ), value ? round(value * factor) : value, ] } + +const removePercentages = (array: Array) => + array.filter((e) => e !== '%') + export function areUnitConvertible(a: Unit | undefined, b: Unit | undefined) { if (a == null || b == null) { return true } - const countByUnitClass = countBy((unit: string) => { - const classIndex = convertibleUnitClasses.findIndex((unitClass) => - unitClass.has(unit) - ) - return classIndex === -1 ? unit : '' + classIndex - }) + + const countByUnitClass = (units: Array) => + units.reduce((counters, unit) => { + const classIndex = convertibleUnitClasses.findIndex((unitClass) => + unitClass.has(unit) + ) + const key = classIndex === -1 ? unit : '' + classIndex + return { ...counters, [key]: 1 + (counters[key] ?? 0) } + }, {}) const [numA, denomA, numB, denomB] = [ a.numerators, @@ -298,12 +299,9 @@ export function areUnitConvertible(a: Unit | undefined, b: Unit | undefined) { b.numerators, b.denominators, ].map(countByUnitClass) - const unitClasses = pipe( - map(keys), - flatten, - uniq - )([numA, denomA, numB, denomB]) - return unitClasses.every( + const uniq = (arr: Array): Array => [...new Set(arr)] + const unitClasses = [numA, denomA, numB, denomB].map(Object.keys).flat() + return uniq(unitClasses).every( (unitClass) => (numA[unitClass] || 0) - (denomA[unitClass] || 0) === (numB[unitClass] || 0) - (denomB[unitClass] || 0) || unitClass === '%'