Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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