diff --git a/src/main/java/org/apache/commons/math4/util/Incrementor.java b/src/main/java/org/apache/commons/math4/util/Incrementor.java index 1f10cacdf..0898ec504 100644 --- a/src/main/java/org/apache/commons/math4/util/Incrementor.java +++ b/src/main/java/org/apache/commons/math4/util/Incrementor.java @@ -28,7 +28,10 @@ import org.apache.commons.math4.exception.NullArgumentException; * select which exception must be thrown. * * @since 3.0 + * + * @deprecated Use {@link IntegerSequence.Incrementor} instead. */ +@Deprecated public class Incrementor { /** * Upper limit for the counter. diff --git a/src/main/java/org/apache/commons/math4/util/IntegerSequence.java b/src/main/java/org/apache/commons/math4/util/IntegerSequence.java new file mode 100644 index 000000000..82e7c97b9 --- /dev/null +++ b/src/main/java/org/apache/commons/math4/util/IntegerSequence.java @@ -0,0 +1,324 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.math4.util; + +import java.util.Iterator; +import org.apache.commons.math4.exception.MaxCountExceededException; +import org.apache.commons.math4.exception.NullArgumentException; +import org.apache.commons.math4.exception.MathUnsupportedOperationException; +import org.apache.commons.math4.exception.NotStrictlyPositiveException; +import org.apache.commons.math4.exception.ZeroException; + +/** + * Provides a sequence of integers. + * + * @since 3.6 + */ +public class IntegerSequence { + /** + * Utility class contains only static methods. + */ + private IntegerSequence() {} + + /** + * Creates a sequence {@code [start .. end]}. + * It calls {@link #range(int,int,int) range(start, end, 1)}. + * + * @param start First value of the range. + * @param end Last value of the range. + * @return a range. + */ + public static Iterable range(int start, + int end) { + return range(start, end, 1); + } + + /** + * Creates a sequence \( a_i, i < 0 <= n \) + * where \( a_i = start + i * step \) + * and \( n \) is such that \( a_n <= max \) and \( a_{n+1} > max \). + * + * @param start First value of the range. + * @param max Last value of the range that satisfies the above + * construction rule. + * @param step Increment. + * @return a range. + */ + public static Iterable range(final int start, + final int max, + final int step) { + return new Iterable() { + /** {@inheritDoc} */ + @Override + public Iterator iterator() { + return Incrementor.create() + .withStart(start) + .withMaximalCount(max + (step > 0 ? 1 : -1)) + .withIncrement(step); + } + }; + } + + /** + * Utility that increments a counter until a maximum is reached, at + * which point, the instance will by default throw a + * {@link MaxCountExceededException}. + * However, the user is able to override this behaviour by defining a + * custom {@link MaxCountExceededCallback callback}, in order to e.g. + * select which exception must be thrown. + */ + public static class Incrementor implements Iterator { + /** Default callback. */ + private static final MaxCountExceededCallback CALLBACK + = new MaxCountExceededCallback() { + /** {@inheritDoc} */ + @Override + public void trigger(int max) throws MaxCountExceededException { + throw new MaxCountExceededException(max); + } + }; + + /** Initial value the counter. */ + private final int init; + /** Upper limit for the counter. */ + private final int maximalCount; + /** Increment. */ + private final int increment; + /** Function called at counter exhaustion. */ + private final MaxCountExceededCallback maxCountCallback; + /** Current count. */ + private int count = 0; + + /** + * Defines a method to be called at counter exhaustion. + * The {@link #trigger(int) trigger} method should usually throw an exception. + */ + public interface MaxCountExceededCallback { + /** + * Function called when the maximal count has been reached. + * + * @param maximalCount Maximal count. + * @throws MaxCountExceededException at counter exhaustion + */ + void trigger(int maximalCount) throws MaxCountExceededException; + } + + /** + * Creates an incrementor. + * The counter will be exhausted either when {@code max} is reached + * or when {@code nTimes} increments have been performed. + * + * @param start Initial value. + * @param max Maximal count. + * @param step Increment. + * @param nTimes Number of increments. + * @param cb Function to be called when the maximal count has been reached. + * @throws NullArgumentException if {@code cb} is {@code null}. + */ + private Incrementor(int start, + int max, + int step, + MaxCountExceededCallback cb) + throws NullArgumentException { + if (cb == null) { + throw new NullArgumentException(); + } + this.init = start; + this.maximalCount = max; + this.increment = step; + this.maxCountCallback = cb; + this.count = start; + } + + /** + * Factory method that creates a default instance. + * The initial and maximal values are set to 0. + * For the new instance to be useful, the maximal count must be set + * by calling {@link #withMaximalCount(int) withMaximalCount}. + * + * @return an new instance. + */ + public static Incrementor create() { + return new Incrementor(0, 0, 1, CALLBACK); + } + + /** + * Creates a new instance with a given initial value. + * The counter is reset to the initial value. + * + * @param start Initial value of the counter. + * @return a new instance. + */ + public Incrementor withStart(int start) { + return new Incrementor(start, + this.maximalCount, + this.increment, + this.maxCountCallback); + } + + /** + * Creates a new instance with a given maximal count. + * The counter is reset to the initial value. + * + * @param max Maximal count. + * @return a new instance. + */ + public Incrementor withMaximalCount(int max) { + return new Incrementor(this.init, + max, + this.increment, + this.maxCountCallback); + } + + /** + * Creates a new instance with a given increment. + * The counter is reset to the initial value. + * + * @param step Increment. + * @return a new instance. + */ + public Incrementor withIncrement(int step) { + if (step == 0) { + throw new ZeroException(); + } + return new Incrementor(this.init, + this.maximalCount, + step, + this.maxCountCallback); + } + + /** + * Creates a new instance with a given callback. + * The counter is reset to the initial value. + * + * @param cb Callback to be called at counter exhaustion. + * @return a new instance. + */ + public Incrementor withCallback(MaxCountExceededCallback cb) { + return new Incrementor(this.init, + this.maximalCount, + this.increment, + cb); + } + + /** + * Gets the upper limit of the counter. + * + * @return the counter upper limit. + */ + public int getMaximalCount() { + return maximalCount; + } + + /** + * Gets the current count. + * + * @return the current count. + */ + public int getCount() { + return count; + } + + /** + * Checks whether incrementing the counter {@code nTimes} is allowed. + * + * @return {@code false} if calling {@link #increment()} + * will trigger a {@code MaxCountExceededException}, + * {@code true} otherwise. + */ + public boolean canIncrement() { + return canIncrement(1); + } + + /** + * Checks whether incrementing the counter several times is allowed. + * + * @param nTimes Number of increments. + * @return {@code false} if calling {@link #increment(int) + * increment(nTimes)} would call the {@link MaxCountExceededCallback callback} + * {@code true} otherwise. + */ + public boolean canIncrement(int nTimes) { + final int finalCount = count + nTimes * increment; + return increment < 0 ? + finalCount > maximalCount : + finalCount < maximalCount; + } + + /** + * Performs multiple increments. + * + * @param nTimes Number of increments. + * @throws MaxCountExceededException at counter exhaustion. + * @throws NotStrictlyPositiveException if {@code nTimes <= 0}. + * + * @see #increment() + */ + public void increment(int nTimes) throws MaxCountExceededException { + if (nTimes <= 0) { + throw new NotStrictlyPositiveException(nTimes); + } + + if (!canIncrement(0)) { + maxCountCallback.trigger(maximalCount); + } + count += nTimes * increment; + } + + /** + * Adds the increment value to the current iteration count. + * At counter exhaustion, this method will call the + * {@link MaxCountExceededCallback#trigger(int) trigger} method of the + * callback object passed to the + * {@link #withCallback(MaxCountExceededCallback)} method. + * If not explictly set, a default callback is used that will throw + * a {@code MaxCountExceededException}. + * + * @throws MaxCountExceededException at counter exhaustion, unless a + * custom {@link MaxCountExceededCallback callback} has been set. + * + * @see #increment(int) + */ + public void increment() throws MaxCountExceededException { + increment(1); + } + + /** {@inheritDoc} */ + @Override + public boolean hasNext() { + return canIncrement(0); + } + + /** {@inheritDoc} */ + @Override + public Integer next() { + final int value = count; + increment(); + return value; + } + + /** + * Not applicable. + * + * @throws MathUnsupportedOperationException + */ + @Override + public void remove() { + throw new MathUnsupportedOperationException(); + } + } +} diff --git a/src/test/java/org/apache/commons/math4/util/IntegerSequenceTest.java b/src/test/java/org/apache/commons/math4/util/IntegerSequenceTest.java new file mode 100644 index 000000000..d1a67646d --- /dev/null +++ b/src/test/java/org/apache/commons/math4/util/IntegerSequenceTest.java @@ -0,0 +1,248 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with this + * work for additional information regarding copyright ownership. The ASF + * licenses this file to You under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law + * or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ +package org.apache.commons.math4.util; + +import java.util.List; +import java.util.ArrayList; +import org.apache.commons.math4.exception.MaxCountExceededException; +import org.apache.commons.math4.exception.TooManyEvaluationsException; +import org.apache.commons.math4.exception.NotStrictlyPositiveException; +import org.apache.commons.math4.exception.ZeroException; +import org.junit.Assert; +import org.junit.Test; + +/** + * Tests for {@link IntegerSequence} and {@link IntegerSequence#Incrementor}. + */ +public class IntegerSequenceTest { + @Test + public void testIncreasingRange() { + final int start = 1; + final int max = 7; + final int step = 2; + + final List seq = new ArrayList(); + for (Integer i : IntegerSequence.range(start, max, step)) { + seq.add(i); + } + + Assert.assertEquals(4, seq.size()); + for (int i = 0; i < seq.size(); i++) { + Assert.assertEquals(start + i * step, seq.get(i).intValue()); + } + } + + @Test + public void testIncreasingRangeNegativeEnd() { + final int start = -10; + final int max = -1; + final int step = 2; + + final List seq = new ArrayList(); + for (Integer i : IntegerSequence.range(start, max, step)) { + seq.add(i); + } + + Assert.assertEquals(5, seq.size()); + for (int i = 0; i < seq.size(); i++) { + Assert.assertEquals(start + i * step, seq.get(i).intValue()); + } + } + + @Test + public void testDecreasingRange() { + final int start = 10; + final int max = -8; + final int step = -3; + + final List seq = new ArrayList(); + for (Integer i : IntegerSequence.range(start, max, step)) { + seq.add(i); + } + + Assert.assertEquals(7, seq.size()); + for (int i = 0; i < seq.size(); i++) { + Assert.assertEquals(start + i * step, seq.get(i).intValue()); + } + } + + @Test + public void testSingleElementRange() { + final int start = 1; + final int max = 1; + final int step = -1; + + final List seq = new ArrayList(); + for (Integer i : IntegerSequence.range(start, max, step)) { + seq.add(i); + } + + Assert.assertEquals(1, seq.size()); + Assert.assertEquals(start, seq.get(0).intValue()); + } + + @Test + public void testBasicRange() { + final int start = -2; + final int end = 4; + + final List seq = new ArrayList(); + for (Integer i : IntegerSequence.range(start, end)) { + seq.add(i); + } + + for (int i = start; i <= end; i++) { + Assert.assertEquals(i, seq.get(i - start).intValue()); + } + } + + @Test + public void testEmptyRange() { + final int start = 2; + final int end = 1; + + final List seq = new ArrayList(); + for (Integer i : IntegerSequence.range(start, end)) { + seq.add(i); + } + + Assert.assertEquals(0, seq.size()); + } + + @Test + public void testEmptyRangeNegativeStart() { + final int start = -2; + final int max = -1; + final int step = -1; + + final List seq = new ArrayList(); + for (Integer i : IntegerSequence.range(start, max, step)) { + seq.add(i); + } + + Assert.assertEquals(0, seq.size()); + } + + @Test(expected=MaxCountExceededException.class) + public void testIncrementorCountExceeded() { + final int start = 1; + final int max = 7; + final int step = 2; + + final IntegerSequence.Incrementor inc = + IntegerSequence.Incrementor.create() + .withStart(start) + .withMaximalCount(max) + .withIncrement(step); + + Assert.assertTrue(inc.canIncrement(2)); + Assert.assertFalse(inc.canIncrement(3)); + + while (true) { + inc.increment(); + } + } + + @Test + public void testCanIncrementZeroTimes() { + final int start = 1; + final int max = 2; + final int step = 1; + + final IntegerSequence.Incrementor inc + = IntegerSequence.Incrementor.create() + .withStart(start) + .withMaximalCount(max) + .withIncrement(step); + + Assert.assertTrue(inc.canIncrement(0)); + } + + @Test(expected=NotStrictlyPositiveException.class) + public void testIncrementZeroTimes() { + final int start = 1; + final int max = 2; + final int step = 1; + + final IntegerSequence.Incrementor inc + = IntegerSequence.Incrementor.create() + .withStart(start) + .withMaximalCount(max) + .withIncrement(step); + + inc.increment(0); + } + + @Test(expected=ZeroException.class) + public void testIncrementZeroStep() { + final int step = 0; + + final IntegerSequence.Incrementor inc + = IntegerSequence.Incrementor.create() + .withIncrement(step); + } + + @Test + public void testIteratorZeroElement() { + final int start = 1; + final int max = 1; + final int step = 1; + + final IntegerSequence.Incrementor inc + = IntegerSequence.Incrementor.create() + .withStart(start) + .withMaximalCount(max) + .withIncrement(step); + + Assert.assertFalse(inc.hasNext()); + try { + inc.increment(); + Assert.fail("exception expected"); + } catch (MaxCountExceededException e) { + // Expected. + } + } + + @Test(expected=TooManyEvaluationsException.class) + public void testIncrementorAlternateException() { + final int start = 1; + final int max = 2; + final int step = 1; + + final IntegerSequence.Incrementor.MaxCountExceededCallback cb + = new IntegerSequence.Incrementor.MaxCountExceededCallback() { + /** {@inheritDoc} */ + public void trigger(int max) { + throw new TooManyEvaluationsException(max); + } + }; + + final IntegerSequence.Incrementor inc + = IntegerSequence.Incrementor.create() + .withStart(start) + .withMaximalCount(max) + .withIncrement(step) + .withCallback(cb); + + try { + // One call must succeed. + inc.increment(); + } catch (RuntimeException e) { + Assert.fail("unexpected exception"); + } + + // Second call must fail. + inc.increment(); + } +}