diff --git a/Geometry/Test/Triangle.test.js b/Geometry/Test/Triangle.test.js new file mode 100644 index 0000000000..7d88b3d980 --- /dev/null +++ b/Geometry/Test/Triangle.test.js @@ -0,0 +1,95 @@ +import Triangle from '../Triangle' + +describe('Triangle', () => { + describe('Constructor', () => { + test('creates a triangle with base and height', () => { + const triangle = new Triangle(5, 10) + expect(triangle).toBeInstanceOf(Triangle) + expect(triangle.base).toBe(5) + expect(triangle.height).toBe(10) + expect(triangle.sides).toEqual([5, null, null]) + }) + + test('creates a triangle with three sides', () => { + const triangle = new Triangle(3, 4, 5) + expect(triangle).toBeInstanceOf(Triangle) + expect(triangle.sides).toEqual([3, 4, 5]) + expect(triangle.base).toBe(3) // Assuming base is one of the sides + expect(triangle.height).toBe(null) + }) + + test('throws an error if invalid number of arguments', () => { + expect(() => new Triangle()).toThrow( + 'Invalid number of arguments. Use either (base, height) or (sideA, sideB, sideC).' + ) + expect(() => new Triangle(1)).toThrow( + 'Invalid number of arguments. Use either (base, height) or (sideA, sideB, sideC).' + ) + }) + + test('throws an error if the triangle is invalid', () => { + expect(() => new Triangle(1, 2, 3)).toThrow( + 'Invalid triangle: The sum of any two sides must be greater than the third side.' + ) + }) + }) + + describe('Area Calculation', () => { + test('calculates area correctly using base and height', () => { + const triangle = new Triangle(5, 10) + expect(triangle.area()).toBe(25) + }) + + test("calculates area correctly using Heron's formula", () => { + const triangle = new Triangle(3, 4, 5) + expect(triangle.area()).toBe(6) // Area of a 3-4-5 triangle + }) + }) + + describe('Perimeter Calculation', () => { + test('calculates perimeter correctly for three sides', () => { + const triangle = new Triangle(3, 4, 5) + expect(triangle.perimeter()).toBe(12) // 3 + 4 + 5 + }) + + test('throws an error if not all sides are known', () => { + const triangle = new Triangle(5, 10) + expect(() => triangle.perimeter()).toThrow( + 'Cannot calculate perimeter: not all sides are known.' + ) + }) + }) + + describe('Getters', () => { + test('base getter returns correct value', () => { + const triangle = new Triangle(5, 10) + expect(triangle.base).toBe(5) + }) + + test('height getter returns correct value', () => { + const triangle = new Triangle(5, 10) + expect(triangle.height).toBe(10) + }) + + test('sides getter returns correct values', () => { + const triangle = new Triangle(3, 4, 5) + expect(triangle.sides).toEqual([3, 4, 5]) + }) + }) + + describe('String Representation', () => { + test('returns correct string representation for base and height', () => { + const triangle = new Triangle(5, 10) + expect(triangle.toString()).toBe( + 'Triangle: base = 5, height = 10, sides = 5, unknown, unknown, area = 25, perimeter = unknown' + ) + }) + + test('returns correct string representation for three sides', () => { + const triangle = new Triangle(3, 4, 5) + expect(triangle.toString()).toBe( + 'Triangle: base = 3, height = unknown, sides = 3, 4, 5, area = 6, perimeter = 12' + ) + }) + }) +}) diff --git a/Geometry/Triangle.js b/Geometry/Triangle.js new file mode 100644 index 0000000000..9bac2213e7 --- /dev/null +++ b/Geometry/Triangle.js @@ -0,0 +1,157 @@ +/** + * This class represents a Triangle and provides methods to calculate its area and perimeter + * based on either all three sides or the base and height. + * @see {@link https://en.wikipedia.org/wiki/Triangle} + * @class + */ +export default class Triangle { + /** @private */ + #base + + /** @private */ + #height + + /** @private */ + #sides + + /** + * Creates a triangle instance. + * @constructor + * @param {...number} args - The dimensions of the triangle. + * @throws {Error} Will throw an error if the number of arguments is invalid or if the triangle is invalid. + */ + constructor(...args) { + if (args.length === 2) { + this.#initializeFromBaseAndHeight(...args) + } else if (args.length === 3) { + this.#initializeFromSides(...args) + } else { + throw new Error( + 'Invalid number of arguments. Use either (base, height) or (sideA, sideB, sideC).' + ) + } + } + + /** + * Initializes the triangle from base and height. + * @private + * @param {number} base - The base of the triangle. + * @param {number} height - The height of the triangle. + */ + #initializeFromBaseAndHeight(base, height) { + this.#base = base + this.#height = height + this.#sides = [base, null, null] + } + + /** + * Initializes the triangle from three sides. + * @private + * @param {number} sideA - The length of side A. + * @param {number} sideB - The length of side B. + * @param {number} sideC - The length of side C. + * @throws {Error} Will throw an error if the triangle is invalid. + */ + #initializeFromSides(sideA, sideB, sideC) { + if (!this.#isValidTriangle(sideA, sideB, sideC)) { + throw new Error( + 'Invalid triangle: The sum of any two sides must be greater than the third side.' + ) + } + this.#sides = [sideA, sideB, sideC] + this.#base = sideA // Assuming base is one of the sides for consistency + this.#height = null + } + + /** + * Checks if three sides can form a valid triangle. + * @private + * @param {number} a - The length of side A. + * @param {number} b - The length of side B. + * @param {number} c - The length of side C. + * @returns {boolean} True if the triangle is valid; false otherwise. + */ + #isValidTriangle(a, b, c) { + return a + b > c && b + c > a && c + a > b + } + + /** + * Calculates the area of the triangle. + * @public + * @returns {number} The area of the triangle. + */ + area() { + if (this.#height !== null) { + return 0.5 * this.#base * this.#height + } + // Using Heron's formula + const [a, b, c] = this.#sides + const s = (a + b + c) / 2 + return Math.sqrt(s * (s - a) * (s - b) * (s - c)) + } + + /** + * Calculates the perimeter of the triangle. + * @public + * @returns {number} The perimeter of the triangle. + * @throws {Error} Will throw an error if not all sides are known. + */ + perimeter() { + if (this.#sides.some((side) => side === null)) { + throw new Error('Cannot calculate perimeter: not all sides are known.') + } + return this.#sides.reduce((sum, side) => sum + side, 0) + } + + /** + * Returns a string representation of the triangle. + * @public + * @returns {string} A string describing the triangle's dimensions and area/perimeter. + */ + toString() { + const areaValue = this.area() + let perimeterValue + + // Check if all sides are known for perimeter calculation + if (this.#sides.some((side) => side === null)) { + perimeterValue = 'unknown' + } else { + perimeterValue = this.perimeter() + } + + return ( + `Triangle: base = ${this.#base}, height = ${ + this.#height ?? 'unknown' + }, ` + + `sides = ${this.#sides.map((side) => side ?? 'unknown').join(', ')}, ` + + `area = ${areaValue}, perimeter = ${perimeterValue}` + ) + } + + /** + * Gets the base of the triangle. + * @public + * @returns {number} The base of the triangle. + */ + get base() { + return this.#base + } + + /** + * Gets the height of the triangle. + * @public + * @returns {number|null} The height of the triangle or null if not defined. + */ + get height() { + return this.#height + } + + /** + * Gets the lengths of all sides of the triangle. + * @public + * @returns {Array} An array containing lengths of all three sides or null for unknown sides. + */ + get sides() { + return this.#sides + } +}