/// import { Cell, Default, handler, lift, recipe, str } from "commontools"; interface SubtotalGroupSeed { label?: string; values?: number[]; } interface NestedComputedTotalsArgs { groups: Default; } interface SubtotalGroupArgs { label: Default; values: Default; index: Default; } interface AppendGroupValueEvent { index?: number; label?: string; value?: number; } type AppendValueEvent = { value?: number } | number | undefined; type ReplaceValuesEvent = { values?: number[] } | undefined; const sanitizeNumber = (value: unknown): number => { if (typeof value !== "number" || !Number.isFinite(value)) { return 0; } return Math.round(value * 100) / 100; }; const sanitizeValues = (value: unknown): number[] => { if (!Array.isArray(value)) { return []; } const sanitized: number[] = []; for (const entry of value) { sanitized.push(sanitizeNumber(entry)); } return sanitized; }; const sanitizeIndex = (value: unknown): number => { if (typeof value !== "number" || !Number.isFinite(value)) { return -1; } const normalized = Math.trunc(value); return normalized < 0 ? -1 : normalized; }; const resolveLabel = (raw: unknown, index: number): string => { if (typeof raw === "string") { const trimmed = raw.trim(); if (trimmed.length > 0) { return trimmed; } } return `Group ${index + 1}`; }; const appendValueToList = handler( (event: AppendValueEvent, context: { values: Cell }) => { const rawValue = typeof event === "number" ? event : event?.value; if (rawValue === undefined) { return; } const amount = sanitizeNumber(rawValue); context.values.push(amount); }, ); const replaceValuesList = handler( (event: ReplaceValuesEvent, context: { values: Cell }) => { if (!event || !Array.isArray(event.values)) { return; } context.values.set(sanitizeValues(event.values)); }, ); const subtotalGroup = recipe( "Nested Totals Subgroup", ({ label, values, index }) => { const normalizedIndex = lift((value: number | undefined) => { const candidate = sanitizeIndex(value); return candidate >= 0 ? candidate : 0; })(index); const items = lift(sanitizeValues)(values); const subtotal = lift((entries: number[]) => { return entries.reduce((sum, value) => sum + value, 0); })(items); const itemCount = lift((entries: number[]) => entries.length)(items); const resolvedLabel = lift( ( state: { raw: string | undefined; idx: number }, ): string => { return resolveLabel(state.raw, state.idx); }, )({ raw: label, idx: normalizedIndex, }); const subtotalLabel = str`${resolvedLabel} subtotal ${subtotal}`; return { index: normalizedIndex, label: resolvedLabel, items, itemCount, subtotal, subtotalLabel, append: appendValueToList({ values }), replace: replaceValuesList({ values }), }; }, ); const instantiateGroups = lift< { groups: Cell }, SubtotalGroupArgs[] >( ({ groups }) => { const raw = groups.get(); const list = Array.isArray(raw) ? raw : []; const children = []; for (let index = 0; index < list.length; index++) { const groupCell = groups.key(index); const labelCell = groupCell.key("label"); const valuesCell = groupCell.key("values"); const child = subtotalGroup({ label: labelCell, values: valuesCell, index, }).for(index); children.push(child); } return children; }, ); const appendToGroup = handler( ( event: AppendGroupValueEvent | undefined, context: { groups: Cell }, ) => { if (!event || event.value === undefined) { return; } const raw = context.groups.get(); const list = Array.isArray(raw) ? raw : []; if (list.length === 0) { return; } let index = sanitizeIndex(event.index); if (index < 0 && typeof event.label === "string") { const trimmed = event.label.trim(); if (trimmed.length > 0) { index = list.findIndex((entry, idx) => { return resolveLabel(entry?.label, idx) === trimmed; }); } } if (index < 0 || index >= list.length) { return; } const groupCell = context.groups.key(index) as Cell; const valuesCell = groupCell.key("values") as Cell; const current = sanitizeValues(valuesCell.get()); const amount = sanitizeNumber(event.value); valuesCell.set([...current, amount]); }, ); export const counterWithNestedComputedTotals = recipe< NestedComputedTotalsArgs >( "Counter With Nested Computed Totals", ({ groups: groupSeeds }) => { const groups = instantiateGroups({ groups: groupSeeds }); const groupTotals = lift((entries: unknown) => { if (!Array.isArray(entries)) { return []; } return entries.map((entry) => { const subtotal = (entry as { subtotal?: unknown }).subtotal; return typeof subtotal === "number" ? subtotal : sanitizeNumber(subtotal); }); })(groups); const grandTotal = lift((totals: number[]) => { return totals.reduce((sum, value) => sum + value, 0); })(groupTotals); const groupLabels = lift((entries: unknown) => { if (!Array.isArray(entries)) { return []; } return entries.map((entry, index) => { const label = (entry as { label?: unknown }).label; return typeof label === "string" ? label : resolveLabel(label, index); }); })(groups); const groupSummaries = lift( ( state: { labels: string[]; totals: number[] }, ): string[] => { const limit = Math.min(state.labels.length, state.totals.length); if (limit === 0) { return ["none"]; } const summaries: string[] = []; for (let index = 0; index < limit; index++) { const label = state.labels[index]; const value = state.totals[index]; summaries.push(`${label}: ${value}`); } return summaries; }, )({ labels: groupLabels, totals: groupTotals }); const summary = lift((state: { parts: string[]; total: number }) => { const visible = state.parts.length > 0 ? state.parts.join(" | ") : "none"; return `${visible} => total ${state.total}`; })({ parts: groupSummaries, total: grandTotal }); const groupCount = lift((entries: unknown) => { return Array.isArray(entries) ? entries.length : 0; })(groups); const totalItems = lift((entries: { itemCount: number }[]) => { if (!Array.isArray(entries)) { return 0; } return entries.reduce((sum, entry) => { const count = entry.itemCount; return typeof count === "number" ? sum + count : sum; }, 0); })(groups); return { seeds: groupSeeds, groups, groupTotals, groupLabels, groupSummaries, groupCount, totalItems, grandTotal, summary, appendToGroup: appendToGroup({ groups: groupSeeds }), }; }, );