diff --git a/src/main/java/com/thealgorithms/maths/ReturnOnInvestment.java b/src/main/java/com/thealgorithms/maths/ReturnOnInvestment.java new file mode 100644 index 000000000000..ddb0e417ef0b --- /dev/null +++ b/src/main/java/com/thealgorithms/maths/ReturnOnInvestment.java @@ -0,0 +1,66 @@ +package com.thealgorithms.maths; + +/** + * Return on Investment (ROI) calculations for evaluating investment profitability. + * + *

This class provides two related computations: + *

+ * + * @see Investopedia – ROI + * @see Investopedia – Annualized Return + */ +public final class ReturnOnInvestment { + + private ReturnOnInvestment() { + } + + /** + * Calculates the simple return on investment as a percentage. + * + * @param gainFromInvestment the total value received from the investment + * @param costOfInvestment the total cost of the investment (must be positive) + * @return ROI as a percentage; negative when a loss occurred + * @throws IllegalArgumentException if {@code costOfInvestment} is not positive + */ + public static double returnOnInvestment(final double gainFromInvestment, final double costOfInvestment) { + if (costOfInvestment <= 0) { + throw new IllegalArgumentException("costOfInvestment must be greater than 0"); + } + return (gainFromInvestment - costOfInvestment) / costOfInvestment * 100.0; + } + + /** + * Calculates the annualized (per-year) return on investment. + * + *

While simple ROI tells you the total gain over an entire holding period, + * annualized ROI normalizes that gain to a yearly rate so that investments + * held for different lengths of time can be compared on equal footing. + * It applies the geometric-mean formula: + * + *

+     *   Annualized ROI = ((1 + simpleROI / 100) ^ (1 / years) - 1) × 100
+     * 
+ * + * @param gainFromInvestment the total value received from the investment + * @param costOfInvestment the total cost of the investment (must be positive) + * @param years the number of years the investment was held (must be positive) + * @return annualized ROI as a percentage + * @throws IllegalArgumentException if {@code costOfInvestment} or {@code years} is not positive + */ + public static double annualizedReturnOnInvestment(final double gainFromInvestment, final double costOfInvestment, final double years) { + if (costOfInvestment <= 0) { + throw new IllegalArgumentException("costOfInvestment must be greater than 0"); + } + if (years <= 0) { + throw new IllegalArgumentException("years must be greater than 0"); + } + final double simpleRoi = returnOnInvestment(gainFromInvestment, costOfInvestment); + return (Math.pow(1.0 + simpleRoi / 100.0, 1.0 / years) - 1.0) * 100.0; + } +} diff --git a/src/test/java/com/thealgorithms/maths/ReturnOnInvestmentTest.java b/src/test/java/com/thealgorithms/maths/ReturnOnInvestmentTest.java new file mode 100644 index 000000000000..df47f27e9d24 --- /dev/null +++ b/src/test/java/com/thealgorithms/maths/ReturnOnInvestmentTest.java @@ -0,0 +1,93 @@ +package com.thealgorithms.maths; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import org.junit.jupiter.api.Test; + +public class ReturnOnInvestmentTest { + + private static final double DELTA = 1e-9; + + // --- Simple ROI --- + + @Test + void testPositiveROI() { + assertEquals(100.0, ReturnOnInvestment.returnOnInvestment(1000, 500)); + } + + @Test + void testZeroROI() { + assertEquals(0.0, ReturnOnInvestment.returnOnInvestment(500, 500)); + } + + @Test + void testNegativeROI() { + assertEquals(-60.0, ReturnOnInvestment.returnOnInvestment(200, 500)); + } + + @Test + void testTotalLoss() { + assertEquals(-100.0, ReturnOnInvestment.returnOnInvestment(0, 500)); + } + + @Test + void testZeroCostThrows() { + assertThrows(IllegalArgumentException.class, () -> ReturnOnInvestment.returnOnInvestment(1000, 0)); + } + + @Test + void testNegativeCostThrows() { + assertThrows(IllegalArgumentException.class, () -> ReturnOnInvestment.returnOnInvestment(1000, -100)); + } + + // --- Annualized ROI --- + + @Test + void testAnnualizedROIOneYear() { + // Over exactly 1 year, annualized ROI == simple ROI + assertEquals(100.0, ReturnOnInvestment.annualizedReturnOnInvestment(1000, 500, 1), DELTA); + } + + @Test + void testAnnualizedROITwoYears() { + // Simple ROI = 100% over 2 years → annualized = (sqrt(2) - 1) * 100 ≈ 41.42% + double expected = (Math.pow(2.0, 0.5) - 1.0) * 100.0; + assertEquals(expected, ReturnOnInvestment.annualizedReturnOnInvestment(1000, 500, 2), DELTA); + } + + @Test + void testAnnualizedROIFractionalYear() { + // 6 months (0.5 years): annualizes to a higher rate than the simple ROI + double expected = (Math.pow(2.0, 2.0) - 1.0) * 100.0; // (1+1)^2 - 1 = 300% + assertEquals(expected, ReturnOnInvestment.annualizedReturnOnInvestment(1000, 500, 0.5), DELTA); + } + + @Test + void testAnnualizedZeroROI() { + // If gain == cost, ROI is 0 regardless of holding period + assertEquals(0.0, ReturnOnInvestment.annualizedReturnOnInvestment(500, 500, 5), DELTA); + } + + @Test + void testAnnualizedNegativeROI() { + // Loss of 50% over 2 years: annualized = (sqrt(0.5) - 1) * 100 ≈ -29.29% + double expected = (Math.pow(0.5, 0.5) - 1.0) * 100.0; + assertEquals(expected, ReturnOnInvestment.annualizedReturnOnInvestment(500, 1000, 2), DELTA); + } + + @Test + void testAnnualizedZeroYearsThrows() { + assertThrows(IllegalArgumentException.class, () -> ReturnOnInvestment.annualizedReturnOnInvestment(1000, 500, 0)); + } + + @Test + void testAnnualizedNegativeYearsThrows() { + assertThrows(IllegalArgumentException.class, () -> ReturnOnInvestment.annualizedReturnOnInvestment(1000, 500, -3)); + } + + @Test + void testAnnualizedZeroCostThrows() { + assertThrows(IllegalArgumentException.class, () -> ReturnOnInvestment.annualizedReturnOnInvestment(1000, 0, 2)); + } +}