Skip to content

Commit f8688ba

Browse files
authored
refactor: Enhance docs, code, add tests in Means (#6750)
1 parent c6497a2 commit f8688ba

File tree

2 files changed

+255
-41
lines changed

2 files changed

+255
-41
lines changed

src/main/java/com/thealgorithms/maths/Means.java

Lines changed: 82 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -4,51 +4,118 @@
44
import org.apache.commons.collections4.IterableUtils;
55

66
/**
7-
* https://en.wikipedia.org/wiki/Mean
7+
* Utility class for computing various types of statistical means.
88
* <p>
9-
* by: Punit Patel
9+
* This class provides static methods to calculate different types of means
10+
* (averages)
11+
* from a collection of numbers. All methods accept any {@link Iterable}
12+
* collection of
13+
* {@link Double} values and return the computed mean as a {@link Double}.
14+
* </p>
15+
*
16+
* <p>
17+
* Supported means:
18+
* <ul>
19+
* <li><b>Arithmetic Mean</b>: The sum of all values divided by the count</li>
20+
* <li><b>Geometric Mean</b>: The nth root of the product of n values</li>
21+
* <li><b>Harmonic Mean</b>: The reciprocal of the arithmetic mean of
22+
* reciprocals</li>
23+
* </ul>
24+
* </p>
25+
*
26+
* @see <a href="https://en.wikipedia.org/wiki/Mean">Mean (Wikipedia)</a>
27+
* @author Punit Patel
1028
*/
1129
public final class Means {
1230

1331
private Means() {
1432
}
1533

1634
/**
17-
* @brief computes the [Arithmetic Mean](https://en.wikipedia.org/wiki/Arithmetic_mean) of the input
18-
* @param numbers the input numbers
19-
* @throws IllegalArgumentException empty input
35+
* Computes the arithmetic mean (average) of the given numbers.
36+
* <p>
37+
* The arithmetic mean is calculated as: (x₁ + x₂ + ... + xₙ) / n
38+
* </p>
39+
* <p>
40+
* Example: For numbers [2, 4, 6], the arithmetic mean is (2+4+6)/3 = 4.0
41+
* </p>
42+
*
43+
* @param numbers the input numbers (must not be empty)
2044
* @return the arithmetic mean of the input numbers
45+
* @throws IllegalArgumentException if the input is empty
46+
* @see <a href="https://en.wikipedia.org/wiki/Arithmetic_mean">Arithmetic
47+
* Mean</a>
2148
*/
2249
public static Double arithmetic(final Iterable<Double> numbers) {
2350
checkIfNotEmpty(numbers);
24-
return StreamSupport.stream(numbers.spliterator(), false).reduce((x, y) -> x + y).get() / IterableUtils.size(numbers);
51+
double sum = StreamSupport.stream(numbers.spliterator(), false).reduce(0d, (x, y) -> x + y);
52+
int size = IterableUtils.size(numbers);
53+
return sum / size;
2554
}
2655

2756
/**
28-
* @brief computes the [Geometric Mean](https://en.wikipedia.org/wiki/Geometric_mean) of the input
29-
* @param numbers the input numbers
30-
* @throws IllegalArgumentException empty input
57+
* Computes the geometric mean of the given numbers.
58+
* <p>
59+
* The geometric mean is calculated as: ⁿ√(x₁ × x₂ × ... × xₙ)
60+
* </p>
61+
* <p>
62+
* Example: For numbers [2, 8], the geometric mean is √(2×8) = √16 = 4.0
63+
* </p>
64+
* <p>
65+
* Note: This method may produce unexpected results for negative numbers,
66+
* as it computes the real-valued nth root which may not exist for negative
67+
* products.
68+
* </p>
69+
*
70+
* @param numbers the input numbers (must not be empty)
3171
* @return the geometric mean of the input numbers
72+
* @throws IllegalArgumentException if the input is empty
73+
* @see <a href="https://en.wikipedia.org/wiki/Geometric_mean">Geometric
74+
* Mean</a>
3275
*/
3376
public static Double geometric(final Iterable<Double> numbers) {
3477
checkIfNotEmpty(numbers);
35-
return Math.pow(StreamSupport.stream(numbers.spliterator(), false).reduce((x, y) -> x * y).get(), 1d / IterableUtils.size(numbers));
78+
double product = StreamSupport.stream(numbers.spliterator(), false).reduce(1d, (x, y) -> x * y);
79+
int size = IterableUtils.size(numbers);
80+
return Math.pow(product, 1.0 / size);
3681
}
3782

3883
/**
39-
* @brief computes the [Harmonic Mean](https://en.wikipedia.org/wiki/Harmonic_mean) of the input
40-
* @param numbers the input numbers
41-
* @throws IllegalArgumentException empty input
84+
* Computes the harmonic mean of the given numbers.
85+
* <p>
86+
* The harmonic mean is calculated as: n / (1/x₁ + 1/x₂ + ... + 1/xₙ)
87+
* </p>
88+
* <p>
89+
* Example: For numbers [1, 2, 4], the harmonic mean is 3/(1/1 + 1/2 + 1/4) =
90+
* 3/1.75 ≈ 1.714
91+
* </p>
92+
* <p>
93+
* Note: This method will produce unexpected results if any input number is
94+
* zero,
95+
* as it involves computing reciprocals.
96+
* </p>
97+
*
98+
* @param numbers the input numbers (must not be empty)
4299
* @return the harmonic mean of the input numbers
100+
* @throws IllegalArgumentException if the input is empty
101+
* @see <a href="https://en.wikipedia.org/wiki/Harmonic_mean">Harmonic Mean</a>
43102
*/
44103
public static Double harmonic(final Iterable<Double> numbers) {
45104
checkIfNotEmpty(numbers);
46-
return IterableUtils.size(numbers) / StreamSupport.stream(numbers.spliterator(), false).reduce(0d, (x, y) -> x + 1d / y);
105+
double sumOfReciprocals = StreamSupport.stream(numbers.spliterator(), false).reduce(0d, (x, y) -> x + 1d / y);
106+
int size = IterableUtils.size(numbers);
107+
return size / sumOfReciprocals;
47108
}
48109

110+
/**
111+
* Validates that the input iterable is not empty.
112+
*
113+
* @param numbers the input numbers to validate
114+
* @throws IllegalArgumentException if the input is empty
115+
*/
49116
private static void checkIfNotEmpty(final Iterable<Double> numbers) {
50117
if (!numbers.iterator().hasNext()) {
51-
throw new IllegalArgumentException("Emtpy list given for Mean computation.");
118+
throw new IllegalArgumentException("Empty list given for Mean computation.");
52119
}
53120
}
54121
}

src/test/java/com/thealgorithms/maths/MeansTest.java

Lines changed: 173 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -2,70 +2,217 @@
22

33
import static org.junit.jupiter.api.Assertions.assertEquals;
44
import static org.junit.jupiter.api.Assertions.assertThrows;
5+
import static org.junit.jupiter.api.Assertions.assertTrue;
56

67
import java.util.ArrayList;
8+
import java.util.Arrays;
79
import java.util.LinkedHashSet;
810
import java.util.LinkedList;
911
import java.util.List;
1012
import java.util.Set;
13+
import java.util.TreeSet;
1114
import java.util.Vector;
12-
import org.assertj.core.util.Lists;
13-
import org.assertj.core.util.Sets;
1415
import org.junit.jupiter.api.Test;
1516

17+
/**
18+
* Test class for {@link Means}.
19+
* <p>
20+
* This class provides comprehensive test coverage for all mean calculation
21+
* methods,
22+
* including edge cases, various collection types, and error conditions.
23+
* </p>
24+
*/
1625
class MeansTest {
1726

27+
private static final double EPSILON = 1e-9;
28+
29+
// ========== Arithmetic Mean Tests ==========
30+
1831
@Test
19-
void arithmeticMeanZeroNumbers() throws IllegalArgumentException {
32+
void testArithmeticMeanThrowsExceptionForEmptyList() {
2033
List<Double> numbers = new ArrayList<>();
21-
assertThrows(IllegalArgumentException.class, () -> Means.arithmetic(numbers));
34+
IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> Means.arithmetic(numbers));
35+
assertTrue(exception.getMessage().contains("Empty list"));
36+
}
37+
38+
@Test
39+
void testArithmeticMeanSingleNumber() {
40+
List<Double> numbers = Arrays.asList(2.5);
41+
assertEquals(2.5, Means.arithmetic(numbers), EPSILON);
42+
}
43+
44+
@Test
45+
void testArithmeticMeanTwoNumbers() {
46+
List<Double> numbers = Arrays.asList(2.0, 4.0);
47+
assertEquals(3.0, Means.arithmetic(numbers), EPSILON);
2248
}
2349

2450
@Test
25-
void geometricMeanZeroNumbers() throws IllegalArgumentException {
51+
void testArithmeticMeanMultipleNumbers() {
52+
List<Double> numbers = Arrays.asList(1.0, 2.0, 3.0, 4.0, 5.0);
53+
assertEquals(3.0, Means.arithmetic(numbers), EPSILON);
54+
}
55+
56+
@Test
57+
void testArithmeticMeanWithTreeSet() {
58+
Set<Double> numbers = new TreeSet<>(Arrays.asList(1.0, 2.5, 83.3, 25.9999, 46.0001, 74.7, 74.5));
59+
assertEquals(44.0, Means.arithmetic(numbers), EPSILON);
60+
}
61+
62+
@Test
63+
void testArithmeticMeanWithNegativeNumbers() {
64+
List<Double> numbers = Arrays.asList(-5.0, -3.0, -1.0, 1.0, 3.0, 5.0);
65+
assertEquals(0.0, Means.arithmetic(numbers), EPSILON);
66+
}
67+
68+
@Test
69+
void testArithmeticMeanWithDecimalNumbers() {
70+
List<Double> numbers = Arrays.asList(1.1, 2.2, 3.3, 4.4, 5.5);
71+
assertEquals(3.3, Means.arithmetic(numbers), EPSILON);
72+
}
73+
74+
@Test
75+
void testArithmeticMeanWithVector() {
76+
Vector<Double> numbers = new Vector<>(Arrays.asList(10.0, 20.0, 30.0));
77+
assertEquals(20.0, Means.arithmetic(numbers), EPSILON);
78+
}
79+
80+
// ========== Geometric Mean Tests ==========
81+
82+
@Test
83+
void testGeometricMeanThrowsExceptionForEmptyList() {
2684
List<Double> numbers = new ArrayList<>();
27-
assertThrows(IllegalArgumentException.class, () -> Means.geometric(numbers));
85+
IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> Means.geometric(numbers));
86+
assertTrue(exception.getMessage().contains("Empty list"));
2887
}
2988

3089
@Test
31-
void harmonicMeanZeroNumbers() throws IllegalArgumentException {
90+
void testGeometricMeanSingleNumber() {
91+
Set<Double> numbers = new LinkedHashSet<>(Arrays.asList(2.5));
92+
assertEquals(2.5, Means.geometric(numbers), EPSILON);
93+
}
94+
95+
@Test
96+
void testGeometricMeanTwoNumbers() {
97+
List<Double> numbers = Arrays.asList(2.0, 8.0);
98+
assertEquals(4.0, Means.geometric(numbers), EPSILON);
99+
}
100+
101+
@Test
102+
void testGeometricMeanMultipleNumbers() {
103+
LinkedList<Double> numbers = new LinkedList<>(Arrays.asList(1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 1.25));
104+
assertEquals(2.6426195539300585, Means.geometric(numbers), EPSILON);
105+
}
106+
107+
@Test
108+
void testGeometricMeanPerfectSquares() {
109+
List<Double> numbers = Arrays.asList(1.0, 4.0, 9.0, 16.0);
110+
double expected = Math.pow(1.0 * 4.0 * 9.0 * 16.0, 1.0 / 4.0);
111+
assertEquals(expected, Means.geometric(numbers), EPSILON);
112+
}
113+
114+
@Test
115+
void testGeometricMeanIdenticalNumbers() {
116+
List<Double> numbers = Arrays.asList(5.0, 5.0, 5.0, 5.0);
117+
assertEquals(5.0, Means.geometric(numbers), EPSILON);
118+
}
119+
120+
@Test
121+
void testGeometricMeanWithLinkedHashSet() {
122+
LinkedHashSet<Double> numbers = new LinkedHashSet<>(Arrays.asList(2.0, 4.0, 8.0));
123+
double expected = Math.pow(2.0 * 4.0 * 8.0, 1.0 / 3.0);
124+
assertEquals(expected, Means.geometric(numbers), EPSILON);
125+
}
126+
127+
// ========== Harmonic Mean Tests ==========
128+
129+
@Test
130+
void testHarmonicMeanThrowsExceptionForEmptyList() {
32131
List<Double> numbers = new ArrayList<>();
33-
assertThrows(IllegalArgumentException.class, () -> Means.harmonic(numbers));
132+
IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> Means.harmonic(numbers));
133+
assertTrue(exception.getMessage().contains("Empty list"));
134+
}
135+
136+
@Test
137+
void testHarmonicMeanSingleNumber() {
138+
LinkedHashSet<Double> numbers = new LinkedHashSet<>(Arrays.asList(2.5));
139+
assertEquals(2.5, Means.harmonic(numbers), EPSILON);
34140
}
35141

36142
@Test
37-
void arithmeticMeanSingleNumber() {
38-
List<Double> numbers = Lists.newArrayList(2.5);
39-
assertEquals(2.5, Means.arithmetic(numbers));
143+
void testHarmonicMeanTwoNumbers() {
144+
List<Double> numbers = Arrays.asList(2.0, 4.0);
145+
double expected = 2.0 / (1.0 / 2.0 + 1.0 / 4.0);
146+
assertEquals(expected, Means.harmonic(numbers), EPSILON);
40147
}
41148

42149
@Test
43-
void geometricMeanSingleNumber() {
44-
Set<Double> numbers = Sets.newHashSet(Lists.newArrayList(2.5));
45-
assertEquals(2.5, Means.geometric(numbers));
150+
void testHarmonicMeanMultipleNumbers() {
151+
Vector<Double> numbers = new Vector<>(Arrays.asList(1.0, 2.5, 83.3, 25.9999, 46.0001, 74.7, 74.5));
152+
assertEquals(4.6697322801074135, Means.harmonic(numbers), EPSILON);
46153
}
47154

48155
@Test
49-
void harmonicMeanSingleNumber() {
50-
LinkedHashSet<Double> numbers = Sets.newLinkedHashSet(2.5);
51-
assertEquals(2.5, Means.harmonic(numbers));
156+
void testHarmonicMeanThreeNumbers() {
157+
List<Double> numbers = Arrays.asList(1.0, 2.0, 4.0);
158+
double expected = 3.0 / (1.0 / 1.0 + 1.0 / 2.0 + 1.0 / 4.0);
159+
assertEquals(expected, Means.harmonic(numbers), EPSILON);
52160
}
53161

54162
@Test
55-
void arithmeticMeanMultipleNumbers() {
56-
Set<Double> numbers = Sets.newTreeSet(1d, 2.5, 83.3, 25.9999, 46.0001, 74.7, 74.5);
57-
assertEquals(44, Means.arithmetic(numbers));
163+
void testHarmonicMeanIdenticalNumbers() {
164+
List<Double> numbers = Arrays.asList(6.0, 6.0, 6.0);
165+
assertEquals(6.0, Means.harmonic(numbers), EPSILON);
58166
}
59167

60168
@Test
61-
void geometricMeanMultipleNumbers() {
62-
LinkedList<Double> numbers = new LinkedList<>(Lists.newArrayList(1d, 2d, 3d, 4d, 5d, 6d, 1.25));
63-
assertEquals(2.6426195539300585, Means.geometric(numbers));
169+
void testHarmonicMeanWithLinkedList() {
170+
LinkedList<Double> numbers = new LinkedList<>(Arrays.asList(3.0, 6.0, 9.0));
171+
double expected = 3.0 / (1.0 / 3.0 + 1.0 / 6.0 + 1.0 / 9.0);
172+
assertEquals(expected, Means.harmonic(numbers), EPSILON);
173+
}
174+
175+
// ========== Additional Edge Case Tests ==========
176+
177+
@Test
178+
void testArithmeticMeanWithVeryLargeNumbers() {
179+
List<Double> numbers = Arrays.asList(1e100, 2e100, 3e100);
180+
assertEquals(2e100, Means.arithmetic(numbers), 1e90);
64181
}
65182

66183
@Test
67-
void harmonicMeanMultipleNumbers() {
68-
Vector<Double> numbers = new Vector<>(Lists.newArrayList(1d, 2.5, 83.3, 25.9999, 46.0001, 74.7, 74.5));
69-
assertEquals(4.6697322801074135, Means.harmonic(numbers));
184+
void testArithmeticMeanWithVerySmallNumbers() {
185+
List<Double> numbers = Arrays.asList(1e-100, 2e-100, 3e-100);
186+
assertEquals(2e-100, Means.arithmetic(numbers), 1e-110);
187+
}
188+
189+
@Test
190+
void testGeometricMeanWithOnes() {
191+
List<Double> numbers = Arrays.asList(1.0, 1.0, 1.0, 1.0);
192+
assertEquals(1.0, Means.geometric(numbers), EPSILON);
193+
}
194+
195+
@Test
196+
void testAllMeansConsistencyForIdenticalValues() {
197+
List<Double> numbers = Arrays.asList(7.5, 7.5, 7.5, 7.5);
198+
double arithmetic = Means.arithmetic(numbers);
199+
double geometric = Means.geometric(numbers);
200+
double harmonic = Means.harmonic(numbers);
201+
202+
assertEquals(7.5, arithmetic, EPSILON);
203+
assertEquals(7.5, geometric, EPSILON);
204+
assertEquals(7.5, harmonic, EPSILON);
205+
}
206+
207+
@Test
208+
void testMeansRelationship() {
209+
// For positive numbers, harmonic mean ≤ geometric mean ≤ arithmetic mean
210+
List<Double> numbers = Arrays.asList(2.0, 4.0, 8.0);
211+
double arithmetic = Means.arithmetic(numbers);
212+
double geometric = Means.geometric(numbers);
213+
double harmonic = Means.harmonic(numbers);
214+
215+
assertTrue(harmonic <= geometric, "Harmonic mean should be ≤ geometric mean");
216+
assertTrue(geometric <= arithmetic, "Geometric mean should be ≤ arithmetic mean");
70217
}
71218
}

0 commit comments

Comments
 (0)