Refactor calculator module helpers
All checks were successful
Deploy Sokko G / deploy (push) Successful in 15s
All checks were successful
Deploy Sokko G / deploy (push) Successful in 15s
This commit is contained in:
parent
e512eef232
commit
61176ba9b7
5 changed files with 211 additions and 170 deletions
|
|
@ -146,6 +146,9 @@ test("toolbox module registry and modules expose expected behavior", async () =>
|
|||
const combosBundle = `${combosModule}\n${combosEditor}\n${combosParts}\n${combosList}`;
|
||||
const combosUtils = await readFile("website/src/features/toolboxes/modules/combosUtils.js", "utf8");
|
||||
const calculatorModule = await readFile("website/src/features/toolboxes/modules/CalculatorModule.jsx", "utf8");
|
||||
const calculatorEntries = await readFile("website/src/features/toolboxes/modules/CalculatorEntries.jsx", "utf8");
|
||||
const calculatorUtils = await readFile("website/src/features/toolboxes/modules/calculatorUtils.js", "utf8");
|
||||
const calculatorBundle = `${calculatorModule}\n${calculatorEntries}\n${calculatorUtils}`;
|
||||
const tableModule = await readFile("website/src/features/toolboxes/modules/TableModule.jsx", "utf8");
|
||||
const tableGrid = await readFile("website/src/features/toolboxes/modules/TableGrid.jsx", "utf8");
|
||||
const tableGridParts = await readFile("website/src/features/toolboxes/modules/TableGridParts.jsx", "utf8");
|
||||
|
|
@ -274,12 +277,12 @@ test("toolbox module registry and modules expose expected behavior", async () =>
|
|||
assert.doesNotMatch(combosModule, /canMoveItem:/);
|
||||
assert.doesNotMatch(combosModule, /canMoveGroup:/);
|
||||
assert.match(calculatorModule, /export function CalculatorModule/);
|
||||
assert.match(calculatorModule, /calculateExpression/);
|
||||
assert.match(calculatorUtils, /export function calculateExpression/);
|
||||
assert.match(calculatorModule, /activeParentId/);
|
||||
assert.match(calculatorModule, /parentId/);
|
||||
assert.match(calculatorModule, /MixTagsInput/);
|
||||
assert.match(calculatorModule, /useGroupedReorder/);
|
||||
assert.match(calculatorModule, /calculator-drag-handle/);
|
||||
assert.match(calculatorEntries, /calculator-drag-handle/);
|
||||
assert.match(calculatorModule, /scrollResults/);
|
||||
assert.match(calculatorModule, /tool-split-scroll-toggle/);
|
||||
assert.match(calculatorModule, /copyChecklistImport/);
|
||||
|
|
@ -293,11 +296,11 @@ test("toolbox module registry and modules expose expected behavior", async () =>
|
|||
assert.match(calculatorModule, /setExpression\(BASE_TOKEN\)/);
|
||||
assert.match(calculatorModule, /editingEntryId/);
|
||||
assert.match(calculatorModule, /updateButton/);
|
||||
assert.match(calculatorModule, /onAddChild/);
|
||||
assert.match(calculatorModule, /entry\.label \|\| formatResult\(entry\.value\)/);
|
||||
assert.match(calculatorEntries, /onAddChild/);
|
||||
assert.match(calculatorEntries, /entry\.label \|\| formatResult\(entry\.value\)/);
|
||||
assert.match(calculatorModule, /ResizeObserver/);
|
||||
assert.match(calculatorModule, /--calculator-scroll-height/);
|
||||
assert.match(calculatorModule, /CalculatorEntries/);
|
||||
assert.match(calculatorBundle, /CalculatorEntries/);
|
||||
assert.match(tableModule, /export function TableModule/);
|
||||
assert.match(tableBundle, /Icon name="add-row"/);
|
||||
assert.match(tableBundle, /Icon name="add-column"/);
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import { exportModuleText, importModuleText, parseColonImportLines } from "../we
|
|||
import { buildInsertedMixTagValue, normalizeMixTagSpacing, normalizeMixTagText, removeMixTagAtOffset, tokenizeMixTagValue } from "../website/src/utils/mixTagsInputUtils.js";
|
||||
import { getEffectiveGoalTreeProgressMode, getVisibleGoalTreeEdges } from "../website/src/features/toolboxes/modules/goalTreeDisplay.js";
|
||||
import { calculateGoalTreeFormula, evaluateGoalTreeNodes } from "../website/src/features/toolboxes/modules/goalTreeUtils.js";
|
||||
import { calculateExpression, createStoredFormula, evaluateCalculatorEntries, getEntriesInTreeOrder } from "../website/src/features/toolboxes/modules/calculatorUtils.js";
|
||||
import { createDefaultDraft, getDateSpanDays, getDraftSelection, normalizeDraftAlertOffsets, normalizeDraftMonthlyRepeatBy } from "../website/src/features/toolboxes/modules/calendarDraft.js";
|
||||
import { getMonthOccurrenceSegments, getWeekGridLayout, getWeekMinutePosition, layoutConcurrentOccurrences } from "../website/src/features/toolboxes/modules/calendarLayout.js";
|
||||
import { appendChecklistItems, moveChecklistItem, moveChecklistItemToCategory, renameChecklistSection } from "../website/src/features/toolboxes/modules/checklistUtils.js";
|
||||
|
|
@ -154,6 +155,21 @@ test("text exchange imports table TSV and calculator hierarchy", () => {
|
|||
assert.equal(calculatorImport.data.entries[1].formula, "base*5");
|
||||
});
|
||||
|
||||
test("calculator helpers evaluate formulas and preserve tree order", () => {
|
||||
const entries = evaluateCalculatorEntries([
|
||||
{ id: "root", label: "Root", value: 10, formula: "10" },
|
||||
{ id: "child", parentId: "root", label: "Child", value: 0, formula: "base*3" },
|
||||
{ id: "sibling", label: "Sibling", value: 2, formula: "1+1" }
|
||||
]);
|
||||
assert.equal(calculateExpression("2 + 3 * 4"), 14);
|
||||
assert.equal(calculateExpression("base / 2", 10), 5);
|
||||
assert.equal(calculateExpression("window.alert(1)"), null);
|
||||
assert.equal(createStoredFormula("+5", 10), "base+5");
|
||||
assert.equal(createStoredFormula("10*2", 10), "base*2");
|
||||
assert.deepEqual(entries.map((entry) => [entry.id, entry.value]), [["root", 10], ["child", 30], ["sibling", 2]]);
|
||||
assert.deepEqual(getEntriesInTreeOrder(entries).map((entry) => entry.id), ["root", "child", "sibling"]);
|
||||
});
|
||||
|
||||
test("calendar layout keeps week compaction and overlapping lanes deterministic", () => {
|
||||
const base = new Date(2026, 0, 5, 8, 0, 0, 0).getTime();
|
||||
const occurrences = [
|
||||
|
|
|
|||
78
website/src/features/toolboxes/modules/CalculatorEntries.jsx
Normal file
78
website/src/features/toolboxes/modules/CalculatorEntries.jsx
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
// Rôle : rend l'arbre des résultats enregistrés du calculateur.
|
||||
import { Icon } from "../../../components/Icon.jsx";
|
||||
import {
|
||||
DISPLAY_NUMBER_FORMATTER,
|
||||
formatDisplayResult,
|
||||
formatResult,
|
||||
getChildren,
|
||||
getEntryResultLengthClass
|
||||
} from "./calculatorUtils.js";
|
||||
|
||||
export function FormattedCalculatorNumber({ value }) {
|
||||
if (!Number.isFinite(value)) return "-";
|
||||
let integerGroupIndex = 0;
|
||||
const normalized = Number.parseFloat(value.toFixed(6));
|
||||
return DISPLAY_NUMBER_FORMATTER.formatToParts(normalized).map((part, index) => {
|
||||
if (part.type === "integer") {
|
||||
const className = `calculator-number-group ${integerGroupIndex % 2 ? "is-muted" : "is-strong"}`;
|
||||
integerGroupIndex += 1;
|
||||
return <span className={className} key={`${part.type}-${index}`}>{part.value}</span>;
|
||||
}
|
||||
if (part.type === "group") return <span className="calculator-number-separator" key={`${part.type}-${index}`}>{part.value}</span>;
|
||||
return <span key={`${part.type}-${index}`}>{part.value}</span>;
|
||||
});
|
||||
}
|
||||
|
||||
export function CalculatorEntries({ entries, parentId, getScopedParentId, activeEntryId, textContent, reorder, onEdit, onAddChild, onDelete }) {
|
||||
const children = getChildren(entries, parentId);
|
||||
if (!children.length) return null;
|
||||
|
||||
return (
|
||||
<ul className="tool-split-entry-list calculator-entry-list">
|
||||
{children.map((entry) => {
|
||||
const className = [
|
||||
entry.id === activeEntryId ? "active" : "",
|
||||
reorder.isItemDragging(entry.id) ? "is-dragging" : "",
|
||||
reorder.isItemDropTarget(entry.id) ? "is-drop-target" : "",
|
||||
reorder.getDropPlacement("item", entry.id, parentId) === "after" ? "drop-after" : ""
|
||||
].filter(Boolean).join(" ");
|
||||
|
||||
return (
|
||||
<li className={className} key={entry.id} {...reorder.getItemProps({ itemId: entry.id, parentId: getScopedParentId(parentId) })}>
|
||||
<div className="tool-split-entry calculator-entry has-drag-handle">
|
||||
<button
|
||||
className="calculator-drag-handle"
|
||||
type="button"
|
||||
onPointerDown={(event) => reorder.itemReorder.startDrag(event, entry.id)}
|
||||
aria-label={`${textContent.reorderTitle || "Déplacer"} ${entry.label || formatResult(entry.value)}`}
|
||||
title={textContent.reorderTitle || "Déplacer"}
|
||||
>
|
||||
<Icon name="drag" />
|
||||
</button>
|
||||
<button className="tool-split-entry-summary" type="button" onClick={() => onEdit(entry)} title={textContent.editTitle || "Modifier"}>
|
||||
<span>
|
||||
<strong className={getEntryResultLengthClass(entry.value)} aria-label={formatDisplayResult(entry.value)}>
|
||||
<FormattedCalculatorNumber value={entry.value} />
|
||||
</strong>
|
||||
{entry.label && <em>{entry.label}</em>}
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => onAddChild(entry)}
|
||||
aria-label={`${textContent.addChildTitle || "Ajouter un calcul enfant"} ${entry.label || formatResult(entry.value)}`}
|
||||
title={textContent.addChildTitle || "Ajouter un calcul enfant"}
|
||||
>
|
||||
<Icon name="add" />
|
||||
</button>
|
||||
<button type="button" className="danger" onClick={() => onDelete(entry.id)} aria-label={`${textContent.deleteTitle || "Supprimer"} ${entry.label}`} title={textContent.deleteTitle || "Supprimer"}>
|
||||
<Icon name="trash" />
|
||||
</button>
|
||||
</div>
|
||||
<CalculatorEntries entries={entries} parentId={entry.id} getScopedParentId={getScopedParentId} activeEntryId={activeEntryId} textContent={textContent} reorder={reorder} onEdit={onEdit} onAddChild={onAddChild} onDelete={onDelete} />
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
);
|
||||
}
|
||||
|
|
@ -3,118 +3,20 @@ import { useLayoutEffect, useMemo, useRef, useState } from "react";
|
|||
import { Icon } from "../../../components/Icon.jsx";
|
||||
import { MixTagsInput } from "../../../components/MixTagsInput.jsx";
|
||||
import { moveItem, useGroupedReorder } from "../../../hooks/useGroupedReorder.js";
|
||||
import { CalculatorEntries, FormattedCalculatorNumber } from "./CalculatorEntries.jsx";
|
||||
import { TextExchangeActions } from "./TextExchangeActions.jsx";
|
||||
|
||||
const DISPLAY_NUMBER_FORMATTER = new Intl.NumberFormat("fr-FR", { maximumFractionDigits: 6 });
|
||||
const BASE_TOKEN = "base";
|
||||
|
||||
function calculateExpression(expression, baseValue = null) {
|
||||
const normalized = String(expression || "").replaceAll(",", ".").trim();
|
||||
if (!normalized) return null;
|
||||
const withBase = normalized.replace(/\bbase\b/gi, () => Number.isFinite(baseValue) ? `(${formatResult(baseValue)})` : "NaN");
|
||||
if (!/^[\d\s+\-*/().]+$/.test(withBase)) return null;
|
||||
|
||||
try {
|
||||
const value = Function(`"use strict"; return (${withBase})`)();
|
||||
return Number.isFinite(value) ? value : null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function formatResult(value) {
|
||||
if (!Number.isFinite(value)) return "";
|
||||
return Number.parseFloat(value.toFixed(6)).toString();
|
||||
}
|
||||
|
||||
function formatDisplayResult(value) {
|
||||
if (!Number.isFinite(value)) return "";
|
||||
return DISPLAY_NUMBER_FORMATTER.format(Number.parseFloat(value.toFixed(6)));
|
||||
}
|
||||
|
||||
function getResultLengthClass(value) {
|
||||
const length = formatResult(value).replace(/[^\d]/g, "").length;
|
||||
if (length >= 13) return "is-compact";
|
||||
if (length >= 9) return "is-long";
|
||||
return "";
|
||||
}
|
||||
|
||||
function getEntryResultLengthClass(value) {
|
||||
const length = formatResult(value).replace(/[^\d]/g, "").length;
|
||||
if (length >= 21) return "is-compact";
|
||||
if (length >= 18) return "is-long";
|
||||
return "";
|
||||
}
|
||||
|
||||
function getChildren(entries, parentId) {
|
||||
return entries.filter((entry) => (entry.parentId || "") === parentId);
|
||||
}
|
||||
|
||||
function getDescendantIds(entries, parentId) {
|
||||
const children = getChildren(entries, parentId);
|
||||
return children.flatMap((child) => [child.id, ...getDescendantIds(entries, child.id)]);
|
||||
}
|
||||
|
||||
function getEntriesInTreeOrder(entries, parentId = "") {
|
||||
return getChildren(entries, parentId).flatMap((entry) => [entry, ...getEntriesInTreeOrder(entries, entry.id)]);
|
||||
}
|
||||
|
||||
function getEntryFormula(entry) {
|
||||
return String(entry?.formula || "").trim();
|
||||
}
|
||||
|
||||
function getEditableFormula(entry) {
|
||||
return getEntryFormula(entry) || formatResult(entry.value);
|
||||
}
|
||||
|
||||
function escapeRegExp(value) {
|
||||
return String(value).replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
||||
}
|
||||
|
||||
function createStoredFormula(expression, parentValue) {
|
||||
const normalized = String(expression || "").replaceAll(",", ".").trim();
|
||||
if (!Number.isFinite(parentValue)) return normalized;
|
||||
const parentText = formatResult(parentValue);
|
||||
if (/\bbase\b/i.test(normalized)) return normalized;
|
||||
const withBase = normalized.replace(new RegExp(`(^|[^\\d.])${escapeRegExp(parentText)}(?=$|[^\\d.])`, "g"), `$1${BASE_TOKEN}`);
|
||||
if (withBase !== normalized) return withBase;
|
||||
if (/^[+\-*/]/.test(normalized)) return `${BASE_TOKEN}${normalized}`;
|
||||
return normalized;
|
||||
}
|
||||
|
||||
function evaluateCalculatorEntries(entries) {
|
||||
const byParent = new Map();
|
||||
entries.forEach((entry) => {
|
||||
const parentId = entry.parentId || "";
|
||||
byParent.set(parentId, [...(byParent.get(parentId) || []), entry]);
|
||||
});
|
||||
|
||||
function visit(parentId = "", parentValue = null) {
|
||||
return (byParent.get(parentId) || []).flatMap((entry) => {
|
||||
const formula = getEntryFormula(entry);
|
||||
const calculatedValue = formula ? calculateExpression(formula, parentValue) : entry.value;
|
||||
const nextEntry = { ...entry, value: calculatedValue ?? entry.value };
|
||||
return [nextEntry, ...visit(entry.id, nextEntry.value)];
|
||||
});
|
||||
}
|
||||
|
||||
return visit();
|
||||
}
|
||||
|
||||
function FormattedCalculatorNumber({ value }) {
|
||||
if (!Number.isFinite(value)) return "-";
|
||||
let integerGroupIndex = 0;
|
||||
const normalized = Number.parseFloat(value.toFixed(6));
|
||||
return DISPLAY_NUMBER_FORMATTER.formatToParts(normalized).map((part, index) => {
|
||||
if (part.type === "integer") {
|
||||
const className = `calculator-number-group ${integerGroupIndex % 2 ? "is-muted" : "is-strong"}`;
|
||||
integerGroupIndex += 1;
|
||||
return <span className={className} key={`${part.type}-${index}`}>{part.value}</span>;
|
||||
}
|
||||
if (part.type === "group") return <span className="calculator-number-separator" key={`${part.type}-${index}`}>{part.value}</span>;
|
||||
return <span key={`${part.type}-${index}`}>{part.value}</span>;
|
||||
});
|
||||
}
|
||||
import {
|
||||
BASE_TOKEN,
|
||||
calculateExpression,
|
||||
createStoredFormula,
|
||||
evaluateCalculatorEntries,
|
||||
formatDisplayResult,
|
||||
formatResult,
|
||||
getDescendantIds,
|
||||
getEditableFormula,
|
||||
getEntriesInTreeOrder,
|
||||
getResultLengthClass
|
||||
} from "./calculatorUtils.js";
|
||||
|
||||
function focusExpressionInputAtEnd(element) {
|
||||
if (!element) return;
|
||||
|
|
@ -393,57 +295,3 @@ export function CalculatorModule({ toolboxId, moduleId, context, textExchangeOpe
|
|||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function CalculatorEntries({ entries, parentId, getScopedParentId, activeEntryId, textContent, reorder, onEdit, onAddChild, onDelete }) {
|
||||
const children = getChildren(entries, parentId);
|
||||
if (!children.length) return null;
|
||||
|
||||
return (
|
||||
<ul className="tool-split-entry-list calculator-entry-list">
|
||||
{children.map((entry) => {
|
||||
const className = [
|
||||
entry.id === activeEntryId ? "active" : "",
|
||||
reorder.isItemDragging(entry.id) ? "is-dragging" : "",
|
||||
reorder.isItemDropTarget(entry.id) ? "is-drop-target" : "",
|
||||
reorder.getDropPlacement("item", entry.id, parentId) === "after" ? "drop-after" : ""
|
||||
].filter(Boolean).join(" ");
|
||||
|
||||
return (
|
||||
<li className={className} key={entry.id} {...reorder.getItemProps({ itemId: entry.id, parentId: getScopedParentId(parentId) })}>
|
||||
<div className="tool-split-entry calculator-entry has-drag-handle">
|
||||
<button
|
||||
className="calculator-drag-handle"
|
||||
type="button"
|
||||
onPointerDown={(event) => reorder.itemReorder.startDrag(event, entry.id)}
|
||||
aria-label={`${textContent.reorderTitle || "Déplacer"} ${entry.label || formatResult(entry.value)}`}
|
||||
title={textContent.reorderTitle || "Déplacer"}
|
||||
>
|
||||
<Icon name="drag" />
|
||||
</button>
|
||||
<button className="tool-split-entry-summary" type="button" onClick={() => onEdit(entry)} title={textContent.editTitle || "Modifier"}>
|
||||
<span>
|
||||
<strong className={getEntryResultLengthClass(entry.value)} aria-label={formatDisplayResult(entry.value)}>
|
||||
<FormattedCalculatorNumber value={entry.value} />
|
||||
</strong>
|
||||
{entry.label && <em>{entry.label}</em>}
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => onAddChild(entry)}
|
||||
aria-label={`${textContent.addChildTitle || "Ajouter un calcul enfant"} ${entry.label || formatResult(entry.value)}`}
|
||||
title={textContent.addChildTitle || "Ajouter un calcul enfant"}
|
||||
>
|
||||
<Icon name="add" />
|
||||
</button>
|
||||
<button type="button" className="danger" onClick={() => onDelete(entry.id)} aria-label={`${textContent.deleteTitle || "Supprimer"} ${entry.label}`} title={textContent.deleteTitle || "Supprimer"}>
|
||||
<Icon name="trash" />
|
||||
</button>
|
||||
</div>
|
||||
<CalculatorEntries entries={entries} parentId={entry.id} getScopedParentId={getScopedParentId} activeEntryId={activeEntryId} textContent={textContent} reorder={reorder} onEdit={onEdit} onAddChild={onAddChild} onDelete={onDelete} />
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
96
website/src/features/toolboxes/modules/calculatorUtils.js
Normal file
96
website/src/features/toolboxes/modules/calculatorUtils.js
Normal file
|
|
@ -0,0 +1,96 @@
|
|||
// Rôle : regroupe les helpers purs du calculateur et de son arbre de résultats.
|
||||
export const DISPLAY_NUMBER_FORMATTER = new Intl.NumberFormat("fr-FR", { maximumFractionDigits: 6 });
|
||||
export const BASE_TOKEN = "base";
|
||||
|
||||
export function calculateExpression(expression, baseValue = null) {
|
||||
const normalized = String(expression || "").replaceAll(",", ".").trim();
|
||||
if (!normalized) return null;
|
||||
const withBase = normalized.replace(/\bbase\b/gi, () => Number.isFinite(baseValue) ? `(${formatResult(baseValue)})` : "NaN");
|
||||
if (!/^[\d\s+\-*/().]+$/.test(withBase)) return null;
|
||||
|
||||
try {
|
||||
const value = Function(`"use strict"; return (${withBase})`)();
|
||||
return Number.isFinite(value) ? value : null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export function formatResult(value) {
|
||||
if (!Number.isFinite(value)) return "";
|
||||
return Number.parseFloat(value.toFixed(6)).toString();
|
||||
}
|
||||
|
||||
export function formatDisplayResult(value) {
|
||||
if (!Number.isFinite(value)) return "";
|
||||
return DISPLAY_NUMBER_FORMATTER.format(Number.parseFloat(value.toFixed(6)));
|
||||
}
|
||||
|
||||
export function getResultLengthClass(value) {
|
||||
const length = formatResult(value).replace(/[^\d]/g, "").length;
|
||||
if (length >= 13) return "is-compact";
|
||||
if (length >= 9) return "is-long";
|
||||
return "";
|
||||
}
|
||||
|
||||
export function getEntryResultLengthClass(value) {
|
||||
const length = formatResult(value).replace(/[^\d]/g, "").length;
|
||||
if (length >= 21) return "is-compact";
|
||||
if (length >= 18) return "is-long";
|
||||
return "";
|
||||
}
|
||||
|
||||
export function getChildren(entries, parentId) {
|
||||
return entries.filter((entry) => (entry.parentId || "") === parentId);
|
||||
}
|
||||
|
||||
export function getDescendantIds(entries, parentId) {
|
||||
const children = getChildren(entries, parentId);
|
||||
return children.flatMap((child) => [child.id, ...getDescendantIds(entries, child.id)]);
|
||||
}
|
||||
|
||||
export function getEntriesInTreeOrder(entries, parentId = "") {
|
||||
return getChildren(entries, parentId).flatMap((entry) => [entry, ...getEntriesInTreeOrder(entries, entry.id)]);
|
||||
}
|
||||
|
||||
export function getEntryFormula(entry) {
|
||||
return String(entry?.formula || "").trim();
|
||||
}
|
||||
|
||||
export function getEditableFormula(entry) {
|
||||
return getEntryFormula(entry) || formatResult(entry.value);
|
||||
}
|
||||
|
||||
export function createStoredFormula(expression, parentValue) {
|
||||
const normalized = String(expression || "").replaceAll(",", ".").trim();
|
||||
if (!Number.isFinite(parentValue)) return normalized;
|
||||
const parentText = formatResult(parentValue);
|
||||
if (/\bbase\b/i.test(normalized)) return normalized;
|
||||
const withBase = normalized.replace(new RegExp(`(^|[^\\d.])${escapeRegExp(parentText)}(?=$|[^\\d.])`, "g"), `$1${BASE_TOKEN}`);
|
||||
if (withBase !== normalized) return withBase;
|
||||
if (/^[+\-*/]/.test(normalized)) return `${BASE_TOKEN}${normalized}`;
|
||||
return normalized;
|
||||
}
|
||||
|
||||
export function evaluateCalculatorEntries(entries) {
|
||||
const byParent = new Map();
|
||||
entries.forEach((entry) => {
|
||||
const parentId = entry.parentId || "";
|
||||
byParent.set(parentId, [...(byParent.get(parentId) || []), entry]);
|
||||
});
|
||||
|
||||
function visit(parentId = "", parentValue = null) {
|
||||
return (byParent.get(parentId) || []).flatMap((entry) => {
|
||||
const formula = getEntryFormula(entry);
|
||||
const calculatedValue = formula ? calculateExpression(formula, parentValue) : entry.value;
|
||||
const nextEntry = { ...entry, value: calculatedValue ?? entry.value };
|
||||
return [nextEntry, ...visit(entry.id, nextEntry.value)];
|
||||
});
|
||||
}
|
||||
|
||||
return visit();
|
||||
}
|
||||
|
||||
function escapeRegExp(value) {
|
||||
return String(value).replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue