Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 61 additions & 0 deletions src/utils/investment-share-calculator.utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
export interface InvestmentShareInput {
id: string;
amount: bigint;
}

export interface InvestmentShareResult {
id: string;
percentage: number; // exact integer percentage (e.g., 33 for 33%)
}

/**
* Calculates investment shares using integer arithmetic to ensure the sum
* is exactly 100% and there is no floating point drift.
* Remainders are assigned to the investor with the largest amount.
*/
export function calculateInvestmentShares(
investments: InvestmentShareInput[]
): InvestmentShareResult[] {
if (investments.length === 0) {
return [];
}

const totalAmount = investments.reduce((sum, inv) => sum + inv.amount, 0n);

if (totalAmount === 0n) {
return investments.map((inv) => ({
id: inv.id,
percentage: 0,
}));
}

const results = investments.map((inv) => {
// Integer division to get the floor percentage
const percentage = Number((inv.amount * 100n) / totalAmount);
return {
id: inv.id,
percentage,
};
});

const currentSum = results.reduce((sum, res) => sum + res.percentage, 0);
const remainder = 100 - currentSum;

if (remainder > 0) {
// Find index of the largest amount.
// If there is a tie, the first one encountered (stable) will be chosen.
let maxAmount = -1n;
let maxIndex = 0;

for (let i = 0; i < investments.length; i++) {
if (investments[i].amount > maxAmount) {
maxAmount = investments[i].amount;
maxIndex = i;
}
}

results[maxIndex].percentage += remainder;
}

return results;
}
104 changes: 104 additions & 0 deletions tests/unit/utils/investment-share-calculator.utils.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import {
calculateInvestmentShares,
InvestmentShareInput,
} from "../../../src/utils/investment-share-calculator.utils";

describe("Investment Share Calculator", () => {
it("should calculate exact percentages for a three-way split with remainder assigned to the first investor", () => {
const investments: InvestmentShareInput[] = [
{ id: "inv1", amount: 3000n },
{ id: "inv2", amount: 3000n },
{ id: "inv3", amount: 3000n },
];

const results = calculateInvestmentShares(investments);

expect(results).toHaveLength(3);
expect(results[0].percentage).toBe(34);
expect(results[1].percentage).toBe(33);
expect(results[2].percentage).toBe(33);

const sum = results.reduce((acc, r) => acc + r.percentage, 0);
expect(sum).toBe(100);
});

it("should calculate exact percentages for a 25/75 split", () => {
const investments: InvestmentShareInput[] = [
{ id: "inv1", amount: 2500n },
{ id: "inv2", amount: 7500n },
];

const results = calculateInvestmentShares(investments);

expect(results).toHaveLength(2);
expect(results[0].percentage).toBe(25);
expect(results[1].percentage).toBe(75);

const sum = results.reduce((acc, r) => acc + r.percentage, 0);
expect(sum).toBe(100);
});

it("should return 100% for a single investor", () => {
const investments: InvestmentShareInput[] = [
{ id: "inv1", amount: 5000n },
];

const results = calculateInvestmentShares(investments);

expect(results).toHaveLength(1);
expect(results[0].percentage).toBe(100);

const sum = results.reduce((acc, r) => acc + r.percentage, 0);
expect(sum).toBe(100);
});

it("should assign the remainder to the investor with the largest commitment", () => {
const investments: InvestmentShareInput[] = [
{ id: "inv1", amount: 3000n },
{ id: "inv2", amount: 3001n },
{ id: "inv3", amount: 3000n },
];

const results = calculateInvestmentShares(investments);

expect(results).toHaveLength(3);
// Base is 33% for everyone, sum is 99%, remainder is 1%
// The largest is inv2 (3001). So it gets 34%
expect(results[0].percentage).toBe(33);
expect(results[1].percentage).toBe(34);
expect(results[2].percentage).toBe(33);

const sum = results.reduce((acc, r) => acc + r.percentage, 0);
expect(sum).toBe(100);
});

it("should always sum to exactly 100%", () => {
const investments: InvestmentShareInput[] = [
{ id: "inv1", amount: 1234n },
{ id: "inv2", amount: 5678n },
{ id: "inv3", amount: 9012n },
{ id: "inv4", amount: 3456n },
];

const results = calculateInvestmentShares(investments);

const sum = results.reduce((acc, r) => acc + r.percentage, 0);
expect(sum).toBe(100);
});

it("should return 0 percentages if total amount is 0", () => {
const investments: InvestmentShareInput[] = [
{ id: "inv1", amount: 0n },
{ id: "inv2", amount: 0n },
];

const results = calculateInvestmentShares(investments);
expect(results[0].percentage).toBe(0);
expect(results[1].percentage).toBe(0);
});

it("should return empty array for empty input", () => {
const results = calculateInvestmentShares([]);
expect(results).toEqual([]);
});
});