diff options
author | Adrien Hopkins <masterofnumbers17@gmail.com> | 2019-10-21 15:25:24 -0400 |
---|---|---|
committer | Adrien Hopkins <masterofnumbers17@gmail.com> | 2019-10-21 15:25:24 -0400 |
commit | 8c8f900416981863607c3c39d737ab1be8540e1a (patch) | |
tree | a036180832095671027babc8b0fc16e3ca4eca47 /src/org/unitConverter/math | |
parent | 511fe144da142082a02b5a5b07e67bb76df1331e (diff) | |
parent | ce7402fb5e52d947b6b7c383fa96e3aaaf9da188 (diff) |
Merge branch 'feature-new-units-def' into develop
Diffstat (limited to 'src/org/unitConverter/math')
6 files changed, 989 insertions, 1 deletions
diff --git a/src/org/unitConverter/math/ConditionalExistenceCollections.java b/src/org/unitConverter/math/ConditionalExistenceCollections.java new file mode 100644 index 0000000..9522885 --- /dev/null +++ b/src/org/unitConverter/math/ConditionalExistenceCollections.java @@ -0,0 +1,407 @@ +/** + * Copyright (C) 2019 Adrien Hopkins + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + */ +package org.unitConverter.math; + +import java.util.AbstractCollection; +import java.util.AbstractMap; +import java.util.AbstractSet; +import java.util.Collection; +import java.util.Iterator; +import java.util.Map; +import java.util.Map.Entry; +import java.util.NoSuchElementException; +import java.util.Set; +import java.util.function.Predicate; + +/** + * Elements in these wrapper collections only exist if they pass a condition. + * <p> + * All of the collections in this class are "views" of the provided collections. They are mutable if the provided + * collections are mutable, they allow null if the provided collections allow null, they will reflect changes in the + * provided collection, etc. + * <p> + * The modification operations will always run the corresponding operations, even if the conditional existence + * collection doesn't change. For example, if you have a set that ignores even numbers, add(2) will still add a 2 to the + * backing set (but the conditional existence set will say it doesn't exist). + * <p> + * The returned collections do <i>not</i> pass the hashCode and equals operations through to the backing collections, + * but rely on {@code Object}'s {@code equals} and {@code hashCode} methods. This is necessary to preserve the contracts + * of these operations in the case that the backing collections are sets or lists. + * <p> + * Other than that, <i>the only difference between the provided collections and the returned collections are that + * elements don't exist if they don't pass the provided condition</i>. + * + * + * @author Adrien Hopkins + * @since 2019-10-17 + */ +// TODO add conditional existence Lists and Sorted/Navigable Sets/Maps +public final class ConditionalExistenceCollections { + /** + * Elements in this collection only exist if they meet a condition. + * + * @author Adrien Hopkins + * @since 2019-10-17 + * @param <E> + * type of element in collection + */ + static final class ConditionalExistenceCollection<E> extends AbstractCollection<E> { + final Collection<E> collection; + final Predicate<E> existenceCondition; + + /** + * Creates the {@code ConditionalExistenceCollection}. + * + * @param collection + * @param existenceCondition + * @since 2019-10-17 + */ + private ConditionalExistenceCollection(final Collection<E> collection, final Predicate<E> existenceCondition) { + this.collection = collection; + this.existenceCondition = existenceCondition; + } + + @Override + public boolean add(final E e) { + return this.collection.add(e) && this.existenceCondition.test(e); + } + + @Override + public void clear() { + this.collection.clear(); + } + + @Override + public boolean contains(final Object o) { + if (!this.collection.contains(o)) + return false; + + // this collection can only contain instances of E + // since the object is in the collection, we know that it must be an instance of E + // therefore this cast will always work + @SuppressWarnings("unchecked") + final E e = (E) o; + + return this.existenceCondition.test(e); + } + + @Override + public Iterator<E> iterator() { + return conditionalExistenceIterator(this.collection.iterator(), this.existenceCondition); + } + + @Override + public boolean remove(final Object o) { + // remove() must be first in the && statement, otherwise it may not execute + final boolean containedObject = this.contains(o); + return this.collection.remove(o) && containedObject; + } + + @Override + public int size() { + return (int) this.collection.stream().filter(this.existenceCondition).count(); + } + } + + /** + * Elements in this wrapper iterator only exist if they pass a condition. + * + * @author Adrien Hopkins + * @since 2019-10-17 + * @param <E> + * type of elements in iterator + */ + static final class ConditionalExistenceIterator<E> implements Iterator<E> { + final Iterator<E> iterator; + final Predicate<E> existenceCondition; + E nextElement; + boolean hasNext; + + /** + * Creates the {@code ConditionalExistenceIterator}. + * + * @param iterator + * @param condition + * @since 2019-10-17 + */ + private ConditionalExistenceIterator(final Iterator<E> iterator, final Predicate<E> condition) { + this.iterator = iterator; + this.existenceCondition = condition; + this.getAndSetNextElement(); + } + + /** + * Gets the next element, and sets nextElement and hasNext accordingly. + * + * @since 2019-10-17 + */ + private void getAndSetNextElement() { + do { + if (!this.iterator.hasNext()) { + this.nextElement = null; + this.hasNext = false; + return; + } + this.nextElement = this.iterator.next(); + } while (!this.existenceCondition.test(this.nextElement)); + this.hasNext = true; + } + + @Override + public boolean hasNext() { + return this.hasNext; + } + + @Override + public E next() { + if (this.hasNext()) { + final E next = this.nextElement; + this.getAndSetNextElement(); + return next; + } else + throw new NoSuchElementException(); + } + + @Override + public void remove() { + this.iterator.remove(); + } + } + + /** + * Mappings in this map only exist if the entry passes some condition. + * + * @author Adrien Hopkins + * @since 2019-10-17 + * @param <K> + * key type + * @param <V> + * value type + */ + static final class ConditionalExistenceMap<K, V> extends AbstractMap<K, V> { + Map<K, V> map; + Predicate<Entry<K, V>> entryExistenceCondition; + + /** + * Creates the {@code ConditionalExistenceMap}. + * + * @param map + * @param entryExistenceCondition + * @since 2019-10-17 + */ + private ConditionalExistenceMap(final Map<K, V> map, final Predicate<Entry<K, V>> entryExistenceCondition) { + this.map = map; + this.entryExistenceCondition = entryExistenceCondition; + } + + @Override + public boolean containsKey(final Object key) { + if (!this.map.containsKey(key)) + return false; + + // only instances of K have mappings in the backing map + // since we know that key is a valid key, it must be an instance of K + @SuppressWarnings("unchecked") + final K keyAsK = (K) key; + + // get and test entry + final V value = this.map.get(key); + final Entry<K, V> entry = new SimpleEntry<>(keyAsK, value); + return this.entryExistenceCondition.test(entry); + } + + @Override + public Set<Entry<K, V>> entrySet() { + return conditionalExistenceSet(this.map.entrySet(), this.entryExistenceCondition); + } + + @Override + public V get(final Object key) { + return this.containsKey(key) ? this.map.get(key) : null; + } + + @Override + public Set<K> keySet() { + // maybe change this to use ConditionalExistenceSet + return super.keySet(); + } + + @Override + public V put(final K key, final V value) { + final V oldValue = this.map.put(key, value); + + // get and test entry + final Entry<K, V> entry = new SimpleEntry<>(key, oldValue); + return this.entryExistenceCondition.test(entry) ? oldValue : null; + } + + @Override + public V remove(final Object key) { + final V oldValue = this.map.remove(key); + return this.containsKey(key) ? oldValue : null; + } + + @Override + public Collection<V> values() { + // maybe change this to use ConditionalExistenceCollection + return super.values(); + } + + } + + /** + * Elements in this set only exist if a certain condition is true. + * + * @author Adrien Hopkins + * @since 2019-10-17 + * @param <E> + * type of element in set + */ + static final class ConditionalExistenceSet<E> extends AbstractSet<E> { + private final Set<E> set; + private final Predicate<E> existenceCondition; + + /** + * Creates the {@code ConditionalNonexistenceSet}. + * + * @param set + * set to use + * @param existenceCondition + * condition where element exists + * @since 2019-10-17 + */ + private ConditionalExistenceSet(final Set<E> set, final Predicate<E> existenceCondition) { + this.set = set; + this.existenceCondition = existenceCondition; + } + + /** + * {@inheritDoc} + * <p> + * Note that this method returns {@code false} if {@code e} does not pass the existence condition. + */ + @Override + public boolean add(final E e) { + return this.set.add(e) && this.existenceCondition.test(e); + } + + @Override + public void clear() { + this.set.clear(); + } + + @Override + public boolean contains(final Object o) { + if (!this.set.contains(o)) + return false; + + // this set can only contain instances of E + // since the object is in the set, we know that it must be an instance of E + // therefore this cast will always work + @SuppressWarnings("unchecked") + final E e = (E) o; + + return this.existenceCondition.test(e); + } + + @Override + public Iterator<E> iterator() { + return conditionalExistenceIterator(this.set.iterator(), this.existenceCondition); + } + + @Override + public boolean remove(final Object o) { + // remove() must be first in the && statement, otherwise it may not execute + final boolean containedObject = this.contains(o); + return this.set.remove(o) && containedObject; + } + + @Override + public int size() { + return (int) this.set.stream().filter(this.existenceCondition).count(); + } + } + + /** + * Elements in the returned wrapper collection are ignored if they don't pass a condition. + * + * @param <E> + * type of elements in collection + * @param collection + * collection to wrap + * @param existenceCondition + * elements only exist if this returns true + * @return wrapper collection + * @since 2019-10-17 + */ + public static final <E> Collection<E> conditionalExistenceCollection(final Collection<E> collection, + final Predicate<E> existenceCondition) { + return new ConditionalExistenceCollection<>(collection, existenceCondition); + } + + /** + * Elements in the returned wrapper iterator are ignored if they don't pass a condition. + * + * @param <E> + * type of elements in iterator + * @param iterator + * iterator to wrap + * @param existenceCondition + * elements only exist if this returns true + * @return wrapper iterator + * @since 2019-10-17 + */ + public static final <E> Iterator<E> conditionalExistenceIterator(final Iterator<E> iterator, + final Predicate<E> existenceCondition) { + return new ConditionalExistenceIterator<>(iterator, existenceCondition); + } + + /** + * Mappings in the returned wrapper map are ignored if the corresponding entry doesn't pass a condition + * + * @param <K> + * type of key in map + * @param <V> + * type of value in map + * @param map + * map to wrap + * @param entryExistenceCondition + * mappings only exist if this returns true + * @return wrapper map + * @since 2019-10-17 + */ + public static final <K, V> Map<K, V> conditionalExistenceMap(final Map<K, V> map, + final Predicate<Entry<K, V>> entryExistenceCondition) { + return new ConditionalExistenceMap<>(map, entryExistenceCondition); + } + + /** + * Elements in the returned wrapper set are ignored if they don't pass a condition. + * + * @param <E> + * type of elements in set + * @param set + * set to wrap + * @param existenceCondition + * elements only exist if this returns true + * @return wrapper set + * @since 2019-10-17 + */ + public static final <E> Set<E> conditionalExistenceSet(final Set<E> set, final Predicate<E> existenceCondition) { + return new ConditionalExistenceSet<>(set, existenceCondition); + } +} diff --git a/src/org/unitConverter/math/ConditionalExistenceCollectionsTest.java b/src/org/unitConverter/math/ConditionalExistenceCollectionsTest.java new file mode 100644 index 0000000..311ace5 --- /dev/null +++ b/src/org/unitConverter/math/ConditionalExistenceCollectionsTest.java @@ -0,0 +1,159 @@ +/** + * Copyright (C) 2019 Adrien Hopkins + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + */ +package org.unitConverter.math; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.NoSuchElementException; + +import org.junit.jupiter.api.Test; +import org.unitConverter.math.ConditionalExistenceCollections.ConditionalExistenceIterator; + +/** + * Tests the {@link #ConditionalExistenceCollections}. + * + * @author Adrien Hopkins + * @since 2019-10-16 + */ +class ConditionalExistenceCollectionsTest { + + /** + * The returned iterator ignores elements that don't start with "a". + * + * @return test iterator + * @since 2019-10-17 + */ + ConditionalExistenceIterator<String> getTestIterator() { + final List<String> items = Arrays.asList("aa", "ab", "ba"); + final Iterator<String> it = items.iterator(); + final ConditionalExistenceIterator<String> cit = (ConditionalExistenceIterator<String>) ConditionalExistenceCollections + .conditionalExistenceIterator(it, s -> s.startsWith("a")); + return cit; + } + + /** + * The returned map ignores mappings where the value is zero. + * + * @return map to be used for test data + * @since 2019-10-16 + */ + Map<String, Integer> getTestMap() { + final Map<String, Integer> map = new HashMap<>(); + map.put("one", 1); + map.put("two", 2); + map.put("zero", 0); + map.put("ten", 10); + final Map<String, Integer> conditionalMap = ConditionalExistenceCollections.conditionalExistenceMap(map, + e -> !Integer.valueOf(0).equals(e.getValue())); + return conditionalMap; + } + + /** + * Test method for {@link org.unitConverter.math.ZeroIsNullMap#containsKey(java.lang.Object)}. + */ + @Test + void testContainsKeyObject() { + final Map<String, Integer> map = this.getTestMap(); + assertTrue(map.containsKey("one")); + assertTrue(map.containsKey("ten")); + assertFalse(map.containsKey("five")); + assertFalse(map.containsKey("zero")); + } + + /** + * Test method for {@link org.unitConverter.math.ZeroIsNullMap#containsValue(java.lang.Object)}. + */ + @Test + void testContainsValueObject() { + final Map<String, Integer> map = this.getTestMap(); + assertTrue(map.containsValue(1)); + assertTrue(map.containsValue(10)); + assertFalse(map.containsValue(5)); + assertFalse(map.containsValue(0)); + } + + /** + * Test method for {@link org.unitConverter.math.ZeroIsNullMap#entrySet()}. + */ + @Test + void testEntrySet() { + final Map<String, Integer> map = this.getTestMap(); + for (final Entry<String, Integer> e : map.entrySet()) { + assertTrue(e.getValue() != 0); + } + } + + /** + * Test method for {@link org.unitConverter.math.ZeroIsNullMap#get(java.lang.Object)}. + */ + @Test + void testGetObject() { + final Map<String, Integer> map = this.getTestMap(); + assertEquals(1, map.get("one")); + assertEquals(10, map.get("ten")); + assertEquals(null, map.get("five")); + assertEquals(null, map.get("zero")); + } + + @Test + void testIterator() { + final ConditionalExistenceIterator<String> testIterator = this.getTestIterator(); + + assertTrue(testIterator.hasNext); + assertTrue(testIterator.hasNext()); + assertEquals("aa", testIterator.nextElement); + assertEquals("aa", testIterator.next()); + + assertTrue(testIterator.hasNext); + assertTrue(testIterator.hasNext()); + assertEquals("ab", testIterator.nextElement); + assertEquals("ab", testIterator.next()); + + assertFalse(testIterator.hasNext); + assertFalse(testIterator.hasNext()); + assertEquals(null, testIterator.nextElement); + assertThrows(NoSuchElementException.class, testIterator::next); + } + + /** + * Test method for {@link org.unitConverter.math.ZeroIsNullMap#keySet()}. + */ + @Test + void testKeySet() { + final Map<String, Integer> map = this.getTestMap(); + assertFalse(map.keySet().contains("zero")); + } + + /** + * Test method for {@link org.unitConverter.math.ZeroIsNullMap#values()}. + */ + @Test + void testValues() { + final Map<String, Integer> map = this.getTestMap(); + assertFalse(map.values().contains(0)); + } + +} diff --git a/src/org/unitConverter/math/DecimalComparison.java b/src/org/unitConverter/math/DecimalComparison.java index 7cdbe5b..859e8da 100644 --- a/src/org/unitConverter/math/DecimalComparison.java +++ b/src/org/unitConverter/math/DecimalComparison.java @@ -16,6 +16,8 @@ */ package org.unitConverter.math; +import java.math.BigDecimal; + /** * A class that contains methods to compare float and double values. * @@ -44,6 +46,18 @@ public final class DecimalComparison { /** * Tests for equality of double values using {@link #DOUBLE_EPSILON}. + * <p> + * <strong>WARNING: </strong>this method is not technically transitive. If a and b are off by slightly less than + * {@code epsilon * max(abs(a), abs(b))}, and b and c are off by slightly less than + * {@code epsilon * max(abs(b), abs(c))}, then equals(a, b) and equals(b, c) will both return true, but equals(a, c) + * will return false. However, this situation is very unlikely to ever happen in a real programming situation. + * <p> + * If this does become a concern, some ways to solve this problem: + * <ol> + * <li>Raise the value of epsilon using {@link #equals(double, double, double)} (this does not make a violation of + * transitivity impossible, it just significantly reduces the chances of it happening) + * <li>Use {@link BigDecimal} instead of {@code double} (this will make a violation of transitivity 100% impossible) + * </ol> * * @param a * first value to test @@ -52,6 +66,7 @@ public final class DecimalComparison { * @return whether they are equal * @since 2019-03-18 * @since v0.2.0 + * @see #hashCode(double) */ public static final boolean equals(final double a, final double b) { return DecimalComparison.equals(a, b, DOUBLE_EPSILON); @@ -60,6 +75,19 @@ public final class DecimalComparison { /** * Tests for double equality using a custom epsilon value. * + * <p> + * <strong>WARNING: </strong>this method is not technically transitive. If a and b are off by slightly less than + * {@code epsilon * max(abs(a), abs(b))}, and b and c are off by slightly less than + * {@code epsilon * max(abs(b), abs(c))}, then equals(a, b) and equals(b, c) will both return true, but equals(a, c) + * will return false. However, this situation is very unlikely to ever happen in a real programming situation. + * <p> + * If this does become a concern, some ways to solve this problem: + * <ol> + * <li>Raise the value of epsilon (this does not make a violation of transitivity impossible, it just significantly + * reduces the chances of it happening) + * <li>Use {@link BigDecimal} instead of {@code double} (this will make a violation of transitivity 100% impossible) + * </ol> + * * @param a * first value to test * @param b @@ -77,6 +105,19 @@ public final class DecimalComparison { /** * Tests for equality of float values using {@link #FLOAT_EPSILON}. * + * <p> + * <strong>WARNING: </strong>this method is not technically transitive. If a and b are off by slightly less than + * {@code epsilon * max(abs(a), abs(b))}, and b and c are off by slightly less than + * {@code epsilon * max(abs(b), abs(c))}, then equals(a, b) and equals(b, c) will both return true, but equals(a, c) + * will return false. However, this situation is very unlikely to ever happen in a real programming situation. + * <p> + * If this does become a concern, some ways to solve this problem: + * <ol> + * <li>Raise the value of epsilon using {@link #equals(float, float, float)} (this does not make a violation of + * transitivity impossible, it just significantly reduces the chances of it happening) + * <li>Use {@link BigDecimal} instead of {@code float} (this will make a violation of transitivity 100% impossible) + * </ol> + * * @param a * first value to test * @param b @@ -92,6 +133,19 @@ public final class DecimalComparison { /** * Tests for float equality using a custom epsilon value. * + * <p> + * <strong>WARNING: </strong>this method is not technically transitive. If a and b are off by slightly less than + * {@code epsilon * max(abs(a), abs(b))}, and b and c are off by slightly less than + * {@code epsilon * max(abs(b), abs(c))}, then equals(a, b) and equals(b, c) will both return true, but equals(a, c) + * will return false. However, this situation is very unlikely to ever happen in a real programming situation. + * <p> + * If this does become a concern, some ways to solve this problem: + * <ol> + * <li>Raise the value of epsilon (this does not make a violation of transitivity impossible, it just significantly + * reduces the chances of it happening) + * <li>Use {@link BigDecimal} instead of {@code float} (this will make a violation of transitivity 100% impossible) + * </ol> + * * @param a * first value to test * @param b @@ -106,6 +160,19 @@ public final class DecimalComparison { return Math.abs(a - b) <= epsilon * Math.max(Math.abs(a), Math.abs(b)); } + /** + * Takes the hash code of doubles. Values that are equal according to {@link #equals(double, double)} will have the + * same hash code. + * + * @param d + * double to hash + * @return hash code of double + * @since 2019-10-16 + */ + public static final int hash(final double d) { + return Float.hashCode((float) d); + } + // You may NOT get any DecimalComparison instances private DecimalComparison() { throw new AssertionError(); diff --git a/src/org/unitConverter/math/ObjectProduct.java b/src/org/unitConverter/math/ObjectProduct.java new file mode 100644 index 0000000..0cf89ec --- /dev/null +++ b/src/org/unitConverter/math/ObjectProduct.java @@ -0,0 +1,276 @@ +/**
+ * Copyright (C) 2018 Adrien Hopkins
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+package org.unitConverter.math;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+import java.util.function.Function;
+
+/**
+ * An immutable product of multiple objects of a type, such as base units. The objects can be multiplied and
+ * exponentiated.
+ *
+ * @author Adrien Hopkins
+ * @since 2019-10-16
+ */
+public final class ObjectProduct<T> {
+ /**
+ * Returns an empty ObjectProduct of a certain type
+ *
+ * @param <T>
+ * type of objects that can be multiplied
+ * @return empty product
+ * @since 2019-10-16
+ */
+ public static final <T> ObjectProduct<T> empty() {
+ return new ObjectProduct<>(new HashMap<>());
+ }
+
+ /**
+ * Gets an {@code ObjectProduct} from an object-to-integer mapping
+ *
+ * @param <T>
+ * type of object in product
+ * @param map
+ * map mapping objects to exponents
+ * @return object product
+ * @since 2019-10-16
+ */
+ public static final <T> ObjectProduct<T> fromExponentMapping(final Map<T, Integer> map) {
+ return new ObjectProduct<>(new HashMap<>(map));
+ }
+
+ /**
+ * Gets an ObjectProduct that has one of the inputted argument, and nothing else.
+ *
+ * @param object
+ * object that will be in the product
+ * @return product
+ * @since 2019-10-16
+ * @throws NullPointerException
+ * if object is null
+ */
+ public static final <T> ObjectProduct<T> oneOf(final T object) {
+ Objects.requireNonNull(object, "object must not be null.");
+ final Map<T, Integer> map = new HashMap<>();
+ map.put(object, 1);
+ return new ObjectProduct<>(map);
+ }
+
+ /**
+ * The objects that make up the product, mapped to their exponents. This map treats zero as null, and is immutable.
+ *
+ * @since 2019-10-16
+ */
+ final Map<T, Integer> exponents;
+
+ /**
+ * Creates the {@code ObjectProduct}.
+ *
+ * @param exponents
+ * objects that make up this product
+ * @since 2019-10-16
+ */
+ private ObjectProduct(final Map<T, Integer> exponents) {
+ this.exponents = Collections.unmodifiableMap(ConditionalExistenceCollections
+ .conditionalExistenceMap(new HashMap<>(exponents), e -> !Integer.valueOf(0).equals(e.getValue())));
+ }
+
+ /**
+ * Calculates the quotient of two products
+ *
+ * @param other
+ * other product
+ * @return quotient of two products
+ * @since 2019-10-16
+ * @throws NullPointerException
+ * if other is null
+ */
+ public ObjectProduct<T> dividedBy(final ObjectProduct<T> other) {
+ Objects.requireNonNull(other, "other must not be null.");
+ // get a list of all objects in both sets
+ final Set<T> objects = new HashSet<>();
+ objects.addAll(this.getBaseSet());
+ objects.addAll(other.getBaseSet());
+
+ // get a list of all exponents
+ final Map<T, Integer> map = new HashMap<>(objects.size());
+ for (final T key : objects) {
+ map.put(key, this.getExponent(key) - other.getExponent(key));
+ }
+
+ // create the product
+ return new ObjectProduct<>(map);
+ }
+
+ // this method relies on the use of ZeroIsNullMap
+ @Override
+ public boolean equals(final Object obj) {
+ if (this == obj)
+ return true;
+ if (!(obj instanceof ObjectProduct))
+ return false;
+ final ObjectProduct<?> other = (ObjectProduct<?>) obj;
+ return Objects.equals(this.exponents, other.exponents);
+ }
+
+ /**
+ * @return immutable map mapping objects to exponents
+ * @since 2019-10-16
+ */
+ public Map<T, Integer> exponentMap() {
+ return this.exponents;
+ }
+
+ /**
+ * @return a set of all of the base objects with non-zero exponents that make up this dimension.
+ * @since 2018-12-12
+ * @since v0.1.0
+ */
+ public final Set<T> getBaseSet() {
+ final Set<T> dimensions = new HashSet<>();
+
+ // add all dimensions with a nonzero exponent - zero exponents shouldn't be there in the first place
+ for (final T dimension : this.exponents.keySet()) {
+ if (!this.exponents.get(dimension).equals(0)) {
+ dimensions.add(dimension);
+ }
+ }
+
+ return dimensions;
+ }
+
+ /**
+ * Gets the exponent for a specific dimension.
+ *
+ * @param dimension
+ * dimension to check
+ * @return exponent for that dimension
+ * @since 2018-12-12
+ * @since v0.1.0
+ */
+ public int getExponent(final T dimension) {
+ return this.exponents.getOrDefault(dimension, 0);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(this.exponents);
+ }
+
+ /**
+ * @return true if this product is a single object, i.e. it has one exponent of one and no other nonzero exponents
+ * @since 2019-10-16
+ */
+ public boolean isSingleObject() {
+ int oneCount = 0;
+ boolean twoOrMore = false; // has exponents of 2 or more
+ for (final T b : this.getBaseSet()) {
+ if (this.getExponent(b) == 1) {
+ oneCount++;
+ } else if (this.getExponent(b) != 0) {
+ twoOrMore = true;
+ }
+ }
+ return oneCount == 1 && !twoOrMore;
+ }
+
+ /**
+ * Multiplies this product by another
+ *
+ * @param other
+ * other product
+ * @return product of two products
+ * @since 2019-10-16
+ * @throws NullPointerException
+ * if other is null
+ */
+ public ObjectProduct<T> times(final ObjectProduct<T> other) {
+ Objects.requireNonNull(other, "other must not be null.");
+ // get a list of all objects in both sets
+ final Set<T> objects = new HashSet<>();
+ objects.addAll(this.getBaseSet());
+ objects.addAll(other.getBaseSet());
+
+ // get a list of all exponents
+ final Map<T, Integer> map = new HashMap<>(objects.size());
+ for (final T key : objects) {
+ map.put(key, this.getExponent(key) + other.getExponent(key));
+ }
+
+ // create the product
+ return new ObjectProduct<>(map);
+ }
+
+ /**
+ * Returns this product, but to an exponent
+ *
+ * @param exponent
+ * exponent
+ * @return result of exponentiation
+ * @since 2019-10-16
+ */
+ public ObjectProduct<T> toExponent(final int exponent) {
+ final Map<T, Integer> map = new HashMap<>(this.exponents);
+ for (final T key : this.exponents.keySet()) {
+ map.put(key, this.getExponent(key) * exponent);
+ }
+ return new ObjectProduct<>(map);
+ }
+
+ @Override
+ public String toString() {
+ return this.toString(Object::toString);
+ }
+
+ /**
+ * Converts this product to a string. The objects that make up this product are represented by
+ * {@code objectToString}
+ *
+ * @param objectToString
+ * function to convert objects to strings
+ * @return string representation of product
+ * @since 2019-10-16
+ */
+ public String toString(final Function<T, String> objectToString) {
+ final List<String> positiveStringComponents = new ArrayList<>();
+ final List<String> negativeStringComponents = new ArrayList<>();
+
+ // for each base object that makes up this object, add it and its exponent
+ for (final T object : this.getBaseSet()) {
+ final int exponent = this.exponents.get(object);
+ if (exponent > 0) {
+ positiveStringComponents.add(String.format("%s^%d", objectToString.apply(object), exponent));
+ } else if (exponent < 0) {
+ negativeStringComponents.add(String.format("%s^%d", objectToString.apply(object), -exponent));
+ }
+ }
+
+ final String positiveString = positiveStringComponents.isEmpty() ? "1"
+ : String.join(" * ", positiveStringComponents);
+ final String negativeString = negativeStringComponents.isEmpty() ? ""
+ : " / " + String.join(" * ", negativeStringComponents);
+
+ return positiveString + negativeString;
+ }
+}
diff --git a/src/org/unitConverter/math/ObjectProductTest.java b/src/org/unitConverter/math/ObjectProductTest.java new file mode 100644 index 0000000..afd18b7 --- /dev/null +++ b/src/org/unitConverter/math/ObjectProductTest.java @@ -0,0 +1,78 @@ +/** + * Copyright (C) 2018 Adrien Hopkins + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + */ +package org.unitConverter.math; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.unitConverter.unit.SI.Dimensions.AREA; +import static org.unitConverter.unit.SI.Dimensions.ENERGY; +import static org.unitConverter.unit.SI.Dimensions.LENGTH; +import static org.unitConverter.unit.SI.Dimensions.MASS; +import static org.unitConverter.unit.SI.Dimensions.MASS_DENSITY; +import static org.unitConverter.unit.SI.Dimensions.QUANTITY; +import static org.unitConverter.unit.SI.Dimensions.TIME; +import static org.unitConverter.unit.SI.Dimensions.VOLUME; + +import org.junit.jupiter.api.Test; +import org.unitConverter.unit.SI; + +/** + * Tests for {@link ObjectProduct} using BaseDimension as a test object. This is NOT part of this program's public API. + * + * @author Adrien Hopkins + * @since 2018-12-12 + * @since v0.1.0 + */ +class ObjectProductTest { + /** + * Tests {@link UnitDimension#equals} + * + * @since 2018-12-12 + * @since v0.1.0 + */ + @Test + public void testEquals() { + assertEquals(LENGTH, LENGTH); + assertFalse(LENGTH.equals(QUANTITY)); + } + + /** + * Tests {@code UnitDimension}'s exponentiation + * + * @since 2019-01-15 + * @since v0.1.0 + */ + @Test + public void testExponents() { + assertEquals(1, LENGTH.getExponent(SI.BaseDimensions.LENGTH)); + assertEquals(3, VOLUME.getExponent(SI.BaseDimensions.LENGTH)); + } + + /** + * Tests {@code UnitDimension}'s multiplication and division. + * + * @since 2018-12-12 + * @since v0.1.0 + */ + @Test + public void testMultiplicationAndDivision() { + assertEquals(AREA, LENGTH.times(LENGTH)); + assertEquals(MASS_DENSITY, MASS.dividedBy(VOLUME)); + assertEquals(ENERGY, AREA.times(MASS).dividedBy(TIME).dividedBy(TIME)); + assertEquals(LENGTH, LENGTH.times(TIME).dividedBy(TIME)); + } +} diff --git a/src/org/unitConverter/math/package-info.java b/src/org/unitConverter/math/package-info.java index 65d6b23..65727e4 100644 --- a/src/org/unitConverter/math/package-info.java +++ b/src/org/unitConverter/math/package-info.java @@ -15,9 +15,10 @@ * along with this program. If not, see <https://www.gnu.org/licenses/>. */ /** - * A module that is capable of parsing expressions of things, like mathematical expressions or unit expressions. + * Supplementary classes that are not related to units, but are necessary for their function. * * @author Adrien Hopkins * @since 2019-03-14 + * @since v0.2.0 */ package org.unitConverter.math;
\ No newline at end of file |