Skip to content

Commit d8570e1

Browse files
committed
feat(util): get percentage from parts
1 parent 7b3bd76 commit d8570e1

File tree

2 files changed

+85
-0
lines changed

2 files changed

+85
-0
lines changed

Diff for: packages/util/src/Percent.ts

+29
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { OpaqueNumber } from './opaqueTypes';
2+
import sum from 'lodash/sum';
23

34
/**
45
* The Percentage is a relative value that indicates the hundredth parts of any quantity.
@@ -8,3 +9,31 @@ import { OpaqueNumber } from './opaqueTypes';
89
*/
910
export type Percent = OpaqueNumber<'Percent'>;
1011
export const Percent = (value: number): Percent => value as unknown as Percent;
12+
13+
/**
14+
* Calculates the percentages for each part from {@link total}.
15+
* When total is omitted, it is assumed that the total is the sum of the parts.
16+
*
17+
* @param parts array of integer values
18+
* @param total optional param to allow sum(parts) to be smaller than the total
19+
* @returns array of floating point percentages, e.g. [0.1, 0.02, 0.587] is equivalent to 10%, 2%, 58.7%
20+
*/
21+
export const calcPercentages = (parts: number[], total = sum(parts)): Percent[] => {
22+
if (parts.length === 0) {
23+
return [];
24+
}
25+
26+
let partsSum = sum(parts);
27+
28+
if (total < partsSum) total = partsSum;
29+
30+
if (total === 0) {
31+
// it means all parts are 0
32+
// set everything to 1 and continue with the normal algorithm
33+
parts = parts.map(() => 1);
34+
partsSum = sum(parts);
35+
total = partsSum;
36+
}
37+
38+
return parts.map((part) => Percent(part / total));
39+
};

Diff for: packages/util/test/Percent.test.ts

+56
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import { calcPercentages } from '../src/Percent';
2+
3+
describe('Percent', () => {
4+
it('single value is always 100%', () => {
5+
expect(calcPercentages([50])).toEqual([1]);
6+
});
7+
8+
it('whole percentages add up to 100%', () => {
9+
expect(calcPercentages([50, 50])).toEqual([0.5, 0.5]);
10+
});
11+
12+
it('floating point percentages', () => {
13+
expect(calcPercentages([403, 597])).toEqual([0.403, 0.597]);
14+
expect(calcPercentages([249, 249, 502])).toEqual([0.249, 0.249, 0.502]);
15+
expect(calcPercentages([255, 245, 265, 235])).toEqual([0.255, 0.245, 0.265, 0.235]);
16+
});
17+
18+
it('percentages smaller than 1%', () => {
19+
expect(calcPercentages([5, 6, 1000 - 5 - 6])).toEqual([0.005, 0.006, 0.989]);
20+
expect(calcPercentages([0, 6, 1000 - 6])).toEqual([0, 0.006, 0.994]);
21+
});
22+
23+
it('one part is zero, total is implicitly zero, so it takes 100% of the total', () => {
24+
expect(calcPercentages([0])).toEqual([1]);
25+
});
26+
27+
it('multiple parts are zero, total is implicitly zero, percent is distributed evenly', () => {
28+
expect(calcPercentages([0, 0, 0, 0])).toEqual([0.25, 0.25, 0.25, 0.25]);
29+
});
30+
31+
it('total is adjusted to equal at least as much as the sum of the parts', () => {
32+
expect(calcPercentages([80], 0)).toEqual([1]);
33+
});
34+
35+
it('returns empty array if no parts are provided', () => {
36+
expect(calcPercentages([])).toEqual([]);
37+
});
38+
39+
describe('parts sum less than 100%', () => {
40+
it('part are zero but total > zero translates to 0% for each part', () => {
41+
expect(calcPercentages([0, 0], 100)).toEqual([0, 0]);
42+
});
43+
44+
it('single 80% value', () => {
45+
expect(calcPercentages([80], 100)).toEqual([0.8]);
46+
});
47+
48+
it('two whole parts adding up to 80%', () => {
49+
expect(calcPercentages([40, 40], 100)).toEqual([0.4, 0.4]);
50+
});
51+
52+
it('multiple rounded parts adding up to 80%', () => {
53+
expect(calcPercentages([205, 205, 215, 175], 1000)).toEqual([0.205, 0.205, 0.215, 0.175]);
54+
});
55+
});
56+
});

0 commit comments

Comments
 (0)