diff --git a/src/main/java/org/apache/commons/math/util/MultidimensionalCounter.java b/src/main/java/org/apache/commons/math/util/MultidimensionalCounter.java new file mode 100644 index 000000000..c08bb6b3b --- /dev/null +++ b/src/main/java/org/apache/commons/math/util/MultidimensionalCounter.java @@ -0,0 +1,299 @@ +/* + * 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.math.util; + +import java.util.Arrays; +import org.apache.commons.math.exception.DimensionMismatchException; +import org.apache.commons.math.exception.OutOfRangeException; +import org.apache.commons.math.exception.NotStrictlyPositiveException; + +/** + * Converter between unidimensional storage structure and multidimensional + * conceptual structure. + * This utility will convert from indices in a multidimensional structure + * to the corresponding index in a one-dimensional array. For example, + * assuming that the ranges (in 3 dimensions) of indices are 2, 4 and 3, + * the following correspondences, between 3-tuples indices and unidimensional + * indices, will hold: + * + */ +public class MultidimensionalCounter implements Iterable { + /** + * Number of dimensions. + */ + private final int dimension; + /** + * Offset for each dimension. + */ + private final int[] uniCounterOffset; + /** + * Counter sizes. + */ + private final int[] size; + /** + * Total number of (one-dimensional) slots. + */ + private final int totalSize; + /** + * Index of last dimension. + */ + private final int last; + + /** + * Perform iteration over the multidimensional counter. + */ + public class Iterator implements java.util.Iterator { + /** + * Multidimensional counter. + */ + private final int[] counter = new int[dimension]; + /** + * Unidimensional counter. + */ + private int count = -1; + + /** + * Create an iterator (see {@link MultidimensionalCounter#iterator()}. + */ + Iterator() { + counter[last] = -1; + } + + /** + * {@inheritDoc} + */ + public boolean hasNext() { + for (int i = 0; i < dimension; i++) { + if (counter[i] != size[i] - 1) { + return true; + } + } + return false; + } + + /** + * @return the unidimensional count after the counter has been + * incremented by {@code 1}. + */ + public Integer next() { + for (int i = last; i >= 0; i--) { + if (counter[i] == size[i] - 1) { + counter[i] = 0; + } else { + ++counter[i]; + break; + } + } + + return ++count; + } + + /** + * Get the current unidimensional counter slot. + * + * @return the index within the unidimensionl counter. + */ + public int getCount() { + return count; + } + /** + * Get the current multidimensional counter slots. + * + * @return the indices within the multidimensional counter. + */ + public int[] getCounts() { + return Arrays.copyOf(counter, dimension); + } + + /** + * Get the current count in the selected dimension. + * + * @param dim Dimension index. + * @return the count at the corresponding index for the current state + * of the iterator. + * @throws IndexOutOfBoundsException if {@code index} is not in the + * correct interval (as defined by the length of the argument in the + * {@link MultidimensionalCounter#MultidimensionalCounter(int[]) + * constructor of the enclosing class}). + */ + public int getCount(int dim) { + return counter[dim]; + } + + /** + * @throws UnsupportedOperationException. + */ + public void remove() { + throw new UnsupportedOperationException(); + } + } + + /** + * Create a counter. + * + * @param size Counter sizes (number of slots in each dimension). + * @throws {@link NotStrictlyPositiveException} if one of the sizes is + * negative or zero. + */ + public MultidimensionalCounter(int ... size) { + dimension = size.length; + this.size = Arrays.copyOf(size, dimension); + + uniCounterOffset = new int[dimension]; + + last = dimension - 1; + int tS = size[last]; + for (int i = 0; i < last; i++) { + int count = 1; + for (int j = i + 1; j < dimension; j++) { + count *= size[j]; + } + uniCounterOffset[i] = count; + tS *= size[i]; + } + uniCounterOffset[last] = 0; + + if (tS <= 0) { + throw new NotStrictlyPositiveException(tS); + } + + totalSize = tS; + } + + /** + * Create an iterator over this counter. + * + * @return the iterator. + */ + public Iterator iterator() { + return new Iterator(); + } + + /** + * Get the number of dimensions of the multidimensional counter. + * + * @return the number of dimensions. + */ + public int getDimension() { + return dimension; + } + + /** + * Convert to multidimensional counter. + * + * @param index Index in unidimensional counter. + * @returns the multidimensional counts. + * @throws {@link OutOfRangeException} if {@code index} is not between + * {@code 0} and the value returned by {@link #getSize()} (excluded). + */ + public int[] getCounts(int index) { + if (index < 0 + || index >= totalSize) { + throw new OutOfRangeException(index, 0, totalSize); + } + + final int[] indices = new int[dimension]; + + int count = 0; + for (int i = 0; i < last; i++) { + int idx = 0; + final int offset = uniCounterOffset[i]; + while (count <= index) { + count += offset; + ++idx; + } + --idx; + count -= offset; + indices[i] = idx; + } + + int idx = 1; + while (count < index) { + count += idx; + ++idx; + } + --idx; + indices[last] = idx; + + return indices; + } + + /** + * Convert to unidimensional counter. + * + * @param c Indices in multidimensional counter. + * @return the index within the unidimensionl counter. + * @throws {@link DimensionMismatchException} if the size of {@code c} + * does not match the size of the array given in the contructor. + * @throws {@link OutOfRangeException} if a value of {@code c} is not in + * the range of the corresponding dimension, as defined in the + * {@link #MultidimensionalCounter(int[]) constructor}. + */ + public int getCount(int ... c) { + if (c.length != dimension) { + throw new DimensionMismatchException(c.length, dimension); + } + int count = 0; + for (int i = 0; i < dimension; i++) { + final int index = c[i]; + if (index < 0 + || index >= size[i]) { + throw new OutOfRangeException(index, 0, size[i] - 1); + } + count += uniCounterOffset[i] * c[i]; + } + return count + c[last]; + } + + /** + * Get the total number of elements. + * + * @return the total size of the unidimensional counter. + */ + public int getSize() { + return totalSize; + } + /** + * Get the number of multidimensional counter slots in each dimension. + * + * @return the sizes of the multidimensional counter in each dimension. + */ + public int[] getSizes() { + return Arrays.copyOf(size, dimension); + } + + /** + * {@inheritDoc} + */ + public String toString() { + final StringBuilder sb = new StringBuilder(); + for (int i = 0; i < dimension; i++) { + sb.append("[").append(getCount(i)).append("]"); + } + return sb.toString(); + } +} diff --git a/src/site/xdoc/changes.xml b/src/site/xdoc/changes.xml index b37ccd8de..38341a3ff 100644 --- a/src/site/xdoc/changes.xml +++ b/src/site/xdoc/changes.xml @@ -52,6 +52,9 @@ The type attribute can be add,update,fix,remove. If the output is not quite correct, check for invisible trailing spaces! --> + + Created "MultidimensionalCounter" class. + Created package "exception" to contain the new exceptions hierarchy. diff --git a/src/site/xdoc/userguide/utilities.xml b/src/site/xdoc/userguide/utilities.xml index 342fbb93d..5c077bf4f 100644 --- a/src/site/xdoc/userguide/utilities.xml +++ b/src/site/xdoc/userguide/utilities.xml @@ -152,7 +152,7 @@ MathUtils utility class. MathUtils currently includes methods to compute the following:
  • - Binomial coeffiecients -- "n choose k" available as an (exact) long value, + Binomial coefficients -- "n choose k" available as an (exact) long value, binomialCoefficient(int, int) for small n, k; as a double, binomialCoefficientDouble(int, int) for larger values; and in a "super-sized" version, binomialCoefficientLog(int, int) @@ -182,6 +182,13 @@

    + + The + MultidimensionalCounter is a utility class that converts a set of indices + (identifying points in a multidimensional space) to a single index (e.g. identifying + a location in a one-dimensional array. + + diff --git a/src/test/java/org/apache/commons/math/util/MultidimensionalCounterTest.java b/src/test/java/org/apache/commons/math/util/MultidimensionalCounterTest.java new file mode 100644 index 000000000..827030df3 --- /dev/null +++ b/src/test/java/org/apache/commons/math/util/MultidimensionalCounterTest.java @@ -0,0 +1,169 @@ +/* + * 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.math.util; + +import org.apache.commons.math.exception.DimensionMismatchException; +import org.apache.commons.math.exception.OutOfRangeException; +import org.apache.commons.math.exception.NotStrictlyPositiveException; +import org.junit.Assert; +import org.junit.Test; + +/** + * + */ +public class MultidimensionalCounterTest { + @Test + public void testPreconditions() { + MultidimensionalCounter c; + + try { + c = new MultidimensionalCounter(0, 1); + Assert.fail("NotStrictlyPositiveException expected"); + } catch (NotStrictlyPositiveException e) { + // Expected. + } + try { + c = new MultidimensionalCounter(2, 0); + Assert.fail("NotStrictlyPositiveException expected"); + } catch (NotStrictlyPositiveException e) { + // Expected. + } + try { + c = new MultidimensionalCounter(-1, 1); + Assert.fail("NotStrictlyPositiveException expected"); + } catch (NotStrictlyPositiveException e) { + // Expected. + } + + c = new MultidimensionalCounter(2, 3); + try { + c.getCount(1, 1, 1); + Assert.fail("DimensionMismatchException expected"); + } catch (DimensionMismatchException e) { + // Expected. + } + try { + c.getCount(3, 1); + Assert.fail("OutOfRangeException expected"); + } catch (OutOfRangeException e) { + // Expected. + } + try { + c.getCount(0, -1); + Assert.fail("OutOfRangeException expected"); + } catch (OutOfRangeException e) { + // Expected. + } + try { + c.getCounts(-1); + Assert.fail("OutOfRangeException expected"); + } catch (OutOfRangeException e) { + // Expected. + } + try { + c.getCounts(6); + Assert.fail("OutOfRangeException expected"); + } catch (OutOfRangeException e) { + // Expected. + } + } + + @Test + public void testIteratorPreconditions() { + MultidimensionalCounter.Iterator iter = (new MultidimensionalCounter(2, 3)).iterator(); + try { + iter.getCount(-1); + Assert.fail("IndexOutOfBoundsException expected"); + } catch (IndexOutOfBoundsException e) { + // Expected. + } + try { + iter.getCount(2); + Assert.fail("IndexOutOfBoundsException expected"); + } catch (IndexOutOfBoundsException e) { + // Expected. + } + } + + @Test + public void testMulti2UniConversion() { + final MultidimensionalCounter c = new MultidimensionalCounter(2, 4, 5); + Assert.assertEquals(c.getCount(1, 2, 3), 33); + } + + @Test + public void testAccessors() { + final int[] originalSize = new int[] {2, 6, 5}; + final MultidimensionalCounter c = new MultidimensionalCounter(originalSize); + final int nDim = c.getDimension(); + Assert.assertEquals(nDim, originalSize.length); + + final int[] size = c.getSizes(); + for (int i = 0; i < nDim; i++) { + Assert.assertEquals(originalSize[i], size[i]); + } + } + + @Test + public void testIterationConsistency() { + final MultidimensionalCounter c = new MultidimensionalCounter(2, 3, 2); + final int[][] expected = new int[][] { + { 0, 0, 0 }, + { 0, 0, 1 }, + { 0, 1, 0 }, + { 0, 1, 1 }, + { 0, 2, 0 }, + { 0, 2, 1 }, + { 1, 0, 0 }, + { 1, 0, 1 }, + { 1, 1, 0 }, + { 1, 1, 1 }, + { 1, 2, 0 }, + { 1, 2, 1 } + }; + + final int totalSize = c.getSize(); + final int nDim = c.getDimension(); + final MultidimensionalCounter.Iterator iter = c.iterator(); + for (int i = 0; i < totalSize; i++) { + if (!iter.hasNext()) { + Assert.fail("Too short"); + } + final int uniDimIndex = iter.next(); + Assert.assertEquals("Wrong iteration at " + i, i, uniDimIndex); + + for (int dimIndex = 0; dimIndex < nDim; dimIndex++) { + Assert.assertEquals("Wrong multidimensional index for [" + i + "][" + dimIndex + "]", + expected[i][dimIndex], iter.getCount(dimIndex)); + } + + Assert.assertEquals("Wrong unidimensional index for [" + i + "]", + c.getCount(expected[i]), uniDimIndex); + + final int[] indices = c.getCounts(uniDimIndex); + for (int dimIndex = 0; dimIndex < nDim; dimIndex++) { + Assert.assertEquals("Wrong multidimensional index for [" + i + "][" + dimIndex + "]", + expected[i][dimIndex], indices[dimIndex]); + } + } + + if (iter.hasNext()) { + Assert.fail("Too long"); + } + } +}