Skip to content

Commit d622eb7

Browse files
committed
add gcd function, refacroting math module
1 parent cdb29dd commit d622eb7

File tree

3 files changed

+142
-64
lines changed

3 files changed

+142
-64
lines changed

.changeset/old-beds-raise.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@zemd/std-modules": patch
3+
---
4+
5+
math: add gcd function, refactoring math module

src/math.test.ts

Lines changed: 67 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,15 @@ import {
33
sign,
44
clamp,
55
clamp01,
6-
degToRad,
7-
radToDeg,
6+
degreesToRadians,
7+
radiansToDegrees,
88
pingPong,
99
wrap,
10-
angleDifferenceDegrees,
11-
angleDifferenceRadians,
10+
angleDeltaDegrees,
11+
angleDeltaRadians,
1212
normalize,
1313
nextPowerOfTwo,
14+
gcd,
1415
} from "./math";
1516

1617
describe("sign", () => {
@@ -62,21 +63,21 @@ describe("clamp01", () => {
6263

6364
describe("degToRad", () => {
6465
it("should convert degrees to radians", () => {
65-
expect(degToRad(0)).toBe(0);
66-
expect(degToRad(90)).toBeCloseTo(Math.PI / 2);
67-
expect(degToRad(180)).toBeCloseTo(Math.PI);
68-
expect(degToRad(360)).toBeCloseTo(2 * Math.PI);
69-
expect(degToRad(-90)).toBeCloseTo(-Math.PI / 2);
66+
expect(degreesToRadians(0)).toBe(0);
67+
expect(degreesToRadians(90)).toBeCloseTo(Math.PI / 2);
68+
expect(degreesToRadians(180)).toBeCloseTo(Math.PI);
69+
expect(degreesToRadians(360)).toBeCloseTo(2 * Math.PI);
70+
expect(degreesToRadians(-90)).toBeCloseTo(-Math.PI / 2);
7071
});
7172
});
7273

7374
describe("radToDeg", () => {
7475
it("should convert radians to degrees", () => {
75-
expect(radToDeg(0)).toBe(0);
76-
expect(radToDeg(Math.PI / 2)).toBeCloseTo(90);
77-
expect(radToDeg(Math.PI)).toBeCloseTo(180);
78-
expect(radToDeg(2 * Math.PI)).toBeCloseTo(360);
79-
expect(radToDeg(-Math.PI / 2)).toBeCloseTo(-90);
76+
expect(radiansToDegrees(0)).toBe(0);
77+
expect(radiansToDegrees(Math.PI / 2)).toBeCloseTo(90);
78+
expect(radiansToDegrees(Math.PI)).toBeCloseTo(180);
79+
expect(radiansToDegrees(2 * Math.PI)).toBeCloseTo(360);
80+
expect(radiansToDegrees(-Math.PI / 2)).toBeCloseTo(-90);
8081
});
8182
});
8283

@@ -123,18 +124,18 @@ describe("wrap", () => {
123124

124125
describe("angleDifferenceDegrees", () => {
125126
it("should calculate angle differences in degrees", () => {
126-
expect(angleDifferenceDegrees(0, 90)).toBe(90);
127-
expect(angleDifferenceDegrees(0, 450)).toBe(90);
128-
expect(angleDifferenceDegrees(350, 10)).toBe(20);
129-
expect(angleDifferenceDegrees(10, 350)).toBe(-20);
127+
expect(angleDeltaDegrees(0, 90)).toBe(90);
128+
expect(angleDeltaDegrees(0, 450)).toBe(90);
129+
expect(angleDeltaDegrees(350, 10)).toBe(20);
130+
expect(angleDeltaDegrees(10, 350)).toBe(-20);
130131
});
131132
});
132133

133134
describe("angleDifferenceRadians", () => {
134135
it("should calculate angle differences in radians", () => {
135-
expect(angleDifferenceRadians(0, Math.PI)).toBeCloseTo(-Math.PI);
136-
expect(angleDifferenceRadians(0, 3 * Math.PI)).toBeCloseTo(-Math.PI);
137-
expect(angleDifferenceRadians(0, Math.PI / 2)).toBeCloseTo(Math.PI / 2);
136+
expect(angleDeltaRadians(0, Math.PI)).toBeCloseTo(-Math.PI);
137+
expect(angleDeltaRadians(0, 3 * Math.PI)).toBeCloseTo(-Math.PI);
138+
expect(angleDeltaRadians(0, Math.PI / 2)).toBeCloseTo(Math.PI / 2);
138139
});
139140
});
140141

@@ -198,3 +199,48 @@ describe("nextPowerOfTwo", () => {
198199
expect(nextPowerOfTwo(1024)).toBe(1024);
199200
});
200201
});
202+
203+
describe("findGreatestCommonDivisor", () => {
204+
it("should calculate GCD of positive integers", () => {
205+
expect(gcd(48, 18)).toBe(6);
206+
expect(gcd(12, 8)).toBe(4);
207+
expect(gcd(17, 13)).toBe(1);
208+
expect(gcd(100, 25)).toBe(25);
209+
});
210+
211+
it("should handle negative integers", () => {
212+
expect(gcd(-12, 8)).toBe(4);
213+
expect(gcd(12, -8)).toBe(4);
214+
expect(gcd(-12, -8)).toBe(4);
215+
});
216+
217+
it("should handle zero values", () => {
218+
expect(gcd(0, 5)).toBe(5);
219+
expect(gcd(5, 0)).toBe(5);
220+
expect(gcd(0, 0)).toBe(0);
221+
});
222+
223+
it("should handle edge cases", () => {
224+
expect(gcd(1, 1)).toBe(1);
225+
expect(gcd(1, 100)).toBe(1);
226+
expect(gcd(Number.MAX_SAFE_INTEGER, 1)).toBe(1);
227+
});
228+
229+
it("should throw TypeError for non-safe integers", () => {
230+
expect(() => {
231+
return gcd(1.5, 2);
232+
}).toThrow(TypeError);
233+
expect(() => {
234+
return gcd(1, 2.5);
235+
}).toThrow(TypeError);
236+
expect(() => {
237+
return gcd(Number.MAX_SAFE_INTEGER + 1, 2);
238+
}).toThrow(TypeError);
239+
expect(() => {
240+
return gcd(Number.POSITIVE_INFINITY, 2);
241+
}).toThrow(TypeError);
242+
expect(() => {
243+
return gcd(Number.NaN, 2);
244+
}).toThrow(TypeError);
245+
});
246+
});

src/math.ts

Lines changed: 70 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -12,33 +12,40 @@ export const sign = (x: number): -1 | 0 | 1 => {
1212
};
1313

1414
/**
15-
* Restricts a value to be within a specified range defined by values min and max. If the value is
16-
* min <= value <= max, returns value. If the value is greater than max, return max; if the value
17-
* is less than min, return min.
18-
* Otherwise, return the value unchanged.
15+
* Restricts a value to be within a specified range. Useful for ensuring values stay within
16+
* valid bounds for UI elements, progress bars, or game physics calculations.
1917
*/
2018
export const clamp = (x: number, min: number, max: number): number => {
2119
return Math.min(Math.max(x, min), max);
2220
};
2321

2422
/**
25-
* Clamps a number between 0 and 1. Alias to `clamp(x, 0, 1)`.
23+
* Clamps a number between 0 and 1. Alias to `clamp(x, 0, 1)`. Commonly used for normalizing percentages,
24+
* opacity values, or color components.
2625
*/
2726
export const clamp01 = (x: number): number => {
2827
return clamp(x, 0, 1);
2928
};
3029

31-
export const degToRad = (degrees: number): number => {
30+
/**
31+
* Converts degrees to radians. Essential for trigonometric calculations,
32+
* canvas rotations, and physics simulations.
33+
*/
34+
export const degreesToRadians = (degrees: number): number => {
3235
return (degrees * Math.PI) / 180;
3336
};
3437

35-
export const radToDeg = (radians: number): number => {
38+
/**
39+
* Converts radians to degrees. Useful for displaying human-readable angles
40+
* and converting between mathematical calculations and user interfaces.
41+
*/
42+
export const radiansToDegrees = (radians: number): number => {
3643
return (radians * 180) / Math.PI;
3744
};
3845

3946
/**
40-
* PingPong returns a value that increments and decrements between zero and the length.
41-
* It follows the triangle wave formula where the bottom is set to zero and the peak is set to length.
47+
* Creates a triangle wave pattern that oscillates between 0 and a specified length.
48+
* Ideal for creating smooth back-and-forth animations, pendulum effects, or breathing UI elements.
4249
*/
4350
export const pingPong = (x: number, length: number): number => {
4451
const doubleLength = length * 2;
@@ -47,24 +54,12 @@ export const pingPong = (x: number, length: number): number => {
4754
};
4855

4956
/**
50-
* Wraps a value within a specified range [from, to).
57+
* Wraps a value within a specified range [from, to), creating cyclic behavior. Perfect for
58+
* implementing angle wrapping, looping animations, circular buffers, or ensuring
59+
* array indices stay within bounds.
5160
*
52-
* This function is useful for creating cyclic behaviors, such as:
53-
* - Wrapping angles to stay within 0-360 degrees
54-
* - Creating looping animations or counters
55-
* - Implementing periodic boundary conditions
56-
* - Ensuring array indices stay within bounds in circular buffers
57-
*
58-
* The function ensures the result is always within [from, to), where 'from' is inclusive
59-
* and 'to' is exclusive. If the input range is invalid (from > to), the parameters
60-
* are automatically swapped to create a valid range.
61-
*
62-
* ```js
63-
* wrap(5, 0, 3); // Returns 2 (5 wraps around: 5 - 3 = 2)
64-
* wrap(-1, 0, 3); // Returns 2 (-1 wraps around: -1 + 3 = 2)
65-
* wrap(7, 2, 5); // Returns 4 (7 wraps to range [2,5): 7 - 3 = 4)
66-
* wrap(370, 0, 360); // Returns 10 (angle wrapping)
67-
* ```
61+
* The result is always within [from, to) where 'from' is inclusive and 'to' is exclusive.
62+
* Invalid ranges are automatically corrected by swapping the parameters.
6863
*/
6964
export const wrap = (value: number, from: number, to: number): number => {
7065
let minValue = from;
@@ -84,43 +79,41 @@ export const wrap = (value: number, from: number, to: number): number => {
8479
};
8580

8681
/**
87-
* Calculates the difference between two angles in degrees.
82+
* Calculates the shortest angular distance between two angles in degrees.
8883
* ```js
89-
* angleDifferenceDegrees(0, 90); // Returns 90
90-
* angleDifferenceDegrees(0, 450); // Also returns 90
84+
* angleDeltaDegrees(0, 90); // Returns 90
85+
* angleDeltaDegrees(0, 450); // Returns 90
9186
* ```
87+
* Essential for smooth rotation animations and determining optimal turning directions.
9288
*/
93-
export const angleDifferenceDegrees = (currentAngle: number, targetAngle: number): number => {
89+
export const angleDeltaDegrees = (currentAngle: number, targetAngle: number): number => {
9490
return wrap(targetAngle - currentAngle, -180, 180);
9591
};
9692

9793
/**
98-
* Calculates the difference between two angles in radians.
94+
* Calculates the shortest angular distance between two angles in radians.
95+
* Useful for mathematical computations involving trigonometric functions and physics simulations.
9996
* ```js
100-
* angleDifferenceRadians(0, Math.PI); // Returns -Math.PI
101-
* angleDifferenceRadians(0, 3 * Math.PI); // Also returns -Math.PI
97+
* angleDeltaRadians(0, Math.PI); // Returns -Math.PI
98+
* angleDeltaRadians(0, 3 * Math.PI); // Returns -Math.PI
10299
* ```
103100
*/
104-
export const angleDifferenceRadians = (currentAngle: number, targetAngle: number): number => {
101+
export const angleDeltaRadians = (currentAngle: number, targetAngle: number): number => {
105102
return wrap(targetAngle - currentAngle, -Math.PI, Math.PI);
106103
};
107104

108105
/**
109-
* Normalizes a value within a specified range [from, to] to a value between 0 and 1.
106+
* Converts a value from one range to a normalized 0-1 scale. Commonly used for
107+
* creating progress indicators, scaling measurements, or preparing data for interpolation.
110108
*/
111109
export const normalize = (value: number, from: number, to: number): number => {
112110
return clamp((value - from) / (to - from), 0, 1);
113111
};
114112

115113
/**
116-
* Returns the smallest power of 2 that is greater than or equal to the input value.
117-
*
118-
* ```js
119-
* nextPowerOfTwo(1); // Returns 1
120-
* nextPowerOfTwo(3); // Returns 4
121-
* nextPowerOfTwo(8); // Returns 8
122-
* nextPowerOfTwo(15); // Returns 16
123-
* ```
114+
* Finds the smallest power of 2 that is greater than or equal to the input value.
115+
* Essential for optimizing memory allocation, texture sizes, and buffer capacities
116+
* in graphics programming and data structures.
124117
*/
125118
export const nextPowerOfTwo = (inputValue: number): number => {
126119
if (!Number.isSafeInteger(inputValue)) {
@@ -155,3 +148,37 @@ export const nextPowerOfTwo = (inputValue: number): number => {
155148

156149
return x + 1;
157150
};
151+
152+
/**
153+
* Calculates the greatest common divisor of two integers using the Euclidean algorithm.
154+
* Useful for simplifying fractions, finding common denominators, or optimizing
155+
* grid layouts and proportional calculations.
156+
*
157+
* @throws {TypeError} When inputs are not safe integers
158+
*/
159+
export const gcd = (a: number, b: number): number => {
160+
if (!Number.isSafeInteger(a) || !Number.isSafeInteger(b)) {
161+
throw new TypeError("Both arguments must be safe integers");
162+
}
163+
164+
// Handle edge cases
165+
const absA = Math.abs(a);
166+
const absB = Math.abs(b);
167+
168+
if (absA === 0) {
169+
return absB;
170+
}
171+
if (absB === 0) {
172+
return absA;
173+
}
174+
175+
// Optimized iterative Euclidean algorithm
176+
let x = absA;
177+
let y = absB;
178+
179+
while (y !== 0) {
180+
[x, y] = [y, x % y] as const;
181+
}
182+
183+
return x;
184+
};

0 commit comments

Comments
 (0)