View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one or more
3    * contributor license agreements.  See the NOTICE file distributed with
4    * this work for additional information regarding copyright ownership.
5    * The ASF licenses this file to You under the Apache License, Version 2.0
6    * (the "License"); you may not use this file except in compliance with
7    * the License.  You may obtain a copy of the License at
8    *
9    *      http://www.apache.org/licenses/LICENSE-2.0
10   *
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS,
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   * See the License for the specific language governing permissions and
15   * limitations under the License.
16   */
17  package org.apache.commons.collections4.bloomfilter;
18  
19  import static org.junit.jupiter.api.Assertions.assertEquals;
20  import static org.junit.jupiter.api.Assertions.assertNotEquals;
21  import static org.junit.jupiter.api.Assertions.assertThrows;
22  
23  import java.util.Arrays;
24  
25  import org.junit.jupiter.api.Test;
26  import org.junit.jupiter.params.ParameterizedTest;
27  import org.junit.jupiter.params.provider.CsvSource;
28  
29  /**
30   * Tests the {@link Shape} class.
31   */
32  public class ShapeTest {
33  
34      /*
35       * values from https://hur.st/bloomfilter/?n=5&p=.1&m=&k=
36       *
37       * n = 5
38       *
39       * p = 0.100375138 (1 in 10)
40       *
41       * m = 24 (3B)
42       *
43       * k = 3
44       */
45  
46      private final Shape shape = Shape.fromKM(3, 24);
47  
48      /**
49       * Tests that if the number of bits is less than 1 an exception is thrown
50       */
51      @Test
52      public void testBadNumberOfBits() {
53          assertThrows(IllegalArgumentException.class, () -> Shape.fromKM(5, 0));
54          assertThrows(IllegalArgumentException.class, () -> Shape.fromNM(5, 0));
55          assertThrows(IllegalArgumentException.class, () -> Shape.fromNMK(5, 0, 7));
56          assertThrows(IllegalArgumentException.class, () -> Shape.fromPMK(0.035, 0, 7));
57      }
58  
59      /**
60       * Tests that if the number of hash functions is less than 1 an exception is thrown.
61       */
62      @Test
63      public void testBadNumberOfHashFunctions() {
64          assertThrows(IllegalArgumentException.class, () -> Shape.fromKM(0, 7));
65          assertThrows(IllegalArgumentException.class, () -> Shape.fromNMK(5, 26, 0));
66          assertThrows(IllegalArgumentException.class, () -> Shape.fromPMK(0.35, 26, 0));
67          assertThrows(IllegalArgumentException.class, () -> Shape.fromNM(2, 1));
68      }
69  
70      /**
71       * Tests that if the number of items less than 1 an IllegalArgumentException is thrown.
72       */
73      @Test
74      public void testBadNumberOfItems() {
75          assertThrows(IllegalArgumentException.class, () -> Shape.fromNM(0, 24));
76          assertThrows(IllegalArgumentException.class, () -> Shape.fromNMK(0, 24, 5));
77          assertThrows(IllegalArgumentException.class, () -> Shape.fromNP(0, 0.02));
78      }
79  
80      /**
81       * Tests that if the calculated probability is greater than or equal to 1 an IllegalArgumentException is thrown
82       */
83      @Test
84      public void testBadProbability() {
85          assertThrows(IllegalArgumentException.class, () -> Shape.fromNMK(4000, 8, 1));
86          assertThrows(IllegalArgumentException.class, () -> Shape.fromNP(10, 0.0));
87          assertThrows(IllegalArgumentException.class, () -> Shape.fromNP(10, 1.0));
88          assertThrows(IllegalArgumentException.class, () -> Shape.fromNP(10, Double.NaN));
89          assertThrows(IllegalArgumentException.class, () -> Shape.fromNP(10, Double.POSITIVE_INFINITY));
90          assertThrows(IllegalArgumentException.class, () -> Shape.fromNP(10, Double.NEGATIVE_INFINITY));
91      }
92  
93      /**
94       * Test equality of shape.
95       */
96      @ParameterizedTest
97      @CsvSource({
98          "3, 24",
99          "1, 24",
100         "1, 1",
101         "13, 124",
102         "13, 224",
103     })
104     public void testEqualsAndHashCode(final int k, final int m) {
105         final Shape shape1 = Shape.fromKM(k, m);
106         assertEquals(shape1, shape1);
107         assertEquals(Arrays.hashCode(new int[] {m, k}), shape1.hashCode(),
108             "Doesn't match Arrays.hashCode(new int[] {m, k})");
109         assertNotEquals(shape1, null);
110         assertNotEquals(shape1, "text");
111         assertNotEquals(shape1, Integer.valueOf(3));
112         assertNotEquals(shape1, Shape.fromKM(k, m + 1));
113         assertNotEquals(shape1, Shape.fromKM(k + 1, m));
114 
115         // Test this is reproducible
116         final Shape shape2 = Shape.fromKM(k, m);
117         assertEquals(shape1, shape2);
118         assertEquals(shape1.hashCode(), shape2.hashCode());
119     }
120 
121     /*
122      * values from https://hur.st/bloomfilter/?n=5&p=.1&m=&k=
123      *
124      * n = 5
125      *
126      * p = 0.100375138 (1 in 10)
127      *
128      * m = 24 (3B)
129      *
130      * k = 3
131      */
132 
133     @Test
134     public void testEstimateN() {
135         for (int i = 0; i < 24; i++) {
136             final double c = i;
137             final double expected = -(24.0 / 3.0) * Math.log1p(-c / 24.0);
138             assertEquals(expected, shape.estimateN(i), "Error on " + i);
139         }
140 
141         assertEquals(Double.POSITIVE_INFINITY, shape.estimateN(24));
142 
143         assertEquals(Double.NaN, shape.estimateN(25));
144     }
145 
146     /**
147      * Tests that if the number of bits less than 1 an IllegalArgumentException is thrown.
148      */
149     @Test
150     public void testFromKM() {
151         assertThrows(IllegalArgumentException.class, () -> Shape.fromKM(5, 0));
152         assertThrows(IllegalArgumentException.class, () -> Shape.fromKM(0, 5));
153     }
154 
155     /**
156      * Tests that the number of items and number of bits is passed the other values are calculated correctly.
157      */
158     @Test
159     public void testFromNM() {
160         /*
161          * values from https://hur.st/bloomfilter/?n=5&m=24
162          */
163         final Shape shape = Shape.fromNM(5, 24);
164 
165         assertEquals(24, shape.getNumberOfBits());
166         assertEquals(3, shape.getNumberOfHashFunctions());
167         assertEquals(0.100375138, shape.getProbability(5), 0.000001);
168 
169         assertThrows(IllegalArgumentException.class, () -> Shape.fromNM(5, 0));
170         assertThrows(IllegalArgumentException.class, () -> Shape.fromNM(0, 5));
171     }
172 
173     /**
174      * Tests that when the number of items, number of bits and number of hash functions is passed the values are
175      * calculated correctly.
176      */
177     @Test
178     public void testFromNMK() {
179         /*
180          * values from https://hur.st/bloomfilter/?n=5&m=24&k=4
181          */
182         final Shape shape = Shape.fromNMK(5, 24, 4);
183 
184         assertEquals(24, shape.getNumberOfBits());
185         assertEquals(4, shape.getNumberOfHashFunctions());
186         assertEquals(0.102194782, shape.getProbability(5), 0.000001);
187 
188         assertThrows(IllegalArgumentException.class,
189                 () -> Shape.fromNMK(Integer.MAX_VALUE, Integer.MAX_VALUE, Integer.MAX_VALUE));
190         assertThrows(IllegalArgumentException.class, () -> Shape.fromNMK(5, 5, 0));
191         assertThrows(IllegalArgumentException.class, () -> Shape.fromNMK(5, 0, 5));
192         assertThrows(IllegalArgumentException.class, () -> Shape.fromNMK(0, 5, 5));
193     }
194 
195     /**
196      * Tests the calculated values of calling the constructor with the probability, number of bits and number of hash
197      * functions.
198      */
199     @Test
200     public void testFromNP() {
201         /*
202          * values from https://hur.st/bloomfilter/?n=5&p=.1&m=24&k=3
203          */
204         final double probability = 1.0 / 2000000;
205         final Shape shape = Shape.fromNP(10, probability);
206 
207         assertEquals(302, shape.getNumberOfBits());
208         assertEquals(21, shape.getNumberOfHashFunctions());
209 
210         assertThrows(IllegalArgumentException.class, () -> Shape.fromNP(Integer.MAX_VALUE, Math.nextDown(1.0)));
211         assertThrows(IllegalArgumentException.class, () -> Shape.fromNP(0, probability));
212         assertThrows(IllegalArgumentException.class, () -> Shape.fromNP(5, 0.0));
213         assertThrows(IllegalArgumentException.class, () -> Shape.fromNP(Integer.MAX_VALUE, Math.nextUp(0.0)));
214         // Test that if calculated number of bits is greater than Integer.MAX_VALUE an
215         // IllegalArgumentException is thrown.
216         assertThrows(IllegalArgumentException.class, () -> Shape.fromNP(Integer.MAX_VALUE, 0.1));
217     }
218 
219     /**
220      * Tests the calculated values of calling the constructor with the probability, number of bits and number of hash
221      * functions.
222      */
223     @Test
224     public void testFromPMK() {
225         /*
226          * values from https://hur.st/bloomfilter/?n=5&p=.1&m=24&k=3
227          */
228         Shape shape = Shape.fromPMK(0.1, 24, 3);
229 
230         assertEquals(24, shape.getNumberOfBits());
231         assertEquals(3, shape.getNumberOfHashFunctions());
232         assertEquals(0.100375138, shape.getProbability(5), 0.000001);
233 
234         assertThrows(IllegalArgumentException.class,
235                 () -> Shape.fromPMK(Math.nextDown(1.0), Integer.MAX_VALUE, Integer.MAX_VALUE));
236         shape = Shape.fromPMK(Math.nextUp(0.0), 5, 5);
237         assertEquals(1.0, shape.getProbability(Integer.MAX_VALUE));
238         assertThrows(IllegalArgumentException.class, () -> Shape.fromPMK(Math.nextDown(1.0), 5, 5));
239         assertThrows(IllegalArgumentException.class, () -> Shape.fromPMK(0.0, 5, 5));
240         assertThrows(IllegalArgumentException.class, () -> Shape.fromPMK(0.5, 0, 5));
241         assertThrows(IllegalArgumentException.class, () -> Shape.fromPMK(0.5, 5, 0));
242     }
243 
244     @Test
245     public void testGetProbability() {
246         for (int i = 0; i <= 24; i++) {
247             final double expected = Math.pow(-Math.expm1(-3.0 * i / 24), 3);
248             assertEquals(expected, shape.getProbability(i), "error at " + i);
249         }
250 
251         assertEquals(0.0, shape.getProbability(0), 0.0);
252 
253         assertThrows(IllegalArgumentException.class, () -> shape.getProbability(-1));
254     }
255 
256     @Test
257     public void testIsSparse() {
258         final int functions = 1; // Ignored
259         for (int i = 1; i <= 3; i++) {
260             final int bits = i * Long.SIZE;
261             final Shape shape = Shape.fromKM(functions, bits);
262             for (int n = 0; n <= bits; n++) {
263                 final int c = n;
264                 // is sparse when number of bits stored as integers is less than 2 times the
265                 // number of bitmaps
266                 assertEquals(n * Integer.SIZE <= Math.ceil((double) bits / Long.SIZE) * Long.SIZE,
267                         shape.isSparse(n), () -> String.format("n=%d : bits=%d", c, bits));
268             }
269         }
270     }
271 
272     /**
273      * Tests that the probability is calculated correctly.
274      */
275     @Test
276     public void testProbability() {
277         final Shape shape = Shape.fromNMK(5, 24, 3);
278         assertEquals(24, shape.getNumberOfBits());
279         assertEquals(3, shape.getNumberOfHashFunctions());
280         assertEquals(0.100375138, shape.getProbability(5), 0.000001);
281     }
282 
283     @Test
284     public void testToString() {
285         assertEquals("Shape[k=3 m=5]", Shape.fromKM(3, 5).toString());
286     }
287 }