Skip to content
66 changes: 66 additions & 0 deletions src/main/java/com/thealgorithms/maths/ReturnOnInvestment.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
package com.thealgorithms.maths;

/**
* Return on Investment (ROI) calculations for evaluating investment profitability.
*
* <p>This class provides two related computations:
* <ul>
* <li><b>Simple ROI</b> – measures total gain relative to cost:
* {@code ROI = (Gain - Cost) / Cost × 100}</li>
* <li><b>Annualized ROI</b> – converts a total ROI over multiple years into
* an equivalent annual rate using the geometric mean:
* {@code Annualized ROI = ((1 + ROI/100)^(1/n) - 1) × 100}</li>
* </ul>
*
* @see <a href="https://www.investopedia.com/terms/r/returnoninvestment.asp">Investopedia – ROI</a>
* @see <a href="https://www.investopedia.com/terms/a/annualized-total-return.asp">Investopedia – Annualized Return</a>
*/
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.
*
* <p>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:
*
* <pre>
* Annualized ROI = ((1 + simpleROI / 100) ^ (1 / years) - 1) × 100
* </pre>
*
* @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;
}
}
Original file line number Diff line number Diff line change
@@ -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));
}
}
Loading