Skip to content

Commit e64fcdb

Browse files
authored
fix(library/math): guards against fractional operands in greatestCommonDivisor (#541)
1 parent 562a19e commit e64fcdb

File tree

5 files changed

+143
-8
lines changed

5 files changed

+143
-8
lines changed

src/library/math/operator/constructive/greatestCommonDivisor.test.ts

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -44,9 +44,9 @@ const symmetricPairCombos = <
4444

4545
describe(greatestCommonDivisor, () => {
4646
describe('the sad outcomes', () => {
47-
describe('when delegated', () => {
48-
const primeA = 2;
47+
const primeA = 2;
4948

49+
describe('when delegated', () => {
5050
const combosOfInoperableNumbers = symmetricPairCombos({
5151
of : numberTaxonomy.undefined,
5252
filler: primeA,
@@ -72,7 +72,16 @@ describe(greatestCommonDivisor, () => {
7272

7373
describe('when generated', () => {
7474
describe('due to fractional operands', () => {
75-
it.todo('should reject them');
75+
const combosOfFractionalNumbers = symmetricPairCombos({
76+
of : numberTaxonomy.fractional.transcendental,
77+
filler: primeA,
78+
});
79+
80+
it.each(combosOfFractionalNumbers)('should reject them', (...$0) => {
81+
expect.assertions(1);
82+
83+
expect(() => greatestCommonDivisor(...$0)).toThrow(Error);
84+
});
7685

7786
it.todo('should safely propagate the error');
7887

src/library/math/operator/constructive/greatestCommonDivisor.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,20 @@
11
import Attempt from '@/library/Attempt';
22

33
import {
4-
isFinite,
4+
isWhole,
55
} from '../predicate';
66

77
const greatestCommonDivisor = (
88
leftHandOperand: number,
99
rightHandOperand: number,
1010
): number => {
1111
if (
12-
!isFinite(leftHandOperand)
13-
) return Attempt.abandon('Left operand must be finite.');
12+
!isWhole(leftHandOperand)
13+
) return Attempt.abandon('Left operand must be whole.');
1414

1515
if (
16-
!isFinite(rightHandOperand)
17-
) return Attempt.abandon('Right operand must be finite.');
16+
!isWhole(rightHandOperand)
17+
) return Attempt.abandon('Right operand must be whole.');
1818

1919
let nextDividend = leftHandOperand;
2020
let previousRemainder = rightHandOperand;
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
11
export * from './isOperable';
22

33
export * from './isFinite';
4+
5+
export * from './isWhole';
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
import {
2+
describe,
3+
it,
4+
expect,
5+
} from 'vitest';
6+
7+
import Attempt from '@/library/Attempt';
8+
9+
import {
10+
numberTaxonomy,
11+
} from '../../numberTaxonomy';
12+
13+
import {
14+
isWhole,
15+
} from './isWhole';
16+
17+
describe(isWhole, () => {
18+
describe('the sad outcomes', () => {
19+
describe('when delegated', () => {
20+
it('should reject inoperable operands', () => {
21+
expect.assertions(1);
22+
23+
expect(() => isWhole(numberTaxonomy.undefined)).toThrow(Error);
24+
});
25+
});
26+
27+
describe('when generated', () => {
28+
const infiniteNumbers = [
29+
numberTaxonomy.infinite.positive,
30+
numberTaxonomy.infinite.negative,
31+
];
32+
33+
describe.each(infiniteNumbers)('due to infinite operands', (eachInfiniteNumber) => {
34+
it('should reject them', () => {
35+
expect.assertions(1);
36+
37+
expect(() => isWhole(eachInfiniteNumber)).toThrow(Error);
38+
});
39+
40+
it('should safely propagate the error', () => {
41+
expect.assertions(1);
42+
43+
expect(() => isWhole(eachInfiniteNumber)).toThrow(Attempt.NonActionableError);
44+
});
45+
46+
it('should communicate clearly with developers', () => {
47+
expect.assertions(1);
48+
49+
expect(() => isWhole(eachInfiniteNumber)).toThrow('Operand must be finite.');
50+
});
51+
});
52+
});
53+
});
54+
55+
describe('the happy outcomes', () => {
56+
const wholeNumbers = [
57+
numberTaxonomy.origin,
58+
numberTaxonomy.signed.negative,
59+
numberTaxonomy.signed.positive,
60+
numberTaxonomy.runtime.max,
61+
];
62+
63+
const fractionalNumbers = [
64+
numberTaxonomy.runtime.min,
65+
numberTaxonomy.fractional.rational,
66+
numberTaxonomy.fractional.irrational,
67+
numberTaxonomy.fractional.transcendental,
68+
];
69+
70+
const finiteNumbers = [
71+
...wholeNumbers,
72+
...fractionalNumbers,
73+
];
74+
75+
it.each(finiteNumbers)('should accept valid operands', (eachFiniteNumber) => {
76+
expect.assertions(1);
77+
78+
expect(() => isWhole(eachFiniteNumber)).not.toThrow();
79+
});
80+
81+
describe.each(wholeNumbers)('when given whole operands', (eachWholeNumber) => {
82+
it('should detect them when fractional portions are omitted', () => {
83+
expect.assertions(1);
84+
85+
expect(isWhole(eachWholeNumber)).toBe(true);
86+
});
87+
88+
it('should detect them when fractional portions are present but zero', () => {
89+
expect.assertions(1);
90+
91+
expect(isWhole((eachWholeNumber + 0.0))).toBe(true);
92+
});
93+
});
94+
95+
it.each(fractionalNumbers)('should detect fractional operands', (eachFractionalNumber) => {
96+
expect.assertions(1);
97+
98+
expect(isWhole(eachFractionalNumber)).toBe(false);
99+
});
100+
});
101+
});
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import Attempt from '@/library/Attempt';
2+
3+
import {
4+
isFinite,
5+
} from './isFinite';
6+
7+
const isWhole = (
8+
givenOperand: number,
9+
): boolean => {
10+
if (
11+
!isFinite(givenOperand)
12+
) return Attempt.abandon('Operand must be finite.');
13+
14+
return Number.isInteger(givenOperand);
15+
};
16+
17+
/** Alias for {@link isWhole} */
18+
const isInteger = isWhole;
19+
20+
export {
21+
isWhole,
22+
isInteger,
23+
};

0 commit comments

Comments
 (0)