Skip to content

Commit

Permalink
feat(util): get percentage from parts
Browse files Browse the repository at this point in the history
  • Loading branch information
mirceahasegan committed May 29, 2023
1 parent 2a88f0f commit dd31107
Show file tree
Hide file tree
Showing 2 changed files with 85 additions and 0 deletions.
29 changes: 29 additions & 0 deletions packages/util/src/Percent.ts
@@ -1,4 +1,5 @@
import { OpaqueNumber } from './opaqueTypes';
import sum from 'lodash/sum';

/**
* The Percentage is a relative value that indicates the hundredth parts of any quantity.
Expand All @@ -8,3 +9,31 @@ import { OpaqueNumber } from './opaqueTypes';
*/
export type Percent = OpaqueNumber<'Percent'>;
export const Percent = (value: number): Percent => value as unknown as Percent;

/**
* Calculates the percentages for each part from {@link total}.
* When total is omitted, it is assumed that the total is the sum of the parts.
*
* @param parts array of integer values
* @param total optional param to allow sum(parts) to be smaller than the total
* @returns array of floating point percentages, e.g. [0.1, 0.02, 0.587] is equivalent to 10%, 2%, 58.7%
*/
export const calcPercentages = (parts: number[], total = sum(parts)): Percent[] => {
if (parts.length === 0) {
return [];
}

let partsSum = sum(parts);

if (total < partsSum) total = partsSum;

if (total === 0) {
// it means all parts are 0
// set everything to 1 and continue with the normal algorithm
parts = parts.map(() => 1);
partsSum = sum(parts);
total = partsSum;
}

return parts.map((part) => Percent(part / total));
};
56 changes: 56 additions & 0 deletions packages/util/test/Percent.test.ts
@@ -0,0 +1,56 @@
import { calcPercentages } from '../src/Percent';

describe('Percent', () => {
it('single value is always 100%', () => {
expect(calcPercentages([50])).toEqual([1]);
});

it('whole percentages add up to 100%', () => {
expect(calcPercentages([50, 50])).toEqual([0.5, 0.5]);
});

it('floating point percentages', () => {
expect(calcPercentages([403, 597])).toEqual([0.403, 0.597]);
expect(calcPercentages([249, 249, 502])).toEqual([0.249, 0.249, 0.502]);
expect(calcPercentages([255, 245, 265, 235])).toEqual([0.255, 0.245, 0.265, 0.235]);
});

it('percentages smaller than 1%', () => {
expect(calcPercentages([5, 6, 1000 - 5 - 6])).toEqual([0.005, 0.006, 0.989]);
expect(calcPercentages([0, 6, 1000 - 6])).toEqual([0, 0.006, 0.994]);
});

it('one part is zero, total is implicitly zero, so it takes 100% of the total', () => {
expect(calcPercentages([0])).toEqual([1]);
});

it('multiple parts are zero, total is implicitly zero, percent is distributed evenly', () => {
expect(calcPercentages([0, 0, 0, 0])).toEqual([0.25, 0.25, 0.25, 0.25]);
});

it('total is adjusted to equal at least as much as the sum of the parts', () => {
expect(calcPercentages([80], 0)).toEqual([1]);
});

it('returns empty array if no parts are provided', () => {
expect(calcPercentages([])).toEqual([]);
});

describe('parts sum less than 100%', () => {
it('part are zero but total > zero translates to 0% for each part', () => {
expect(calcPercentages([0, 0], 100)).toEqual([0, 0]);
});

it('single 80% value', () => {
expect(calcPercentages([80], 100)).toEqual([0.8]);
});

it('two whole parts adding up to 80%', () => {
expect(calcPercentages([40, 40], 100)).toEqual([0.4, 0.4]);
});

it('multiple rounded parts adding up to 80%', () => {
expect(calcPercentages([205, 205, 215, 175], 1000)).toEqual([0.205, 0.205, 0.215, 0.175]);
});
});
});

0 comments on commit dd31107

Please sign in to comment.