diff --git a/commons-numbers-complex-arrays/pom.xml b/commons-numbers-complex-arrays/pom.xml new file mode 100644 index 000000000..1f2f2f4e5 --- /dev/null +++ b/commons-numbers-complex-arrays/pom.xml @@ -0,0 +1,64 @@ + + + + + commons-numbers-parent + org.apache.commons + 1.1-SNAPSHOT + + 4.0.0 + + commons-numbers-complex-arrays + Apache Commons Numbers Complex Arrays + + + + + org.apache.commons + commons-numbers-complex + + + + + + org.apache.commons.numbers.complex.arrays + + org.apache.commons.numbers.complex.arrays + + org.apache.commons.numbers.complex.arrays + + ${basedir}/.. + + + + + + + com.github.siom79.japicmp + japicmp-maven-plugin + + true + + + + + diff --git a/commons-numbers-complex-arrays/src/main/java/org/apache/commons/numbers/complex/arrays/ComplexList.java b/commons-numbers-complex-arrays/src/main/java/org/apache/commons/numbers/complex/arrays/ComplexList.java new file mode 100644 index 000000000..969832dcc --- /dev/null +++ b/commons-numbers-complex-arrays/src/main/java/org/apache/commons/numbers/complex/arrays/ComplexList.java @@ -0,0 +1,325 @@ +/* + * 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.numbers.complex.arrays; + +import org.apache.commons.numbers.complex.Complex; + +import java.util.AbstractList; +import java.util.Arrays; +import java.util.Collection; + +/** + * Resizable-double array implementation of the List interface. Implements all optional list operations, + * and permits all elements. In addition to implementing the List interface, + * this class provides methods to manipulate the size of the array that is used internally to store the list. + * + *

Each ComplexList instance has a capacity. The capacity is half the size of the double array used to store the elements + * in the list. As elements are added to an ComplexList, its capacity grows automatically. + * The complex number is stored using an interleaved format and so the maximum number of elements that may be added is + * approximately 2^30. This is half the maximum capacity of java.util.ArrayList. + * The memory usage is more efficient than using a List of Complex objects as the underlying numbers are not stored + * using instances of Complex.

+ * + *

An application can increase the capacity of an ComplexList instance before adding a large number of elements + * using the ensureCapacity operation. This may reduce the amount of incremental reallocation.

+ */ +public class ComplexList extends AbstractList { + + /** + * The maximum size of array to allocate. + * Ensuring Max capacity is even with additional space for vm array headers. + */ + private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 9; + + /** + * Max capacity for size of complex numbers in the list. + */ + private static final int MAX_CAPACITY = MAX_ARRAY_SIZE / 2; + + /** + * error in case of allocation above max capacity. + */ + private static final String OOM_ERROR_STRING = "cannot allocate capacity %s greater than max " + MAX_CAPACITY; + + /** + * Default initial capacity. + */ + private static final int DEFAULT_CAPACITY = 8; + + /** + * Size label message. + */ + private static final String SIZE_MSG = ", Size: "; + /** + * Index position label message. + */ + private static final String INDEX_MSG = "Index: "; + + /** + * The double array buffer into which the elements of the ComplexList are stored. + */ + private double[] realAndImagParts; + + /** + * Size of ComplexList. + */ + private int size; + + /** + * Constructs an empty list up to the specified capacity without a memory reallocation. + * + * @param capacity Capacity of list. + * @throws IllegalArgumentException if the {@code capacity} is greater than {@code MAX_CAPACITY}. + */ + public ComplexList(int capacity) { + if (capacity > MAX_CAPACITY) { + throw new IllegalArgumentException(String.format(OOM_ERROR_STRING, capacity)); + } + final int arrayLength = Math.max(DEFAULT_CAPACITY, capacity) * 2; + realAndImagParts = new double[arrayLength]; + } + + /** + * Constructs an empty list. + */ + public ComplexList() { + realAndImagParts = new double[DEFAULT_CAPACITY * 2]; + } + + /** + * {@inheritDoc} + */ + @Override + public int size() { + return size; + } + + /** + * Checks if the given index is in range. + * + * @param index Index of the element to range check. + * @throws IndexOutOfBoundsException if index isn't within the range. + */ + private void rangeCheck(int index) { + if (index >= size) { + throw new IndexOutOfBoundsException(outOfBoundsMsg(index)); + } + } + + /** + * A version of rangeCheck used by add and addAll. + * + * @param index Index of the element to range check. + * @throws IndexOutOfBoundsException if index isn't within the range of list. + */ + private void rangeCheckForInsert(int index) { + if (index < 0 || index > size) { + throw new IndexOutOfBoundsException(outOfBoundsMsg(index)); + } + } + + /** + * Gets the complex number \( (a + i b) \) at the indexed position of the list. + * + * {@inheritDoc} + * @return the complex number. + */ + @Override + public Complex get(int index) { + rangeCheck(index); + final int i = index << 1; + return Complex.ofCartesian(realAndImagParts[i], realAndImagParts[i + 1]); + } + + /** + * Replaces the element at the specified position in this list with the specified element's + * real and imaginary parts. No range checks are done. + * + * @param index Index of the element to replace. + * @param real Real part \( a \) of the complex number \( (a +ib) \). + * @param imaginary Imaginary part \( b \) of the complex number \( (a +ib) \). + */ + private void setNoRangeCheck(int index, double real, double imaginary) { + final int i = index << 1; + realAndImagParts[i] = real; + realAndImagParts[i + 1] = imaginary; + } + + /** + * {@inheritDoc} + * + * @param element Complex element to be set. + */ + @Override + public Complex set(int index, Complex element) { + rangeCheck(index); + final int i = index << 1; + final Complex oldValue = Complex.ofCartesian(realAndImagParts[i], realAndImagParts[i + 1]); + setNoRangeCheck(index, element.getReal(), element.getImaginary()); + return oldValue; + } + + /** + * Increases the capacity of this ComplexList instance, if necessary, to ensure that it can hold at + * least the number of elements specified by the minimum capacity argument. + * + * @param minCapacity Desired minimum capacity. + * @return the backing double array. + * @throws OutOfMemoryError if the {@code minCapacity} is greater than {@code MAX_ARRAY_SIZE}. + */ + private double[] ensureCapacityInternal(int minCapacity) { + modCount++; + final long minArrayCapacity = Integer.toUnsignedLong(minCapacity) << 1; + if (minArrayCapacity > MAX_ARRAY_SIZE) { + throw new OutOfMemoryError(String.format(OOM_ERROR_STRING, minArrayCapacity)); + } + final long oldArrayCapacity = realAndImagParts.length; + if (minArrayCapacity > oldArrayCapacity) { + long newArrayCapacity = oldArrayCapacity + (oldArrayCapacity >> 1); + // Round-odd up to even + newArrayCapacity += newArrayCapacity & 1; + + // Ensure minArrayCapacity <= newArrayCapacity <= MAX_ARRAY_SIZE + // Note: At this point minArrayCapacity <= MAX_ARRAY_SIZE + if (newArrayCapacity > MAX_ARRAY_SIZE) { + newArrayCapacity = MAX_ARRAY_SIZE; + } else if (newArrayCapacity < minArrayCapacity) { + newArrayCapacity = minArrayCapacity; + } + realAndImagParts = Arrays.copyOf(realAndImagParts, (int) newArrayCapacity); + } + return realAndImagParts; + } + + /** + * Increases the capacity of this ComplexList instance, if necessary, to ensure that it can hold at + * least an additional number of elements specified by the capacity argument. + * + * @param capacity Desired capacity. + */ + private void expand(int capacity) { + ensureCapacityInternal(size + capacity); + } + + /** + * {@inheritDoc} + * + * @param element Complex element to be appended to this list + */ + @Override + public boolean add(Complex element) { + double[] e = realAndImagParts; + if (size == (e.length >>> 1)) { + e = ensureCapacityInternal(size + 1); + } + final int i = size << 1; + e[i] = element.real(); + e[i + 1] = element.imag(); + size++; + return true; + } + + /** + * {@inheritDoc} + */ + @Override + public void add(int index, Complex element) { + rangeCheckForInsert(index); + double[] e = realAndImagParts; + if (size == e.length >>> 1) { + e = ensureCapacityInternal(size + 1); + } + final int i = index << 1; + final int s = size << 1; + System.arraycopy(e, i, e, i + 2, s - i); + e[i] = element.real(); + e[i + 1] = element.imag(); + size++; + } + + /** + * {@inheritDoc} + */ + @Override + public boolean addAll(Collection c) { + final int numNew = c.size(); + expand(numNew); + double[] realAndImgData = new double[numNew * 2]; + int i = 0; + for (final Complex val : c) { + realAndImgData[i++] = val.getReal(); + realAndImgData[i++] = val.getImaginary(); + } + final int s = size << 1; + System.arraycopy(realAndImgData, 0, realAndImagParts, s, realAndImgData.length); + size += numNew; + return numNew != 0; + } + + /** + * {@inheritDoc} + */ + @Override + public boolean addAll(int index, Collection c) { + rangeCheckForInsert(index); + final int numNew = c.size(); + final int numNew2 = numNew << 1; + expand(numNew); + final double[] realAndImgData = new double[numNew * 2]; + int i = 0; + for (final Complex val : c) { + realAndImgData[i++] = val.getReal(); + realAndImgData[i++] = val.getImaginary(); + } + final int numMoved = (size - index) * 2; + final int index2 = index << 1; + System.arraycopy(realAndImagParts, index2, realAndImagParts, index2 + numNew2, numMoved); + System.arraycopy(realAndImgData, 0, realAndImagParts, index2, realAndImgData.length); + size += numNew; + return numNew != 0; + } + + /** + * {@inheritDoc} + */ + @Override + public Complex remove(int index) { + rangeCheck(index); + modCount++; + final int i = index << 1; + final int s = size << 1; + final Complex oldValue = Complex.ofCartesian(realAndImagParts[i], realAndImagParts[i + 1]); + final int numMoved = s - i - 2; + if (numMoved > 0) { + System.arraycopy(realAndImagParts, i + 2, realAndImagParts, i, numMoved); + } + size--; + return oldValue; + } + + /** + * Constructs an IndexOutOfBoundsException detail message. + * + * @param index Index of the element. + * @return message detailing the exception. + */ + private String outOfBoundsMsg(int index) { + return INDEX_MSG + index + SIZE_MSG + size; + } + +} diff --git a/commons-numbers-complex-arrays/src/main/java/org/apache/commons/numbers/complex/arrays/package-info.java b/commons-numbers-complex-arrays/src/main/java/org/apache/commons/numbers/complex/arrays/package-info.java new file mode 100644 index 000000000..5897abaed --- /dev/null +++ b/commons-numbers-complex-arrays/src/main/java/org/apache/commons/numbers/complex/arrays/package-info.java @@ -0,0 +1,20 @@ +/* + * 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. + */ +/** + * Complex numbers. + */ +package org.apache.commons.numbers.complex.arrays; diff --git a/commons-numbers-complex-arrays/src/site/resources/profile.jacoco b/commons-numbers-complex-arrays/src/site/resources/profile.jacoco new file mode 100644 index 000000000..a12755f3b --- /dev/null +++ b/commons-numbers-complex-arrays/src/site/resources/profile.jacoco @@ -0,0 +1,17 @@ +# 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. +# ----------------------------------------------------------------------------- +# +# Empty file used to automatically trigger JaCoCo profile from commons parent pom diff --git a/commons-numbers-complex-arrays/src/site/resources/profile.japicmp b/commons-numbers-complex-arrays/src/site/resources/profile.japicmp new file mode 100644 index 000000000..f7c126cb4 --- /dev/null +++ b/commons-numbers-complex-arrays/src/site/resources/profile.japicmp @@ -0,0 +1,17 @@ +# 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. +# ----------------------------------------------------------------------------- +# +# Empty file used to automatically trigger JApiCmp profile from commons parent pom diff --git a/commons-numbers-complex-arrays/src/site/site.xml b/commons-numbers-complex-arrays/src/site/site.xml new file mode 100644 index 000000000..f6746edee --- /dev/null +++ b/commons-numbers-complex-arrays/src/site/site.xml @@ -0,0 +1,32 @@ + + + + + Apache Commons Numbers + /images/commons_numbers.small.png + /index.html + + + + + + + + + diff --git a/commons-numbers-complex-arrays/src/site/xdoc/index.xml b/commons-numbers-complex-arrays/src/site/xdoc/index.xml new file mode 100644 index 000000000..84e4b537e --- /dev/null +++ b/commons-numbers-complex-arrays/src/site/xdoc/index.xml @@ -0,0 +1,37 @@ + + + + + + + + Commons Numbers Complex Arrays + + + +
+

+ Commons Numbers provides number types and utilities. +

+

+ The "complex arrays" module contains utilities for working with complex number arrays and lists. +

+
+ + +
diff --git a/commons-numbers-complex-arrays/src/test/java/org/apache/commons/numbers/complex/arrays/ComplexListTest.java b/commons-numbers-complex-arrays/src/test/java/org/apache/commons/numbers/complex/arrays/ComplexListTest.java new file mode 100644 index 000000000..d25f52e5a --- /dev/null +++ b/commons-numbers-complex-arrays/src/test/java/org/apache/commons/numbers/complex/arrays/ComplexListTest.java @@ -0,0 +1,217 @@ +/* + * 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.numbers.complex.arrays; + +import org.apache.commons.numbers.complex.Complex; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.Function; +import java.util.stream.IntStream; + + +public class ComplexListTest { + + private static final int MAX_CAPACITY = (Integer.MAX_VALUE - 9) / 2; + + @Test + void testGetAndSetMethod() { + assertListOperation(list -> { + list.add(Complex.ofCartesian(42, 13)); + list.addAll(1, list); + list.addAll(list); + list.set(2, Complex.ofCartesian(200, 1)); + return list.get(2); + }); + } + + @Test + void testAddAndAddAll() { + List l1 = new ArrayList<>(); + List l2 = new ComplexList(); + assertListOperation(list -> list.add(Complex.ofCartesian(1, 2)), l1, l2); + assertListOperation(list -> { + list.add(1, Complex.ofCartesian(10, 20)); + return Boolean.TRUE; + }, l1, l2); + assertListOperation(list -> list.add(Complex.ofCartesian(13, 14)), l1, l2); + assertListOperation(list -> list.add(Complex.ofCartesian(15, 16)), l1, l2); + assertListOperation(list -> list.add(Complex.ofCartesian(17, 18)), l1, l2); + assertListOperation(list -> { + list.addAll(1, list); + return Boolean.TRUE; + }, l1, l2); + assertListOperation(list -> list.add(Complex.ofCartesian(19, 20)), l1, l2); + assertListOperation(list -> list.add(Complex.ofCartesian(21, 22)), l1, l2); + assertListOperation(list -> list.add(Complex.ofCartesian(23, 24)), l1, l2); + + //Testing add at an index for branch condition (size == realAndImagParts.length >>> 1) + List l3 = new ArrayList<>(); + List l4 = new ComplexList(); + assertListOperation(list -> list.add(Complex.ofCartesian(1, 2)), l3, l4); + assertListOperation(list -> { + list.add(1, Complex.ofCartesian(10, 20)); + return Boolean.TRUE; + }, l3, l4); + assertListOperation(list -> list.add(Complex.ofCartesian(13, 14)), l3, l4); + assertListOperation(list -> list.add(Complex.ofCartesian(15, 16)), l3, l4); + assertListOperation(list -> list.add(Complex.ofCartesian(17, 18)), l3, l4); + assertListOperation(list -> { + list.addAll(1, list); + return Boolean.TRUE; + }, l3, l4); + assertListOperation(list -> list.add(Complex.ofCartesian(19, 20)), l3, l4); + assertListOperation(list -> list.add(Complex.ofCartesian(21, 22)), l3, l4); + assertListOperation(list -> { + list.add(1, Complex.ofCartesian(10, 20)); + return Boolean.TRUE; + }, l3, l4); + + //Testing branch condition (newArrayCapacity < minArrayCapacity) in ensureCapacity + ComplexList list1 = new ComplexList(); + int size = 5; + IntStream.range(0, size).mapToObj(i -> Complex.ofCartesian(i, -i)).forEach(list1::add); + + List l5 = new ArrayList<>(); + List l6 = new ComplexList(); + assertListOperation(list -> list.add(Complex.ofCartesian(1, 2)), l5, l6); + // Expand the list by doubling in size until at the known minArrayCapacity + while (l5.size() < 8) { + assertListOperation(list -> list.addAll(list), l5, l6); + } + assertListOperation(list -> list.addAll(list1), l5, l6); + + //Test for adding an empty list to an empty list + ComplexList list = new ComplexList(); + assertListOperation(l -> { + l.addAll(list); + return l.addAll(0, list); + }); + } + + @Test + void testRemove() { + assertListOperation(list -> { + list.add(Complex.ofCartesian(42, 13)); + list.addAll(list); + list.remove(0); + return list.remove(0); + }); + } + + @Test + void testGetAndSetIndexOutOfBoundExceptions() { + ComplexList list = new ComplexList(); + // Empty list throws + Assertions.assertThrows(IndexOutOfBoundsException.class, () -> list.get(0)); + int size = 5; + IntStream.range(0, size).mapToObj(i -> Complex.ofCartesian(i, -i)).forEach(list::add); + + Assertions.assertThrows(IndexOutOfBoundsException.class, () -> list.get(-1)); + Assertions.assertThrows(IndexOutOfBoundsException.class, () -> list.get(size)); + Assertions.assertThrows(IndexOutOfBoundsException.class, () -> list.get(size + 1)); + + Assertions.assertThrows(IndexOutOfBoundsException.class, () -> list.set(-2, Complex.ofCartesian(200, 1))); + Assertions.assertThrows(IndexOutOfBoundsException.class, () -> list.set(size, Complex.ofCartesian(200, 1))); + Assertions.assertThrows(IndexOutOfBoundsException.class, () -> list.set(size + 1, Complex.ofCartesian(200, 1))); + } + + @Test + void testAddIndexOutOfBoundExceptions() { + ComplexList list = new ComplexList(); + int size = 5; + IntStream.range(0, size).mapToObj(i -> Complex.ofCartesian(i, -i)).forEach(list::add); + + Assertions.assertThrows(IndexOutOfBoundsException.class, () -> + list.add(-1, Complex.ofCartesian(42, 13))); + Assertions.assertThrows(IndexOutOfBoundsException.class, () -> + list.add(size + 1, Complex.ofCartesian(42, 13))); + } + + @Test + void testRemoveIndexOutOfBoundExceptions() { + ComplexList list = new ComplexList(); + list.add(Complex.ofCartesian(42, 13)); + list.addAll(list); + list.remove(0); + Assertions.assertThrows(IndexOutOfBoundsException.class, () -> list.remove(1)); + Assertions.assertThrows(IndexOutOfBoundsException.class, () -> list.remove(-1)); + } + + @ParameterizedTest + @ValueSource(ints = {0, 1, 10}) + void testConstructor(int size) { + List l1 = new ArrayList<>(size); + List l2 = new ComplexList(size); + Assertions.assertEquals(l1, l2); + assertListOperation(l -> l.add(Complex.ofCartesian(10, 20)), l1, l2); + assertListOperation(l -> { + l.add(1, Complex.ofCartesian(10, 20)); + return Boolean.TRUE; + }, l1, l2); + assertListOperation(l -> l.addAll(1, l), l1, l2); + } + + @Test + void testCapacityExceptions() { + + Assertions.assertThrows(IllegalArgumentException.class, () -> new ComplexList(MAX_CAPACITY + 1)); + + // Set-up required sizes + ComplexList list = new ComplexList(); + List l = new SizedList(Integer.MAX_VALUE); + Assertions.assertThrows(OutOfMemoryError.class, () -> list.addAll(l)); + + List l2 = new SizedList(MAX_CAPACITY + 1); + Assertions.assertThrows(OutOfMemoryError.class, () -> list.addAll(l2)); + } + + private static void assertListOperation(Function, T> operation, + List l1, List l2) { + T t1 = operation.apply(l1); + T t2 = operation.apply(l2); + Assertions.assertEquals(t1, t2); + Assertions.assertEquals(l1, l2); + } + + private static void assertListOperation(Function, T> operation) { + assertListOperation(operation, new ArrayList<>(), new ComplexList()); + } + + /** + * This class purposely gives a fixed size and so is a non-functional list. + * It is used to trigger capacity exceptions when adding a collection to ComplexList. + */ + private static class SizedList extends ArrayList { + private final int fixedSize; + + SizedList(int fixedSize) { + super(); + this.fixedSize = fixedSize; + } + + @Override + public int size() { + return fixedSize; + } + } +} diff --git a/pom.xml b/pom.xml index cce4ce398..5bb4f3666 100644 --- a/pom.xml +++ b/pom.xml @@ -109,6 +109,7 @@ commons-numbers-core commons-numbers-complex + commons-numbers-complex-arrays commons-numbers-primes commons-numbers-quaternion commons-numbers-fraction @@ -150,6 +151,11 @@ commons-numbers-complex ${project.version} + + org.apache.commons + commons-numbers-complex-arrays + ${project.version} + org.apache.commons commons-numbers-fraction