///
import { type Cell, Default, handler, lift, recipe, str } from "commontools";
interface ContributionItemSeed {
label?: string;
value?: number;
}
interface ContributionGroupSeed {
label?: string;
items?: ContributionItemSeed[];
}
interface NestedComputedPercentagesArgs {
groups: Default;
}
interface ContributionUpdateEvent {
groupIndex?: number;
itemIndex?: number;
value?: number;
itemLabel?: string;
groupLabel?: string;
}
interface SanitizedContributionItem {
label: string;
value: number;
}
interface SanitizedContributionGroup {
label: string;
items: SanitizedContributionItem[];
total: number;
}
const sanitizeNumber = (raw: unknown): number => {
if (typeof raw !== "number" || !Number.isFinite(raw)) {
return 0;
}
return Math.round(raw * 100) / 100;
};
const sanitizeIndex = (raw: unknown): number => {
if (typeof raw !== "number" || !Number.isFinite(raw)) {
return -1;
}
const index = Math.trunc(raw);
return index < 0 ? -1 : index;
};
const sanitizeLabel = (raw: unknown, fallback: string): string => {
if (typeof raw === "string") {
const trimmed = raw.trim();
if (trimmed.length > 0) {
return trimmed;
}
}
return fallback;
};
const sanitizeGroups = (
value: ContributionGroupSeed[] | undefined,
): SanitizedContributionGroup[] => {
if (!Array.isArray(value)) {
return [];
}
const sanitized: SanitizedContributionGroup[] = [];
for (let groupIndex = 0; groupIndex < value.length; groupIndex++) {
const group = value[groupIndex];
const label = sanitizeLabel(group?.label, `Group ${groupIndex + 1}`);
const rawItems = Array.isArray(group?.items) ? group.items : [];
const items: SanitizedContributionItem[] = [];
for (let itemIndex = 0; itemIndex < rawItems.length; itemIndex++) {
const item = rawItems[itemIndex];
const itemLabel = sanitizeLabel(
item?.label,
`Item ${itemIndex + 1}`,
);
items.push({
label: itemLabel,
value: sanitizeNumber(item?.value),
});
}
const total = items.reduce((sum, item) => sum + item.value, 0);
sanitized.push({ label, items, total });
}
return sanitized;
};
const recordContribution = handler(
(
event: ContributionUpdateEvent | undefined,
context: { groups: Cell },
) => {
if (!event) {
return;
}
const groupIndex = sanitizeIndex(event.groupIndex);
if (groupIndex < 0) {
return;
}
const groupsValue = context.groups.get();
if (!Array.isArray(groupsValue) || groupIndex >= groupsValue.length) {
return;
}
const groupCell = context.groups.key(groupIndex) as Cell<
ContributionGroupSeed
>;
if (typeof event.groupLabel === "string") {
groupCell.key("label").set(event.groupLabel.trim());
}
if (event.value === undefined && event.itemLabel === undefined) {
return;
}
const itemsCell = groupCell.key("items") as Cell;
const current = itemsCell.get();
const items = Array.isArray(current) ? [...current] : [];
let itemIndex = sanitizeIndex(event.itemIndex);
const hasValue = event.value !== undefined;
const sanitizedValue = hasValue ? sanitizeNumber(event.value) : undefined;
if (itemIndex < 0) {
if (!hasValue) {
return;
}
items.push({ value: sanitizedValue });
itemIndex = items.length - 1;
} else if (itemIndex >= items.length) {
if (!hasValue) {
return;
}
const fillers = itemIndex - items.length;
for (let index = 0; index < fillers; index++) {
items.push({ value: 0 });
}
items.push({ value: sanitizedValue });
}
const existing = items[itemIndex] ?? {};
const nextLabel = event.itemLabel !== undefined
? event.itemLabel
: existing.label;
items[itemIndex] = {
label: typeof nextLabel === "string" ? nextLabel.trim() : existing.label,
value: hasValue
? sanitizedValue ?? sanitizeNumber(existing?.value)
: sanitizeNumber(existing?.value),
};
itemsCell.set(items);
},
);
const calculatePercent = (value: number, total: number): number => {
if (total <= 0) {
return 0;
}
const ratio = (value / total) * 100;
return Math.round(ratio * 100) / 100;
};
export const counterWithNestedComputedPercentages = recipe<
NestedComputedPercentagesArgs
>(
"Counter With Nested Computed Percentages",
({ groups }) => {
const sanitizedGroups = lift(sanitizeGroups)(groups);
const grandTotal = lift((entries: SanitizedContributionGroup[]) => {
return entries.reduce((sum, group) => sum + group.total, 0);
})(sanitizedGroups);
const groupBreakdown = lift(
(
state: {
groups: SanitizedContributionGroup[];
total: number;
},
) => {
const breakdown: Array<{
label: string;
total: number;
percentOfTotal: number;
items: Array<{
label: string;
value: number;
percentOfGroup: number;
percentOfTotal: number;
}>;
}> = [];
for (const group of state.groups) {
const percentOfTotal = calculatePercent(group.total, state.total);
const items: Array<{
label: string;
value: number;
percentOfGroup: number;
percentOfTotal: number;
}> = [];
for (const item of group.items) {
const percentOfGroup = calculatePercent(item.value, group.total);
const percentItemTotal = calculatePercent(item.value, state.total);
items.push({
label: item.label,
value: item.value,
percentOfGroup,
percentOfTotal: percentItemTotal,
});
}
breakdown.push({
label: group.label,
total: group.total,
percentOfTotal,
items,
});
}
return breakdown;
},
)({ groups: sanitizedGroups, total: grandTotal });
const groupSummaries = lift(
(
state: {
groups: Array<{
label: string;
total: number;
percentOfTotal: number;
}>;
},
) => {
if (state.groups.length === 0) {
return ["no groups"];
}
const summaries: string[] = [];
for (const group of state.groups) {
summaries.push(
`${group.label}: ${group.total} (${group.percentOfTotal}%)`,
);
}
return summaries;
},
)({
groups: groupBreakdown,
});
const summary = lift(
(state: { total: number; parts: string[] }): string => {
const visible = state.parts.length > 0
? state.parts.join(" | ")
: "no groups";
return `Grand total ${state.total}: ${visible}`;
},
)({ total: grandTotal, parts: groupSummaries });
const highlightedGroup = lift(
(
state: {
groups: Array<{
label: string;
percentOfTotal: number;
}>;
},
) => {
if (state.groups.length === 0) {
return "none";
}
const sorted = [...state.groups].sort((left, right) => {
return right.percentOfTotal - left.percentOfTotal;
});
return `${sorted[0].label} is ${sorted[0].percentOfTotal}%`;
},
)({ groups: groupBreakdown });
return {
groups,
sanitizedGroups,
grandTotal,
groupBreakdown,
groupSummaries,
summary,
highlightedGroup,
label: str`${summary} | Top ${highlightedGroup}`,
recordContribution: recordContribution({ groups }),
};
},
);