From 72eed55d9f834de8302564fbe6dfa3d1d446afb4 Mon Sep 17 00:00:00 2001 From: Adrien Hopkins Date: Sat, 7 Aug 2021 15:25:54 -0500 Subject: Renamed sevenUnits.math to sevenUnits.utils --- .../sevenUnits/converterGUI/SevenUnitsGUI.java | 4 +- .../math/ConditionalExistenceCollections.java | 468 ------------- .../java/sevenUnits/math/DecimalComparison.java | 256 ------- .../java/sevenUnits/math/ExpressionParser.java | 735 --------------------- src/main/java/sevenUnits/math/ObjectProduct.java | 284 -------- src/main/java/sevenUnits/math/UncertainDouble.java | 419 ------------ src/main/java/sevenUnits/math/package-info.java | 24 - src/main/java/sevenUnits/unit/FunctionalUnit.java | 2 +- .../java/sevenUnits/unit/FunctionalUnitlike.java | 2 +- src/main/java/sevenUnits/unit/LinearUnit.java | 6 +- src/main/java/sevenUnits/unit/LinearUnitValue.java | 4 +- src/main/java/sevenUnits/unit/MultiUnit.java | 2 +- src/main/java/sevenUnits/unit/SI.java | 2 +- src/main/java/sevenUnits/unit/Unit.java | 4 +- src/main/java/sevenUnits/unit/UnitDatabase.java | 10 +- src/main/java/sevenUnits/unit/UnitPrefix.java | 2 +- src/main/java/sevenUnits/unit/Unitlike.java | 2 +- .../utils/ConditionalExistenceCollections.java | 468 +++++++++++++ .../java/sevenUnits/utils/DecimalComparison.java | 256 +++++++ .../java/sevenUnits/utils/ExpressionParser.java | 735 +++++++++++++++++++++ src/main/java/sevenUnits/utils/ObjectProduct.java | 284 ++++++++ .../java/sevenUnits/utils/UncertainDouble.java | 419 ++++++++++++ src/main/java/sevenUnits/utils/package-info.java | 25 + 23 files changed, 2207 insertions(+), 2206 deletions(-) delete mode 100644 src/main/java/sevenUnits/math/ConditionalExistenceCollections.java delete mode 100644 src/main/java/sevenUnits/math/DecimalComparison.java delete mode 100644 src/main/java/sevenUnits/math/ExpressionParser.java delete mode 100644 src/main/java/sevenUnits/math/ObjectProduct.java delete mode 100644 src/main/java/sevenUnits/math/UncertainDouble.java delete mode 100644 src/main/java/sevenUnits/math/package-info.java create mode 100644 src/main/java/sevenUnits/utils/ConditionalExistenceCollections.java create mode 100644 src/main/java/sevenUnits/utils/DecimalComparison.java create mode 100644 src/main/java/sevenUnits/utils/ExpressionParser.java create mode 100644 src/main/java/sevenUnits/utils/ObjectProduct.java create mode 100644 src/main/java/sevenUnits/utils/UncertainDouble.java create mode 100644 src/main/java/sevenUnits/utils/package-info.java (limited to 'src/main/java') diff --git a/src/main/java/sevenUnits/converterGUI/SevenUnitsGUI.java b/src/main/java/sevenUnits/converterGUI/SevenUnitsGUI.java index fb498da..e1eb2f4 100644 --- a/src/main/java/sevenUnits/converterGUI/SevenUnitsGUI.java +++ b/src/main/java/sevenUnits/converterGUI/SevenUnitsGUI.java @@ -65,8 +65,6 @@ import javax.swing.WindowConstants; import javax.swing.border.TitledBorder; import sevenUnits.ProgramInfo; -import sevenUnits.math.ConditionalExistenceCollections; -import sevenUnits.math.ObjectProduct; import sevenUnits.unit.BaseDimension; import sevenUnits.unit.BritishImperial; import sevenUnits.unit.LinearUnit; @@ -77,6 +75,8 @@ import sevenUnits.unit.Unit; import sevenUnits.unit.UnitDatabase; import sevenUnits.unit.UnitPrefix; import sevenUnits.unit.UnitValue; +import sevenUnits.utils.ConditionalExistenceCollections; +import sevenUnits.utils.ObjectProduct; /** * @author Adrien Hopkins diff --git a/src/main/java/sevenUnits/math/ConditionalExistenceCollections.java b/src/main/java/sevenUnits/math/ConditionalExistenceCollections.java deleted file mode 100644 index 5ce1bd7..0000000 --- a/src/main/java/sevenUnits/math/ConditionalExistenceCollections.java +++ /dev/null @@ -1,468 +0,0 @@ -/** - * 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 . - */ -package sevenUnits.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. - *

- * 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. - *

- * 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). - *

- * The returned collections do not 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. - *

- * Other than that, 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. - * - * - * @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 type of element in collection - */ - static final class ConditionalExistenceCollection - extends AbstractCollection { - final Collection collection; - final Predicate existenceCondition; - - /** - * Creates the {@code ConditionalExistenceCollection}. - * - * @param collection - * @param existenceCondition - * @since 2019-10-17 - */ - private ConditionalExistenceCollection(final Collection collection, - final Predicate 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 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(); - } - - @Override - public Object[] toArray() { - // ensure the toArray operation is supported - this.collection.toArray(); - - // if it works, do it for real - return super.toArray(); - } - - @Override - public T[] toArray(T[] a) { - // ensure the toArray operation is supported - this.collection.toArray(); - - // if it works, do it for real - return super.toArray(a); - } - } - - /** - * Elements in this wrapper iterator only exist if they pass a condition. - * - * @author Adrien Hopkins - * @since 2019-10-17 - * @param type of elements in iterator - */ - static final class ConditionalExistenceIterator implements Iterator { - final Iterator iterator; - final Predicate existenceCondition; - E nextElement; - boolean hasNext; - - /** - * Creates the {@code ConditionalExistenceIterator}. - * - * @param iterator - * @param condition - * @since 2019-10-17 - */ - private ConditionalExistenceIterator(final Iterator iterator, - final Predicate 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 key type - * @param value type - */ - static final class ConditionalExistenceMap extends AbstractMap { - Map map; - Predicate> entryExistenceCondition; - - /** - * Creates the {@code ConditionalExistenceMap}. - * - * @param map - * @param entryExistenceCondition - * @since 2019-10-17 - */ - private ConditionalExistenceMap(final Map map, - final Predicate> 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 entry = new SimpleEntry<>(keyAsK, value); - return this.entryExistenceCondition.test(entry); - } - - @Override - public Set> entrySet() { - return conditionalExistenceSet(this.map.entrySet(), - this.entryExistenceCondition); - } - - @Override - public V get(final Object key) { - return this.containsKey(key) ? this.map.get(key) : null; - } - - private final Entry getEntry(K key) { - return new Entry<>() { - @Override - public K getKey() { - return key; - } - - @Override - public V getValue() { - return ConditionalExistenceMap.this.map.get(key); - } - - @Override - public V setValue(V value) { - return ConditionalExistenceMap.this.map.put(key, value); - } - }; - } - - @Override - public Set keySet() { - return conditionalExistenceSet(super.keySet(), - k -> this.entryExistenceCondition.test(this.getEntry(k))); - } - - @Override - public V put(final K key, final V value) { - final V oldValue = this.map.put(key, value); - - // get and test entry - final Entry 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 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 type of element in set - */ - static final class ConditionalExistenceSet extends AbstractSet { - private final Set set; - private final Predicate existenceCondition; - - /** - * Creates the {@code ConditionalNonexistenceSet}. - * - * @param set set to use - * @param existenceCondition condition where element exists - * @since 2019-10-17 - */ - private ConditionalExistenceSet(final Set set, - final Predicate existenceCondition) { - this.set = set; - this.existenceCondition = existenceCondition; - } - - /** - * {@inheritDoc} - *

- * 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 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(); - } - - @Override - public Object[] toArray() { - // ensure the toArray operation is supported - this.set.toArray(); - - // if it works, do it for real - return super.toArray(); - } - - @Override - public T[] toArray(T[] a) { - // ensure the toArray operation is supported - this.set.toArray(); - - // if it works, do it for real - return super.toArray(a); - } - } - - /** - * Elements in the returned wrapper collection are ignored if they don't pass - * a condition. - * - * @param 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 Collection conditionalExistenceCollection( - final Collection collection, - final Predicate existenceCondition) { - return new ConditionalExistenceCollection<>(collection, - existenceCondition); - } - - /** - * Elements in the returned wrapper iterator are ignored if they don't pass a - * condition. - * - * @param 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 Iterator conditionalExistenceIterator( - final Iterator iterator, final Predicate existenceCondition) { - return new ConditionalExistenceIterator<>(iterator, existenceCondition); - } - - /** - * Mappings in the returned wrapper map are ignored if the corresponding - * entry doesn't pass a condition - * - * @param type of key in map - * @param 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 Map conditionalExistenceMap( - final Map map, - final Predicate> entryExistenceCondition) { - return new ConditionalExistenceMap<>(map, entryExistenceCondition); - } - - /** - * Elements in the returned wrapper set are ignored if they don't pass a - * condition. - * - * @param 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 Set conditionalExistenceSet(final Set set, - final Predicate existenceCondition) { - return new ConditionalExistenceSet<>(set, existenceCondition); - } -} diff --git a/src/main/java/sevenUnits/math/DecimalComparison.java b/src/main/java/sevenUnits/math/DecimalComparison.java deleted file mode 100644 index 24a9b2f..0000000 --- a/src/main/java/sevenUnits/math/DecimalComparison.java +++ /dev/null @@ -1,256 +0,0 @@ -/** - * 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 . - */ -package sevenUnits.math; - -import java.math.BigDecimal; - -/** - * A class that contains methods to compare float and double values. - * - * @author Adrien Hopkins - * @since 2019-03-18 - * @since v0.2.0 - */ -public final class DecimalComparison { - /** - * The value used for double comparison. If two double values are within this - * value multiplied by the larger value, they are considered equal. - * - * @since 2019-03-18 - * @since v0.2.0 - */ - public static final double DOUBLE_EPSILON = 1.0e-15; - - /** - * The value used for float comparison. If two float values are within this - * value multiplied by the larger value, they are considered equal. - * - * @since 2019-03-18 - * @since v0.2.0 - */ - public static final float FLOAT_EPSILON = 1.0e-6f; - - /** - * Tests for equality of double values using {@link #DOUBLE_EPSILON}. - *

- * WARNING: 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. - *

- * If this does become a concern, some ways to solve this problem: - *

    - *
  1. 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) - *
  2. Use {@link BigDecimal} instead of {@code double} (this will make a - * violation of transitivity 100% impossible) - *
- * - * @param a first value to test - * @param b second value to test - * @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); - } - - /** - * Tests for double equality using a custom epsilon value. - * - *

- * WARNING: 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. - *

- * If this does become a concern, some ways to solve this problem: - *

    - *
  1. Raise the value of epsilon (this does not make a violation of - * transitivity impossible, it just significantly reduces the chances of it - * happening) - *
  2. Use {@link BigDecimal} instead of {@code double} (this will make a - * violation of transitivity 100% impossible) - *
- * - * @param a first value to test - * @param b second value to test - * @param epsilon allowed difference - * @return whether they are equal - * @since 2019-03-18 - * @since v0.2.0 - */ - public static final boolean equals(final double a, final double b, - final double epsilon) { - return Math.abs(a - b) <= epsilon * Math.max(Math.abs(a), Math.abs(b)); - } - - /** - * Tests for equality of float values using {@link #FLOAT_EPSILON}. - * - *

- * WARNING: 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. - *

- * If this does become a concern, some ways to solve this problem: - *

    - *
  1. 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) - *
  2. Use {@link BigDecimal} instead of {@code float} (this will make a - * violation of transitivity 100% impossible) - *
- * - * @param a first value to test - * @param b second value to test - * @return whether they are equal - * @since 2019-03-18 - * @since v0.2.0 - */ - public static final boolean equals(final float a, final float b) { - return DecimalComparison.equals(a, b, FLOAT_EPSILON); - } - - /** - * Tests for float equality using a custom epsilon value. - * - *

- * WARNING: 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. - *

- * If this does become a concern, some ways to solve this problem: - *

    - *
  1. Raise the value of epsilon (this does not make a violation of - * transitivity impossible, it just significantly reduces the chances of it - * happening) - *
  2. Use {@link BigDecimal} instead of {@code float} (this will make a - * violation of transitivity 100% impossible) - *
- * - * @param a first value to test - * @param b second value to test - * @param epsilon allowed difference - * @return whether they are equal - * @since 2019-03-18 - * @since v0.2.0 - */ - public static final boolean equals(final float a, final float b, - final float epsilon) { - return Math.abs(a - b) <= epsilon * Math.max(Math.abs(a), Math.abs(b)); - } - - /** - * Tests for equality of {@code UncertainDouble} values using - * {@link #DOUBLE_EPSILON}. - *

- * WARNING: 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. - *

- * If this does become a concern, some ways to solve this problem: - *

    - *
  1. Raise the value of epsilon using - * {@link #equals(UncertainDouble, UncertainDouble, double)} (this does not - * make a violation of transitivity impossible, it just significantly reduces - * the chances of it happening) - *
  2. Use {@link BigDecimal} instead of {@code double} (this will make a - * violation of transitivity 100% impossible) - *
- * - * @param a first value to test - * @param b second value to test - * @return whether they are equal - * @since 2020-09-07 - * @see #hashCode(double) - */ - public static final boolean equals(final UncertainDouble a, - final UncertainDouble b) { - return DecimalComparison.equals(a.value(), b.value()) - && DecimalComparison.equals(a.uncertainty(), b.uncertainty()); - } - - /** - * Tests for {@code UncertainDouble} equality using a custom epsilon value. - * - *

- * WARNING: 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. - *

- * If this does become a concern, some ways to solve this problem: - *

    - *
  1. Raise the value of epsilon (this does not make a violation of - * transitivity impossible, it just significantly reduces the chances of it - * happening) - *
  2. Use {@link BigDecimal} instead of {@code double} (this will make a - * violation of transitivity 100% impossible) - *
- * - * @param a first value to test - * @param b second value to test - * @param epsilon allowed difference - * @return whether they are equal - * @since 2019-03-18 - * @since v0.2.0 - */ - public static final boolean equals(final UncertainDouble a, - final UncertainDouble b, final double epsilon) { - return DecimalComparison.equals(a.value(), b.value(), epsilon) - && DecimalComparison.equals(a.uncertainty(), b.uncertainty(), - epsilon); - } - - /** - * 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/main/java/sevenUnits/math/ExpressionParser.java b/src/main/java/sevenUnits/math/ExpressionParser.java deleted file mode 100644 index 5c8183b..0000000 --- a/src/main/java/sevenUnits/math/ExpressionParser.java +++ /dev/null @@ -1,735 +0,0 @@ -/** - * 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 . - */ -package sevenUnits.math; - -import java.util.ArrayDeque; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Deque; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.function.BinaryOperator; -import java.util.function.Function; -import java.util.function.UnaryOperator; - -/** - * An object that can parse expressions with unary or binary operators. - * - * @author Adrien Hopkins - * @param type of object that exists in parsed expressions - * @since 2019-03-14 - * @since v0.2.0 - */ -public final class ExpressionParser { - /** - * A builder that can create {@code ExpressionParser} instances. - * - * @author Adrien Hopkins - * @param type of object that exists in parsed expressions - * @since 2019-03-17 - * @since v0.2.0 - */ - public static final class Builder { - /** - * A function that obtains a parseable object from a string. For example, - * an integer {@code ExpressionParser} would use - * {@code Integer::parseInt}. - * - * @since 2019-03-14 - * @since v0.2.0 - */ - private final Function objectObtainer; - - /** - * The function of the space as an operator (like 3 x y) - * - * @since 2019-03-22 - * @since v0.2.0 - */ - private String spaceFunction = null; - - /** - * A map mapping operator strings to operator functions, for unary - * operators. - * - * @since 2019-03-14 - * @since v0.2.0 - */ - private final Map> unaryOperators; - - /** - * A map mapping operator strings to operator functions, for binary - * operators. - * - * @since 2019-03-14 - * @since v0.2.0 - */ - private final Map> binaryOperators; - - /** - * Creates the {@code Builder}. - * - * @param objectObtainer a function that can turn strings into objects of - * the type handled by the parser. - * @throws NullPointerException if {@code objectObtainer} is null - * @since 2019-03-17 - * @since v0.2.0 - */ - public Builder(final Function objectObtainer) { - this.objectObtainer = Objects.requireNonNull(objectObtainer, - "objectObtainer must not be null."); - this.unaryOperators = new HashMap<>(); - this.binaryOperators = new HashMap<>(); - } - - /** - * Adds a binary operator to the builder. - * - * @param text text used to reference the operator, like '+' - * @param operator operator to add - * @param priority operator's priority, which determines which operators - * are applied first - * @return this builder - * @throws NullPointerException if {@code text} or {@code operator} is - * null - * @since 2019-03-17 - * @since v0.2.0 - */ - public Builder addBinaryOperator(final String text, - final BinaryOperator operator, final int priority) { - Objects.requireNonNull(text, "text must not be null."); - Objects.requireNonNull(operator, "operator must not be null."); - - // Unfortunately, I cannot use a lambda because the - // PriorityBinaryOperator requires arguments. - final PriorityBinaryOperator priorityOperator = new PriorityBinaryOperator<>( - priority) { - @Override - public T apply(final T t, final T u) { - return operator.apply(t, u); - } - - }; - this.binaryOperators.put(text, priorityOperator); - return this; - } - - /** - * Adds a function for spaces. You must use the text of an existing binary - * operator. - * - * @param operator text of operator to use - * @return this builder - * @since 2019-03-22 - * @since v0.2.0 - */ - public Builder addSpaceFunction(final String operator) { - Objects.requireNonNull(operator, "operator must not be null."); - - if (!this.binaryOperators.containsKey(operator)) - throw new IllegalArgumentException(String - .format("Could not find binary operator '%s'", operator)); - - this.spaceFunction = operator; - return this; - } - - /** - * Adds a unary operator to the builder. - * - * @param text text used to reference the operator, like '-' - * @param operator operator to add - * @param priority operator's priority, which determines which operators - * are applied first - * @return this builder - * @throws NullPointerException if {@code text} or {@code operator} is - * null - * @since 2019-03-17 - * @since v0.2.0 - */ - public Builder addUnaryOperator(final String text, - final UnaryOperator operator, final int priority) { - Objects.requireNonNull(text, "text must not be null."); - Objects.requireNonNull(operator, "operator must not be null."); - - // Unfortunately, I cannot use a lambda because the - // PriorityUnaryOperator requires arguments. - final PriorityUnaryOperator priorityOperator = new PriorityUnaryOperator<>( - priority) { - @Override - public T apply(final T t) { - return operator.apply(t); - } - }; - this.unaryOperators.put(text, priorityOperator); - return this; - } - - /** - * @return an {@code ExpressionParser} instance with the properties - * given to this builder - * @since 2019-03-17 - * @since v0.2.0 - */ - public ExpressionParser build() { - return new ExpressionParser<>(this.objectObtainer, this.unaryOperators, - this.binaryOperators, this.spaceFunction); - } - } - - /** - * A binary operator with a priority field that determines which operators - * apply first. - * - * @author Adrien Hopkins - * @param type of operand and result - * @since 2019-03-17 - * @since v0.2.0 - */ - private static abstract class PriorityBinaryOperator - implements BinaryOperator, Comparable> { - /** - * The operator's priority. Higher-priority operators are applied before - * lower-priority operators - * - * @since 2019-03-17 - * @since v0.2.0 - */ - private final int priority; - - /** - * Creates the {@code PriorityBinaryOperator}. - * - * @param priority operator's priority - * @since 2019-03-17 - * @since v0.2.0 - */ - public PriorityBinaryOperator(final int priority) { - this.priority = priority; - } - - /** - * Compares this object to another by priority. - * - *

- * {@inheritDoc} - *

- * - * @since 2019-03-17 - * @since v0.2.0 - */ - @Override - public int compareTo(final PriorityBinaryOperator o) { - if (this.priority < o.priority) - return -1; - else if (this.priority > o.priority) - return 1; - else - return 0; - } - - /** - * @return priority - * @since 2019-03-22 - * @since v0.2.0 - */ - public final int getPriority() { - return this.priority; - } - } - - /** - * A unary operator with a priority field that determines which operators - * apply first. - * - * @author Adrien Hopkins - * @param type of operand and result - * @since 2019-03-17 - * @since v0.2.0 - */ - private static abstract class PriorityUnaryOperator - implements UnaryOperator, Comparable> { - /** - * The operator's priority. Higher-priority operators are applied before - * lower-priority operators - * - * @since 2019-03-17 - * @since v0.2.0 - */ - private final int priority; - - /** - * Creates the {@code PriorityUnaryOperator}. - * - * @param priority operator's priority - * @since 2019-03-17 - * @since v0.2.0 - */ - public PriorityUnaryOperator(final int priority) { - this.priority = priority; - } - - /** - * Compares this object to another by priority. - * - *

- * {@inheritDoc} - *

- * - * @since 2019-03-17 - * @since v0.2.0 - */ - @Override - public int compareTo(final PriorityUnaryOperator o) { - if (this.priority < o.priority) - return -1; - else if (this.priority > o.priority) - return 1; - else - return 0; - } - - /** - * @return priority - * @since 2019-03-22 - * @since v0.2.0 - */ - public final int getPriority() { - return this.priority; - } - } - - /** - * The types of tokens that are available. - * - * @author Adrien Hopkins - * @since 2019-03-14 - * @since v0.2.0 - */ - private static enum TokenType { - OBJECT, UNARY_OPERATOR, BINARY_OPERATOR; - } - - /** - * The opening bracket. - * - * @since 2019-03-22 - * @since v0.2.0 - */ - public static final char OPENING_BRACKET = '('; - - /** - * The closing bracket. - * - * @since 2019-03-22 - * @since v0.2.0 - */ - public static final char CLOSING_BRACKET = ')'; - - /** - * Finds the other bracket in a pair of brackets, given the position of one. - * - * @param string string that contains brackets - * @param bracketPosition position of first bracket - * @return position of matching bracket - * @throws NullPointerException if string is null - * @since 2019-03-22 - * @since v0.2.0 - */ - private static int findBracketPair(final String string, - final int bracketPosition) { - Objects.requireNonNull(string, "string must not be null."); - - final char openingBracket = string.charAt(bracketPosition); - - // figure out what closing bracket to look for - final char closingBracket; - switch (openingBracket) { - case '(': - closingBracket = ')'; - break; - case '[': - closingBracket = ']'; - break; - case '{': - closingBracket = '}'; - break; - default: - throw new IllegalArgumentException( - String.format("Invalid bracket '%s'", openingBracket)); - } - - // level of brackets. every opening bracket increments this; every closing - // bracket decrements it - int bracketLevel = 0; - - // iterate over the string to find the closing bracket - for (int currentPosition = bracketPosition; currentPosition < string - .length(); currentPosition++) { - final char currentCharacter = string.charAt(currentPosition); - - if (currentCharacter == openingBracket) { - bracketLevel++; - } else if (currentCharacter == closingBracket) { - bracketLevel--; - if (bracketLevel == 0) - return currentPosition; - } - } - - throw new IllegalArgumentException("No matching bracket found."); - } - - /** - * A function that obtains a parseable object from a string. For example, an - * integer {@code ExpressionParser} would use {@code Integer::parseInt}. - * - * @since 2019-03-14 - * @since v0.2.0 - */ - private final Function objectObtainer; - - /** - * A map mapping operator strings to operator functions, for unary operators. - * - * @since 2019-03-14 - * @since v0.2.0 - */ - private final Map> unaryOperators; - - /** - * A map mapping operator strings to operator functions, for binary - * operators. - * - * @since 2019-03-14 - * @since v0.2.0 - */ - private final Map> binaryOperators; - - /** - * The operator for space, or null if spaces have no function. - * - * @since 2019-03-22 - * @since v0.2.0 - */ - private final String spaceOperator; - - /** - * Creates the {@code ExpressionParser}. - * - * @param objectObtainer function to get objects from strings - * @param unaryOperators unary operators available to the parser - * @param binaryOperators binary operators available to the parser - * @param spaceOperator operator used by spaces - * @since 2019-03-14 - * @since v0.2.0 - */ - private ExpressionParser(final Function objectObtainer, - final Map> unaryOperators, - final Map> binaryOperators, - final String spaceOperator) { - this.objectObtainer = objectObtainer; - this.unaryOperators = unaryOperators; - this.binaryOperators = binaryOperators; - this.spaceOperator = spaceOperator; - } - - /** - * Converts a given mathematical expression to reverse Polish notation - * (operators after operands). - *

- * For example,
- * {@code 2 * (3 + 4)}
- * becomes
- * {@code 2 3 4 + *}. - * - * @param expression expression - * @return expression in RPN - * @since 2019-03-17 - * @since v0.2.0 - */ - private String convertExpressionToReversePolish(final String expression) { - Objects.requireNonNull(expression, "expression must not be null."); - - final List components = new ArrayList<>(); - - // the part of the expression remaining to parse - String partialExpression = expression; - - // find and deal with brackets - while (partialExpression.indexOf(OPENING_BRACKET) != -1) { - final int openingBracketPosition = partialExpression - .indexOf(OPENING_BRACKET); - final int closingBracketPosition = findBracketPair(partialExpression, - openingBracketPosition); - - // check for function - if (openingBracketPosition > 0 - && partialExpression.charAt(openingBracketPosition - 1) != ' ') { - // function like sin(2) or tempF(32) - // find the position of the last space - int spacePosition = openingBracketPosition; - while (spacePosition >= 0 - && partialExpression.charAt(spacePosition) != ' ') { - spacePosition--; - } - // then split the function into pre-function and function, using the - // space position - components.addAll(Arrays.asList(partialExpression - .substring(0, spacePosition + 1).split(" "))); - components.add(partialExpression.substring(spacePosition + 1, - closingBracketPosition + 1)); - partialExpression = partialExpression - .substring(closingBracketPosition + 1); - } else { - // normal brackets like (1 + 2) * (3 / 5) - components.addAll(Arrays.asList(partialExpression - .substring(0, openingBracketPosition).split(" "))); - components.add(this.convertExpressionToReversePolish( - partialExpression.substring(openingBracketPosition + 1, - closingBracketPosition))); - partialExpression = partialExpression - .substring(closingBracketPosition + 1); - } - } - - // add everything else - components.addAll(Arrays.asList(partialExpression.split(" "))); - - // remove empty entries - while (components.contains("")) { - components.remove(""); - } - - // deal with space multiplication (x y) - if (this.spaceOperator != null) { - for (int i = 0; i < components.size() - 1; i++) { - if (this.getTokenType(components.get(i)) == TokenType.OBJECT && this - .getTokenType(components.get(i + 1)) == TokenType.OBJECT) { - components.add(++i, this.spaceOperator); - } - } - } - - // turn the expression into reverse Polish - while (true) { - final int highestPriorityOperatorPosition = this - .findHighestPriorityOperatorPosition(components); - if (highestPriorityOperatorPosition == -1) { - break; - } - - // swap components based on what kind of operator there is - // 1 + 2 becomes 2 1 + - // - 1 becomes 1 - - switch (this - .getTokenType(components.get(highestPriorityOperatorPosition))) { - case UNARY_OPERATOR: - final String unaryOperator = components - .remove(highestPriorityOperatorPosition); - final String operand = components - .remove(highestPriorityOperatorPosition); - components.add(highestPriorityOperatorPosition, - operand + " " + unaryOperator); - break; - case BINARY_OPERATOR: - final String binaryOperator = components - .remove(highestPriorityOperatorPosition); - final String operand1 = components - .remove(highestPriorityOperatorPosition - 1); - final String operand2 = components - .remove(highestPriorityOperatorPosition - 1); - components.add(highestPriorityOperatorPosition - 1, - operand2 + " " + operand1 + " " + binaryOperator); - break; - default: - throw new AssertionError("Expected operator, found non-operator."); - } - } - - // join all of the components together, then ensure there is only one - // space in a row - String expressionRPN = String.join(" ", components).replaceAll(" +", " "); - - while (expressionRPN.charAt(0) == ' ') { - expressionRPN = expressionRPN.substring(1); - } - while (expressionRPN.charAt(expressionRPN.length() - 1) == ' ') { - expressionRPN = expressionRPN.substring(0, expressionRPN.length() - 1); - } - return expressionRPN; - } - - /** - * Finds the position of the highest-priority operator in a list - * - * @param components components to test - * @param blacklist positions of operators that should be ignored - * @return position of highest priority, or -1 if the list contains no - * operators - * @throws NullPointerException if components is null - * @since 2019-03-22 - * @since v0.2.0 - */ - private int findHighestPriorityOperatorPosition( - final List components) { - Objects.requireNonNull(components, "components must not be null."); - // find highest priority - int maxPriority = Integer.MIN_VALUE; - int maxPriorityPosition = -1; - - // go over components one by one - // if it is an operator, test its priority to see if it's max - // if it is, update maxPriority and maxPriorityPosition - for (int i = 0; i < components.size(); i++) { - - switch (this.getTokenType(components.get(i))) { - case UNARY_OPERATOR: - final PriorityUnaryOperator unaryOperator = this.unaryOperators - .get(components.get(i)); - final int unaryPriority = unaryOperator.getPriority(); - - if (unaryPriority > maxPriority) { - maxPriority = unaryPriority; - maxPriorityPosition = i; - } - break; - case BINARY_OPERATOR: - final PriorityBinaryOperator binaryOperator = this.binaryOperators - .get(components.get(i)); - final int binaryPriority = binaryOperator.getPriority(); - - if (binaryPriority > maxPriority) { - maxPriority = binaryPriority; - maxPriorityPosition = i; - } - break; - default: - break; - } - } - - // max priority position found - return maxPriorityPosition; - } - - /** - * Determines whether an inputted string is an object or an operator - * - * @param token string to input - * @return type of token it is - * @throws NullPointerException if {@code expression} is null - * @since 2019-03-14 - * @since v0.2.0 - */ - private TokenType getTokenType(final String token) { - Objects.requireNonNull(token, "token must not be null."); - - if (this.unaryOperators.containsKey(token)) - return TokenType.UNARY_OPERATOR; - else if (this.binaryOperators.containsKey(token)) - return TokenType.BINARY_OPERATOR; - else - return TokenType.OBJECT; - } - - /** - * Parses an expression. - * - * @param expression expression to parse - * @return result - * @throws NullPointerException if {@code expression} is null - * @since 2019-03-14 - * @since v0.2.0 - */ - public T parseExpression(final String expression) { - return this.parseReversePolishExpression( - this.convertExpressionToReversePolish(expression)); - } - - /** - * Parses an expression expressed in reverse Polish notation. - * - * @param expression expression to parse - * @return result - * @throws NullPointerException if {@code expression} is null - * @since 2019-03-14 - * @since v0.2.0 - */ - private T parseReversePolishExpression(final String expression) { - Objects.requireNonNull(expression, "expression must not be null."); - - final Deque stack = new ArrayDeque<>(); - - // iterate over every item in the expression, then - for (final String item : expression.split(" ")) { - // choose a path based on what kind of thing was just read - switch (this.getTokenType(item)) { - - case BINARY_OPERATOR: - if (stack.size() < 2) - throw new IllegalStateException(String.format( - "Attempted to call binary operator %s with only %d arguments.", - item, stack.size())); - - // get two arguments and operator, then apply! - final T o1 = stack.pop(); - final T o2 = stack.pop(); - final BinaryOperator binaryOperator = this.binaryOperators - .get(item); - - stack.push(binaryOperator.apply(o1, o2)); - break; - - case OBJECT: - // just add it to the stack - stack.push(this.objectObtainer.apply(item)); - break; - - case UNARY_OPERATOR: - if (stack.size() < 1) - throw new IllegalStateException(String.format( - "Attempted to call unary operator %s with only %d arguments.", - item, stack.size())); - - // get one argument and operator, then apply! - final T o = stack.pop(); - final UnaryOperator unaryOperator = this.unaryOperators - .get(item); - - stack.push(unaryOperator.apply(o)); - break; - default: - throw new AssertionError( - String.format("Internal error: Invalid token type %s.", - this.getTokenType(item))); - - } - } - - // return answer, or throw an exception if I can't - if (stack.size() > 1) - throw new IllegalStateException( - "Computation ended up with more than one answer."); - else if (stack.size() == 0) - throw new IllegalStateException( - "Computation ended up without an answer."); - return stack.pop(); - } -} diff --git a/src/main/java/sevenUnits/math/ObjectProduct.java b/src/main/java/sevenUnits/math/ObjectProduct.java deleted file mode 100644 index db95fd4..0000000 --- a/src/main/java/sevenUnits/math/ObjectProduct.java +++ /dev/null @@ -1,284 +0,0 @@ -/** - * 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 . - */ -package sevenUnits.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 { - /** - * Returns an empty ObjectProduct of a certain type - * - * @param - * type of objects that can be multiplied - * @return empty product - * @since 2019-10-16 - */ - public static final ObjectProduct empty() { - return new ObjectProduct<>(new HashMap<>()); - } - - /** - * Gets an {@code ObjectProduct} from an object-to-integer mapping - * - * @param - * type of object in product - * @param map - * map mapping objects to exponents - * @return object product - * @since 2019-10-16 - */ - public static final ObjectProduct fromExponentMapping(final Map 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 ObjectProduct oneOf(final T object) { - Objects.requireNonNull(object, "object must not be null."); - final Map 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 exponents; - - /** - * Creates the {@code ObjectProduct}. - * - * @param exponents - * objects that make up this product - * @since 2019-10-16 - */ - private ObjectProduct(final Map exponents) { - this.exponents = Collections.unmodifiableMap(ConditionalExistenceCollections.conditionalExistenceMap(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 dividedBy(final ObjectProduct other) { - Objects.requireNonNull(other, "other must not be null."); - // get a list of all objects in both sets - final Set objects = new HashSet<>(); - objects.addAll(this.getBaseSet()); - objects.addAll(other.getBaseSet()); - - // get a list of all exponents - final Map 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 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 getBaseSet() { - final Set 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 times(final ObjectProduct other) { - Objects.requireNonNull(other, "other must not be null."); - // get a list of all objects in both sets - final Set objects = new HashSet<>(); - objects.addAll(this.getBaseSet()); - objects.addAll(other.getBaseSet()); - - // get a list of all exponents - final Map 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 toExponent(final int exponent) { - final Map map = new HashMap<>(this.exponents); - for (final T key : this.exponents.keySet()) { - map.put(key, this.getExponent(key) * exponent); - } - return new ObjectProduct<>(map); - } - - /** - * Converts this product to a string using the objects' {@link Object#toString()} method. If objects have a long - * toString representation, it is recommended to use {@link #toString(Function)} instead to shorten the returned - * string. - * - *

- * {@inheritDoc} - */ - @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 objectToString) { - final List positiveStringComponents = new ArrayList<>(); - final List 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/main/java/sevenUnits/math/UncertainDouble.java b/src/main/java/sevenUnits/math/UncertainDouble.java deleted file mode 100644 index b81bb79..0000000 --- a/src/main/java/sevenUnits/math/UncertainDouble.java +++ /dev/null @@ -1,419 +0,0 @@ -/** - * Copyright (C) 2020 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 . - */ -package sevenUnits.math; - -import java.math.BigDecimal; -import java.math.RoundingMode; -import java.util.Objects; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -/** - * A double with an associated uncertainty value. For example, 3.2 ± 0.2. - *

- * All methods in this class throw a NullPointerException if any of their - * arguments is null. - * - * @since 2020-09-07 - */ -public final class UncertainDouble implements Comparable { - /** - * The exact value 0 - */ - public static final UncertainDouble ZERO = UncertainDouble.of(0, 0); - - /** - * A regular expression that can recognize toString forms - */ - private static final Pattern TO_STRING = Pattern - .compile("([a-zA-Z_0-9\\.\\,]+)" // a number - // optional "± [number]" - + "(?:\\s*(?:±|\\+-)\\s*([a-zA-Z_0-9\\.\\,]+))?"); - - /** - * Parses a string in the form of {@link UncertainDouble#toString(boolean)} - * and returns the corresponding {@code UncertainDouble} instance. - *

- * This method allows some alternative forms of the string representation, - * such as using "+-" instead of "±". - * - * @param s string to parse - * @return {@code UncertainDouble} instance - * @throws IllegalArgumentException if the string is invalid - * @since 2020-09-07 - */ - public static final UncertainDouble fromString(String s) { - Objects.requireNonNull(s, "s may not be null"); - final Matcher matcher = TO_STRING.matcher(s); - - double value, uncertainty; - try { - value = Double.parseDouble(matcher.group(1)); - } catch (IllegalStateException | NumberFormatException e) { - throw new IllegalArgumentException( - "String " + s + " not in correct format."); - } - - final String uncertaintyString = matcher.group(2); - if (uncertaintyString == null) { - uncertainty = 0; - } else { - try { - uncertainty = Double.parseDouble(uncertaintyString); - } catch (final NumberFormatException e) { - throw new IllegalArgumentException( - "String " + s + " not in correct format."); - } - } - - return UncertainDouble.of(value, uncertainty); - } - - /** - * Gets an {@code UncertainDouble} from its value and absolute - * uncertainty. - * - * @since 2020-09-07 - */ - public static final UncertainDouble of(double value, double uncertainty) { - return new UncertainDouble(value, uncertainty); - } - - /** - * Gets an {@code UncertainDouble} from its value and relative - * uncertainty. - * - * @since 2020-09-07 - */ - public static final UncertainDouble ofRelative(double value, - double relativeUncertainty) { - return new UncertainDouble(value, value * relativeUncertainty); - } - - private final double value; - - private final double uncertainty; - - /** - * @param value - * @param uncertainty - * @since 2020-09-07 - */ - private UncertainDouble(double value, double uncertainty) { - this.value = value; - // uncertainty should only ever be positive - this.uncertainty = Math.abs(uncertainty); - } - - /** - * Compares this {@code UncertainDouble} with another - * {@code UncertainDouble}. - *

- * This method only compares the values, not the uncertainties. So 3.1 ± 0.5 - * is considered less than 3.2 ± 0.5, even though they are equivalent. - *

- * Note: The natural ordering of this class is inconsistent with - * equals. Specifically, if two {@code UncertainDouble} instances {@code a} - * and {@code b} have the same value but different uncertainties, - * {@code a.compareTo(b)} will return 0 but {@code a.equals(b)} will return - * {@code false}. - */ - @Override - public final int compareTo(UncertainDouble o) { - return Double.compare(this.value, o.value); - } - - /** - * Returns the quotient of {@code this} and {@code other}. - * - * @since 2020-09-07 - */ - public final UncertainDouble dividedBy(UncertainDouble other) { - Objects.requireNonNull(other, "other may not be null"); - return UncertainDouble.ofRelative(this.value / other.value, Math - .hypot(this.relativeUncertainty(), other.relativeUncertainty())); - } - - /** - * Returns the quotient of {@code this} and the exact value {@code other}. - * - * @since 2020-09-07 - */ - public final UncertainDouble dividedByExact(double other) { - return UncertainDouble.of(this.value / other, this.uncertainty / other); - } - - @Override - public final boolean equals(Object obj) { - if (this == obj) - return true; - if (!(obj instanceof UncertainDouble)) - return false; - final UncertainDouble other = (UncertainDouble) obj; - if (Double.compare(this.value, other.value) != 0) - return false; - if (Double.compare(this.uncertainty, other.uncertainty) != 0) - return false; - return true; - } - - /** - * @param other another {@code UncertainDouble} - * @return true iff this and {@code other} are within each other's - * uncertainty range. - * @since 2020-09-07 - */ - public final boolean equivalent(UncertainDouble other) { - Objects.requireNonNull(other, "other may not be null"); - return Math.abs(this.value - other.value) <= Math.min(this.uncertainty, - other.uncertainty); - } - - /** - * Gets the preferred scale for rounding a value for toString. - * - * @since 2020-09-07 - */ - private final int getDisplayScale() { - // round based on uncertainty - // if uncertainty starts with 1 (ignoring zeroes and the decimal - // point), rounds - // so that uncertainty has 2 significant digits. - // otherwise, rounds so that uncertainty has 1 significant digits. - // the value is rounded to the same number of decimal places as the - // uncertainty. - final BigDecimal bigUncertainty = BigDecimal.valueOf(this.uncertainty); - - // the scale that will give the uncertainty two decimal places - final int twoDecimalPlacesScale = bigUncertainty.scale() - - bigUncertainty.precision() + 2; - final BigDecimal roundedUncertainty = bigUncertainty - .setScale(twoDecimalPlacesScale, RoundingMode.HALF_EVEN); - - if (roundedUncertainty.unscaledValue().intValue() >= 20) - return twoDecimalPlacesScale - 1; // one decimal place - else - return twoDecimalPlacesScale; - } - - @Override - public final int hashCode() { - final int prime = 31; - int result = 1; - result = prime * result + Double.hashCode(this.value); - result = prime * result + Double.hashCode(this.uncertainty); - return result; - } - - /** - * @return true iff the value has no uncertainty - * - * @since 2020-09-07 - */ - public final boolean isExact() { - return this.uncertainty == 0; - } - - /** - * Returns the difference of {@code this} and {@code other}. - * - * @since 2020-09-07 - */ - public final UncertainDouble minus(UncertainDouble other) { - Objects.requireNonNull(other, "other may not be null"); - return UncertainDouble.of(this.value - other.value, - Math.hypot(this.uncertainty, other.uncertainty)); - } - - /** - * Returns the difference of {@code this} and the exact value {@code other}. - * - * @since 2020-09-07 - */ - public final UncertainDouble minusExact(double other) { - return UncertainDouble.of(this.value - other, this.uncertainty); - } - - /** - * Returns the sum of {@code this} and {@code other}. - * - * @since 2020-09-07 - */ - public final UncertainDouble plus(UncertainDouble other) { - Objects.requireNonNull(other, "other may not be null"); - return UncertainDouble.of(this.value + other.value, - Math.hypot(this.uncertainty, other.uncertainty)); - } - - /** - * Returns the sum of {@code this} and the exact value {@code other}. - * - * @since 2020-09-07 - */ - public final UncertainDouble plusExact(double other) { - return UncertainDouble.of(this.value + other, this.uncertainty); - } - - /** - * @return relative uncertainty - * @since 2020-09-07 - */ - public final double relativeUncertainty() { - return this.uncertainty / this.value; - } - - /** - * Returns the product of {@code this} and {@code other}. - * - * @since 2020-09-07 - */ - public final UncertainDouble times(UncertainDouble other) { - Objects.requireNonNull(other, "other may not be null"); - return UncertainDouble.ofRelative(this.value * other.value, Math - .hypot(this.relativeUncertainty(), other.relativeUncertainty())); - } - - /** - * Returns the product of {@code this} and the exact value {@code other}. - * - * @since 2020-09-07 - */ - public final UncertainDouble timesExact(double other) { - return UncertainDouble.of(this.value * other, this.uncertainty * other); - } - - /** - * Returns the result of {@code this} raised to the exponent {@code other}. - * - * @since 2020-09-07 - */ - public final UncertainDouble toExponent(UncertainDouble other) { - Objects.requireNonNull(other, "other may not be null"); - - final double result = Math.pow(this.value, other.value); - final double relativeUncertainty = Math.hypot( - other.value * this.relativeUncertainty(), - Math.log(this.value) * other.uncertainty); - - return UncertainDouble.ofRelative(result, relativeUncertainty); - } - - /** - * Returns the result of {@code this} raised the exact exponent - * {@code other}. - * - * @since 2020-09-07 - */ - public final UncertainDouble toExponentExact(double other) { - return UncertainDouble.ofRelative(Math.pow(this.value, other), - this.relativeUncertainty() * other); - } - - /** - * Returns a string representation of this {@code UncertainDouble}. - *

- * This method returns the same value as {@link #toString(boolean)}, but - * {@code showUncertainty} is true if and only if the uncertainty is - * non-zero. - * - *

- * Examples: - * - *

-	 * UncertainDouble.of(3.27, 0.22).toString() = "3.3 ± 0.2"
-	 * UncertainDouble.of(3.27, 0.13).toString() = "3.27 ± 0.13"
-	 * UncertainDouble.of(-5.01, 0).toString() = "-5.01"
-	 * 
- * - * @since 2020-09-07 - */ - @Override - public final String toString() { - return this.toString(!this.isExact()); - } - - /** - * Returns a string representation of this {@code UncertainDouble}. - *

- * If {@code showUncertainty} is true, the string will be of the form "VALUE - * ± UNCERTAINTY", and if it is false the string will be of the form "VALUE" - *

- * VALUE represents a string representation of this {@code UncertainDouble}'s - * value. If the uncertainty is non-zero, the string will be rounded to the - * same precision as the uncertainty, otherwise it will not be rounded. The - * string is still rounded if {@code showUncertainty} is false.
- * UNCERTAINTY represents a string representation of this - * {@code UncertainDouble}'s uncertainty. If the uncertainty ends in 1X - * (where X represents any digit) it will be rounded to two significant - * digits otherwise it will be rounded to one significant digit. - *

- * Examples: - * - *

-	 * UncertainDouble.of(3.27, 0.22).toString(false) = "3.3"
-	 * UncertainDouble.of(3.27, 0.22).toString(true) = "3.3 ± 0.2"
-	 * UncertainDouble.of(3.27, 0.13).toString(false) = "3.27"
-	 * UncertainDouble.of(3.27, 0.13).toString(true) = "3.27 ± 0.13"
-	 * UncertainDouble.of(-5.01, 0).toString(false) = "-5.01"
-	 * UncertainDouble.of(-5.01, 0).toString(true) = "-5.01 ± 0.0"
-	 * 
- * - * @since 2020-09-07 - */ - public final String toString(boolean showUncertainty) { - String valueString, uncertaintyString; - - // generate the string representation of value and uncertainty - if (this.isExact()) { - uncertaintyString = "0.0"; - valueString = Double.toString(this.value); - - } else { - // round the value and uncertainty according to getDisplayScale() - final BigDecimal bigValue = BigDecimal.valueOf(this.value); - final BigDecimal bigUncertainty = BigDecimal.valueOf(this.uncertainty); - - final int displayScale = this.getDisplayScale(); - final BigDecimal roundedUncertainty = bigUncertainty - .setScale(displayScale, RoundingMode.HALF_EVEN); - final BigDecimal roundedValue = bigValue.setScale(displayScale, - RoundingMode.HALF_EVEN); - - valueString = roundedValue.toString(); - uncertaintyString = roundedUncertainty.toString(); - } - - // return "value" or "value ± uncertainty" depending on showUncertainty - return valueString + (showUncertainty ? " ± " + uncertaintyString : ""); - } - - /** - * @return absolute uncertainty - * @since 2020-09-07 - */ - public final double uncertainty() { - return this.uncertainty; - } - - /** - * @return value without uncertainty - * @since 2020-09-07 - */ - public final double value() { - return this.value; - } -} diff --git a/src/main/java/sevenUnits/math/package-info.java b/src/main/java/sevenUnits/math/package-info.java deleted file mode 100644 index 62f72cd..0000000 --- a/src/main/java/sevenUnits/math/package-info.java +++ /dev/null @@ -1,24 +0,0 @@ -/** - * 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 . - */ -/** - * 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 sevenUnits.math; \ No newline at end of file diff --git a/src/main/java/sevenUnits/unit/FunctionalUnit.java b/src/main/java/sevenUnits/unit/FunctionalUnit.java index 7bacc9d..df457e4 100644 --- a/src/main/java/sevenUnits/unit/FunctionalUnit.java +++ b/src/main/java/sevenUnits/unit/FunctionalUnit.java @@ -19,7 +19,7 @@ package sevenUnits.unit; import java.util.Objects; import java.util.function.DoubleUnaryOperator; -import sevenUnits.math.ObjectProduct; +import sevenUnits.utils.ObjectProduct; /** * A unit that uses functional objects to convert to and from its base. diff --git a/src/main/java/sevenUnits/unit/FunctionalUnitlike.java b/src/main/java/sevenUnits/unit/FunctionalUnitlike.java index 61503e8..2ee9e19 100644 --- a/src/main/java/sevenUnits/unit/FunctionalUnitlike.java +++ b/src/main/java/sevenUnits/unit/FunctionalUnitlike.java @@ -19,7 +19,7 @@ package sevenUnits.unit; import java.util.function.DoubleFunction; import java.util.function.ToDoubleFunction; -import sevenUnits.math.ObjectProduct; +import sevenUnits.utils.ObjectProduct; /** * A unitlike form that converts using two conversion functions. diff --git a/src/main/java/sevenUnits/unit/LinearUnit.java b/src/main/java/sevenUnits/unit/LinearUnit.java index 7c46443..25c2e2e 100644 --- a/src/main/java/sevenUnits/unit/LinearUnit.java +++ b/src/main/java/sevenUnits/unit/LinearUnit.java @@ -18,9 +18,9 @@ package sevenUnits.unit; import java.util.Objects; -import sevenUnits.math.DecimalComparison; -import sevenUnits.math.ObjectProduct; -import sevenUnits.math.UncertainDouble; +import sevenUnits.utils.DecimalComparison; +import sevenUnits.utils.ObjectProduct; +import sevenUnits.utils.UncertainDouble; /** * A unit that can be expressed as a product of its base and a number. For diff --git a/src/main/java/sevenUnits/unit/LinearUnitValue.java b/src/main/java/sevenUnits/unit/LinearUnitValue.java index 687a8b4..ffb9271 100644 --- a/src/main/java/sevenUnits/unit/LinearUnitValue.java +++ b/src/main/java/sevenUnits/unit/LinearUnitValue.java @@ -19,8 +19,8 @@ package sevenUnits.unit; import java.util.Objects; import java.util.Optional; -import sevenUnits.math.DecimalComparison; -import sevenUnits.math.UncertainDouble; +import sevenUnits.utils.DecimalComparison; +import sevenUnits.utils.UncertainDouble; /** * A possibly uncertain value expressed in a linear unit. diff --git a/src/main/java/sevenUnits/unit/MultiUnit.java b/src/main/java/sevenUnits/unit/MultiUnit.java index 014e055..83cdb03 100644 --- a/src/main/java/sevenUnits/unit/MultiUnit.java +++ b/src/main/java/sevenUnits/unit/MultiUnit.java @@ -20,7 +20,7 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.List; -import sevenUnits.math.ObjectProduct; +import sevenUnits.utils.ObjectProduct; /** * A combination of units, like "5 foot + 7 inch". All but the last units should diff --git a/src/main/java/sevenUnits/unit/SI.java b/src/main/java/sevenUnits/unit/SI.java index e0e5a54..278fb5e 100644 --- a/src/main/java/sevenUnits/unit/SI.java +++ b/src/main/java/sevenUnits/unit/SI.java @@ -18,7 +18,7 @@ package sevenUnits.unit; import java.util.Set; -import sevenUnits.math.ObjectProduct; +import sevenUnits.utils.ObjectProduct; /** * All of the units, prefixes and dimensions that are used by the SI, as well as diff --git a/src/main/java/sevenUnits/unit/Unit.java b/src/main/java/sevenUnits/unit/Unit.java index 1e875af..8fcacb8 100644 --- a/src/main/java/sevenUnits/unit/Unit.java +++ b/src/main/java/sevenUnits/unit/Unit.java @@ -23,8 +23,8 @@ import java.util.Objects; import java.util.Set; import java.util.function.DoubleUnaryOperator; -import sevenUnits.math.DecimalComparison; -import sevenUnits.math.ObjectProduct; +import sevenUnits.utils.DecimalComparison; +import sevenUnits.utils.ObjectProduct; /** * A unit that is composed of base units. diff --git a/src/main/java/sevenUnits/unit/UnitDatabase.java b/src/main/java/sevenUnits/unit/UnitDatabase.java index 1960229..a40f000 100644 --- a/src/main/java/sevenUnits/unit/UnitDatabase.java +++ b/src/main/java/sevenUnits/unit/UnitDatabase.java @@ -44,11 +44,11 @@ import java.util.function.Predicate; import java.util.regex.Matcher; import java.util.regex.Pattern; -import sevenUnits.math.ConditionalExistenceCollections; -import sevenUnits.math.DecimalComparison; -import sevenUnits.math.ExpressionParser; -import sevenUnits.math.ObjectProduct; -import sevenUnits.math.UncertainDouble; +import sevenUnits.utils.ConditionalExistenceCollections; +import sevenUnits.utils.DecimalComparison; +import sevenUnits.utils.ExpressionParser; +import sevenUnits.utils.ObjectProduct; +import sevenUnits.utils.UncertainDouble; /** * A database of units, prefixes and dimensions, and their names. diff --git a/src/main/java/sevenUnits/unit/UnitPrefix.java b/src/main/java/sevenUnits/unit/UnitPrefix.java index e4a27bd..308f4b0 100644 --- a/src/main/java/sevenUnits/unit/UnitPrefix.java +++ b/src/main/java/sevenUnits/unit/UnitPrefix.java @@ -20,7 +20,7 @@ import java.util.Objects; import java.util.Optional; import java.util.Set; -import sevenUnits.math.DecimalComparison; +import sevenUnits.utils.DecimalComparison; /** * A prefix that can be applied to a {@code LinearUnit} to multiply it by some value diff --git a/src/main/java/sevenUnits/unit/Unitlike.java b/src/main/java/sevenUnits/unit/Unitlike.java index fd361b6..d2dcbbb 100644 --- a/src/main/java/sevenUnits/unit/Unitlike.java +++ b/src/main/java/sevenUnits/unit/Unitlike.java @@ -22,7 +22,7 @@ import java.util.Objects; import java.util.function.DoubleFunction; import java.util.function.ToDoubleFunction; -import sevenUnits.math.ObjectProduct; +import sevenUnits.utils.ObjectProduct; /** * An object that can convert a value between multiple forms (instances of the diff --git a/src/main/java/sevenUnits/utils/ConditionalExistenceCollections.java b/src/main/java/sevenUnits/utils/ConditionalExistenceCollections.java new file mode 100644 index 0000000..2adb579 --- /dev/null +++ b/src/main/java/sevenUnits/utils/ConditionalExistenceCollections.java @@ -0,0 +1,468 @@ +/** + * 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 . + */ +package sevenUnits.utils; + +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. + *

+ * 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. + *

+ * 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). + *

+ * The returned collections do not 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. + *

+ * Other than that, 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. + * + * + * @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 type of element in collection + */ + static final class ConditionalExistenceCollection + extends AbstractCollection { + final Collection collection; + final Predicate existenceCondition; + + /** + * Creates the {@code ConditionalExistenceCollection}. + * + * @param collection + * @param existenceCondition + * @since 2019-10-17 + */ + private ConditionalExistenceCollection(final Collection collection, + final Predicate 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 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(); + } + + @Override + public Object[] toArray() { + // ensure the toArray operation is supported + this.collection.toArray(); + + // if it works, do it for real + return super.toArray(); + } + + @Override + public T[] toArray(T[] a) { + // ensure the toArray operation is supported + this.collection.toArray(); + + // if it works, do it for real + return super.toArray(a); + } + } + + /** + * Elements in this wrapper iterator only exist if they pass a condition. + * + * @author Adrien Hopkins + * @since 2019-10-17 + * @param type of elements in iterator + */ + static final class ConditionalExistenceIterator implements Iterator { + final Iterator iterator; + final Predicate existenceCondition; + E nextElement; + boolean hasNext; + + /** + * Creates the {@code ConditionalExistenceIterator}. + * + * @param iterator + * @param condition + * @since 2019-10-17 + */ + private ConditionalExistenceIterator(final Iterator iterator, + final Predicate 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 key type + * @param value type + */ + static final class ConditionalExistenceMap extends AbstractMap { + Map map; + Predicate> entryExistenceCondition; + + /** + * Creates the {@code ConditionalExistenceMap}. + * + * @param map + * @param entryExistenceCondition + * @since 2019-10-17 + */ + private ConditionalExistenceMap(final Map map, + final Predicate> 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 entry = new SimpleEntry<>(keyAsK, value); + return this.entryExistenceCondition.test(entry); + } + + @Override + public Set> entrySet() { + return conditionalExistenceSet(this.map.entrySet(), + this.entryExistenceCondition); + } + + @Override + public V get(final Object key) { + return this.containsKey(key) ? this.map.get(key) : null; + } + + private final Entry getEntry(K key) { + return new Entry<>() { + @Override + public K getKey() { + return key; + } + + @Override + public V getValue() { + return ConditionalExistenceMap.this.map.get(key); + } + + @Override + public V setValue(V value) { + return ConditionalExistenceMap.this.map.put(key, value); + } + }; + } + + @Override + public Set keySet() { + return conditionalExistenceSet(super.keySet(), + k -> this.entryExistenceCondition.test(this.getEntry(k))); + } + + @Override + public V put(final K key, final V value) { + final V oldValue = this.map.put(key, value); + + // get and test entry + final Entry 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 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 type of element in set + */ + static final class ConditionalExistenceSet extends AbstractSet { + private final Set set; + private final Predicate existenceCondition; + + /** + * Creates the {@code ConditionalNonexistenceSet}. + * + * @param set set to use + * @param existenceCondition condition where element exists + * @since 2019-10-17 + */ + private ConditionalExistenceSet(final Set set, + final Predicate existenceCondition) { + this.set = set; + this.existenceCondition = existenceCondition; + } + + /** + * {@inheritDoc} + *

+ * 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 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(); + } + + @Override + public Object[] toArray() { + // ensure the toArray operation is supported + this.set.toArray(); + + // if it works, do it for real + return super.toArray(); + } + + @Override + public T[] toArray(T[] a) { + // ensure the toArray operation is supported + this.set.toArray(); + + // if it works, do it for real + return super.toArray(a); + } + } + + /** + * Elements in the returned wrapper collection are ignored if they don't pass + * a condition. + * + * @param 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 Collection conditionalExistenceCollection( + final Collection collection, + final Predicate existenceCondition) { + return new ConditionalExistenceCollection<>(collection, + existenceCondition); + } + + /** + * Elements in the returned wrapper iterator are ignored if they don't pass a + * condition. + * + * @param 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 Iterator conditionalExistenceIterator( + final Iterator iterator, final Predicate existenceCondition) { + return new ConditionalExistenceIterator<>(iterator, existenceCondition); + } + + /** + * Mappings in the returned wrapper map are ignored if the corresponding + * entry doesn't pass a condition + * + * @param type of key in map + * @param 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 Map conditionalExistenceMap( + final Map map, + final Predicate> entryExistenceCondition) { + return new ConditionalExistenceMap<>(map, entryExistenceCondition); + } + + /** + * Elements in the returned wrapper set are ignored if they don't pass a + * condition. + * + * @param 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 Set conditionalExistenceSet(final Set set, + final Predicate existenceCondition) { + return new ConditionalExistenceSet<>(set, existenceCondition); + } +} diff --git a/src/main/java/sevenUnits/utils/DecimalComparison.java b/src/main/java/sevenUnits/utils/DecimalComparison.java new file mode 100644 index 0000000..a5cbbaa --- /dev/null +++ b/src/main/java/sevenUnits/utils/DecimalComparison.java @@ -0,0 +1,256 @@ +/** + * 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 . + */ +package sevenUnits.utils; + +import java.math.BigDecimal; + +/** + * A class that contains methods to compare float and double values. + * + * @author Adrien Hopkins + * @since 2019-03-18 + * @since v0.2.0 + */ +public final class DecimalComparison { + /** + * The value used for double comparison. If two double values are within this + * value multiplied by the larger value, they are considered equal. + * + * @since 2019-03-18 + * @since v0.2.0 + */ + public static final double DOUBLE_EPSILON = 1.0e-15; + + /** + * The value used for float comparison. If two float values are within this + * value multiplied by the larger value, they are considered equal. + * + * @since 2019-03-18 + * @since v0.2.0 + */ + public static final float FLOAT_EPSILON = 1.0e-6f; + + /** + * Tests for equality of double values using {@link #DOUBLE_EPSILON}. + *

+ * WARNING: 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. + *

+ * If this does become a concern, some ways to solve this problem: + *

    + *
  1. 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) + *
  2. Use {@link BigDecimal} instead of {@code double} (this will make a + * violation of transitivity 100% impossible) + *
+ * + * @param a first value to test + * @param b second value to test + * @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); + } + + /** + * Tests for double equality using a custom epsilon value. + * + *

+ * WARNING: 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. + *

+ * If this does become a concern, some ways to solve this problem: + *

    + *
  1. Raise the value of epsilon (this does not make a violation of + * transitivity impossible, it just significantly reduces the chances of it + * happening) + *
  2. Use {@link BigDecimal} instead of {@code double} (this will make a + * violation of transitivity 100% impossible) + *
+ * + * @param a first value to test + * @param b second value to test + * @param epsilon allowed difference + * @return whether they are equal + * @since 2019-03-18 + * @since v0.2.0 + */ + public static final boolean equals(final double a, final double b, + final double epsilon) { + return Math.abs(a - b) <= epsilon * Math.max(Math.abs(a), Math.abs(b)); + } + + /** + * Tests for equality of float values using {@link #FLOAT_EPSILON}. + * + *

+ * WARNING: 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. + *

+ * If this does become a concern, some ways to solve this problem: + *

    + *
  1. 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) + *
  2. Use {@link BigDecimal} instead of {@code float} (this will make a + * violation of transitivity 100% impossible) + *
+ * + * @param a first value to test + * @param b second value to test + * @return whether they are equal + * @since 2019-03-18 + * @since v0.2.0 + */ + public static final boolean equals(final float a, final float b) { + return DecimalComparison.equals(a, b, FLOAT_EPSILON); + } + + /** + * Tests for float equality using a custom epsilon value. + * + *

+ * WARNING: 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. + *

+ * If this does become a concern, some ways to solve this problem: + *

    + *
  1. Raise the value of epsilon (this does not make a violation of + * transitivity impossible, it just significantly reduces the chances of it + * happening) + *
  2. Use {@link BigDecimal} instead of {@code float} (this will make a + * violation of transitivity 100% impossible) + *
+ * + * @param a first value to test + * @param b second value to test + * @param epsilon allowed difference + * @return whether they are equal + * @since 2019-03-18 + * @since v0.2.0 + */ + public static final boolean equals(final float a, final float b, + final float epsilon) { + return Math.abs(a - b) <= epsilon * Math.max(Math.abs(a), Math.abs(b)); + } + + /** + * Tests for equality of {@code UncertainDouble} values using + * {@link #DOUBLE_EPSILON}. + *

+ * WARNING: 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. + *

+ * If this does become a concern, some ways to solve this problem: + *

    + *
  1. Raise the value of epsilon using + * {@link #equals(UncertainDouble, UncertainDouble, double)} (this does not + * make a violation of transitivity impossible, it just significantly reduces + * the chances of it happening) + *
  2. Use {@link BigDecimal} instead of {@code double} (this will make a + * violation of transitivity 100% impossible) + *
+ * + * @param a first value to test + * @param b second value to test + * @return whether they are equal + * @since 2020-09-07 + * @see #hashCode(double) + */ + public static final boolean equals(final UncertainDouble a, + final UncertainDouble b) { + return DecimalComparison.equals(a.value(), b.value()) + && DecimalComparison.equals(a.uncertainty(), b.uncertainty()); + } + + /** + * Tests for {@code UncertainDouble} equality using a custom epsilon value. + * + *

+ * WARNING: 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. + *

+ * If this does become a concern, some ways to solve this problem: + *

    + *
  1. Raise the value of epsilon (this does not make a violation of + * transitivity impossible, it just significantly reduces the chances of it + * happening) + *
  2. Use {@link BigDecimal} instead of {@code double} (this will make a + * violation of transitivity 100% impossible) + *
+ * + * @param a first value to test + * @param b second value to test + * @param epsilon allowed difference + * @return whether they are equal + * @since 2019-03-18 + * @since v0.2.0 + */ + public static final boolean equals(final UncertainDouble a, + final UncertainDouble b, final double epsilon) { + return DecimalComparison.equals(a.value(), b.value(), epsilon) + && DecimalComparison.equals(a.uncertainty(), b.uncertainty(), + epsilon); + } + + /** + * 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/main/java/sevenUnits/utils/ExpressionParser.java b/src/main/java/sevenUnits/utils/ExpressionParser.java new file mode 100644 index 0000000..1d3d44d --- /dev/null +++ b/src/main/java/sevenUnits/utils/ExpressionParser.java @@ -0,0 +1,735 @@ +/** + * 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 . + */ +package sevenUnits.utils; + +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Deque; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.function.BinaryOperator; +import java.util.function.Function; +import java.util.function.UnaryOperator; + +/** + * An object that can parse expressions with unary or binary operators. + * + * @author Adrien Hopkins + * @param type of object that exists in parsed expressions + * @since 2019-03-14 + * @since v0.2.0 + */ +public final class ExpressionParser { + /** + * A builder that can create {@code ExpressionParser} instances. + * + * @author Adrien Hopkins + * @param type of object that exists in parsed expressions + * @since 2019-03-17 + * @since v0.2.0 + */ + public static final class Builder { + /** + * A function that obtains a parseable object from a string. For example, + * an integer {@code ExpressionParser} would use + * {@code Integer::parseInt}. + * + * @since 2019-03-14 + * @since v0.2.0 + */ + private final Function objectObtainer; + + /** + * The function of the space as an operator (like 3 x y) + * + * @since 2019-03-22 + * @since v0.2.0 + */ + private String spaceFunction = null; + + /** + * A map mapping operator strings to operator functions, for unary + * operators. + * + * @since 2019-03-14 + * @since v0.2.0 + */ + private final Map> unaryOperators; + + /** + * A map mapping operator strings to operator functions, for binary + * operators. + * + * @since 2019-03-14 + * @since v0.2.0 + */ + private final Map> binaryOperators; + + /** + * Creates the {@code Builder}. + * + * @param objectObtainer a function that can turn strings into objects of + * the type handled by the parser. + * @throws NullPointerException if {@code objectObtainer} is null + * @since 2019-03-17 + * @since v0.2.0 + */ + public Builder(final Function objectObtainer) { + this.objectObtainer = Objects.requireNonNull(objectObtainer, + "objectObtainer must not be null."); + this.unaryOperators = new HashMap<>(); + this.binaryOperators = new HashMap<>(); + } + + /** + * Adds a binary operator to the builder. + * + * @param text text used to reference the operator, like '+' + * @param operator operator to add + * @param priority operator's priority, which determines which operators + * are applied first + * @return this builder + * @throws NullPointerException if {@code text} or {@code operator} is + * null + * @since 2019-03-17 + * @since v0.2.0 + */ + public Builder addBinaryOperator(final String text, + final BinaryOperator operator, final int priority) { + Objects.requireNonNull(text, "text must not be null."); + Objects.requireNonNull(operator, "operator must not be null."); + + // Unfortunately, I cannot use a lambda because the + // PriorityBinaryOperator requires arguments. + final PriorityBinaryOperator priorityOperator = new PriorityBinaryOperator<>( + priority) { + @Override + public T apply(final T t, final T u) { + return operator.apply(t, u); + } + + }; + this.binaryOperators.put(text, priorityOperator); + return this; + } + + /** + * Adds a function for spaces. You must use the text of an existing binary + * operator. + * + * @param operator text of operator to use + * @return this builder + * @since 2019-03-22 + * @since v0.2.0 + */ + public Builder addSpaceFunction(final String operator) { + Objects.requireNonNull(operator, "operator must not be null."); + + if (!this.binaryOperators.containsKey(operator)) + throw new IllegalArgumentException(String + .format("Could not find binary operator '%s'", operator)); + + this.spaceFunction = operator; + return this; + } + + /** + * Adds a unary operator to the builder. + * + * @param text text used to reference the operator, like '-' + * @param operator operator to add + * @param priority operator's priority, which determines which operators + * are applied first + * @return this builder + * @throws NullPointerException if {@code text} or {@code operator} is + * null + * @since 2019-03-17 + * @since v0.2.0 + */ + public Builder addUnaryOperator(final String text, + final UnaryOperator operator, final int priority) { + Objects.requireNonNull(text, "text must not be null."); + Objects.requireNonNull(operator, "operator must not be null."); + + // Unfortunately, I cannot use a lambda because the + // PriorityUnaryOperator requires arguments. + final PriorityUnaryOperator priorityOperator = new PriorityUnaryOperator<>( + priority) { + @Override + public T apply(final T t) { + return operator.apply(t); + } + }; + this.unaryOperators.put(text, priorityOperator); + return this; + } + + /** + * @return an {@code ExpressionParser} instance with the properties + * given to this builder + * @since 2019-03-17 + * @since v0.2.0 + */ + public ExpressionParser build() { + return new ExpressionParser<>(this.objectObtainer, this.unaryOperators, + this.binaryOperators, this.spaceFunction); + } + } + + /** + * A binary operator with a priority field that determines which operators + * apply first. + * + * @author Adrien Hopkins + * @param type of operand and result + * @since 2019-03-17 + * @since v0.2.0 + */ + private static abstract class PriorityBinaryOperator + implements BinaryOperator, Comparable> { + /** + * The operator's priority. Higher-priority operators are applied before + * lower-priority operators + * + * @since 2019-03-17 + * @since v0.2.0 + */ + private final int priority; + + /** + * Creates the {@code PriorityBinaryOperator}. + * + * @param priority operator's priority + * @since 2019-03-17 + * @since v0.2.0 + */ + public PriorityBinaryOperator(final int priority) { + this.priority = priority; + } + + /** + * Compares this object to another by priority. + * + *

+ * {@inheritDoc} + *

+ * + * @since 2019-03-17 + * @since v0.2.0 + */ + @Override + public int compareTo(final PriorityBinaryOperator o) { + if (this.priority < o.priority) + return -1; + else if (this.priority > o.priority) + return 1; + else + return 0; + } + + /** + * @return priority + * @since 2019-03-22 + * @since v0.2.0 + */ + public final int getPriority() { + return this.priority; + } + } + + /** + * A unary operator with a priority field that determines which operators + * apply first. + * + * @author Adrien Hopkins + * @param type of operand and result + * @since 2019-03-17 + * @since v0.2.0 + */ + private static abstract class PriorityUnaryOperator + implements UnaryOperator, Comparable> { + /** + * The operator's priority. Higher-priority operators are applied before + * lower-priority operators + * + * @since 2019-03-17 + * @since v0.2.0 + */ + private final int priority; + + /** + * Creates the {@code PriorityUnaryOperator}. + * + * @param priority operator's priority + * @since 2019-03-17 + * @since v0.2.0 + */ + public PriorityUnaryOperator(final int priority) { + this.priority = priority; + } + + /** + * Compares this object to another by priority. + * + *

+ * {@inheritDoc} + *

+ * + * @since 2019-03-17 + * @since v0.2.0 + */ + @Override + public int compareTo(final PriorityUnaryOperator o) { + if (this.priority < o.priority) + return -1; + else if (this.priority > o.priority) + return 1; + else + return 0; + } + + /** + * @return priority + * @since 2019-03-22 + * @since v0.2.0 + */ + public final int getPriority() { + return this.priority; + } + } + + /** + * The types of tokens that are available. + * + * @author Adrien Hopkins + * @since 2019-03-14 + * @since v0.2.0 + */ + private static enum TokenType { + OBJECT, UNARY_OPERATOR, BINARY_OPERATOR; + } + + /** + * The opening bracket. + * + * @since 2019-03-22 + * @since v0.2.0 + */ + public static final char OPENING_BRACKET = '('; + + /** + * The closing bracket. + * + * @since 2019-03-22 + * @since v0.2.0 + */ + public static final char CLOSING_BRACKET = ')'; + + /** + * Finds the other bracket in a pair of brackets, given the position of one. + * + * @param string string that contains brackets + * @param bracketPosition position of first bracket + * @return position of matching bracket + * @throws NullPointerException if string is null + * @since 2019-03-22 + * @since v0.2.0 + */ + private static int findBracketPair(final String string, + final int bracketPosition) { + Objects.requireNonNull(string, "string must not be null."); + + final char openingBracket = string.charAt(bracketPosition); + + // figure out what closing bracket to look for + final char closingBracket; + switch (openingBracket) { + case '(': + closingBracket = ')'; + break; + case '[': + closingBracket = ']'; + break; + case '{': + closingBracket = '}'; + break; + default: + throw new IllegalArgumentException( + String.format("Invalid bracket '%s'", openingBracket)); + } + + // level of brackets. every opening bracket increments this; every closing + // bracket decrements it + int bracketLevel = 0; + + // iterate over the string to find the closing bracket + for (int currentPosition = bracketPosition; currentPosition < string + .length(); currentPosition++) { + final char currentCharacter = string.charAt(currentPosition); + + if (currentCharacter == openingBracket) { + bracketLevel++; + } else if (currentCharacter == closingBracket) { + bracketLevel--; + if (bracketLevel == 0) + return currentPosition; + } + } + + throw new IllegalArgumentException("No matching bracket found."); + } + + /** + * A function that obtains a parseable object from a string. For example, an + * integer {@code ExpressionParser} would use {@code Integer::parseInt}. + * + * @since 2019-03-14 + * @since v0.2.0 + */ + private final Function objectObtainer; + + /** + * A map mapping operator strings to operator functions, for unary operators. + * + * @since 2019-03-14 + * @since v0.2.0 + */ + private final Map> unaryOperators; + + /** + * A map mapping operator strings to operator functions, for binary + * operators. + * + * @since 2019-03-14 + * @since v0.2.0 + */ + private final Map> binaryOperators; + + /** + * The operator for space, or null if spaces have no function. + * + * @since 2019-03-22 + * @since v0.2.0 + */ + private final String spaceOperator; + + /** + * Creates the {@code ExpressionParser}. + * + * @param objectObtainer function to get objects from strings + * @param unaryOperators unary operators available to the parser + * @param binaryOperators binary operators available to the parser + * @param spaceOperator operator used by spaces + * @since 2019-03-14 + * @since v0.2.0 + */ + private ExpressionParser(final Function objectObtainer, + final Map> unaryOperators, + final Map> binaryOperators, + final String spaceOperator) { + this.objectObtainer = objectObtainer; + this.unaryOperators = unaryOperators; + this.binaryOperators = binaryOperators; + this.spaceOperator = spaceOperator; + } + + /** + * Converts a given mathematical expression to reverse Polish notation + * (operators after operands). + *

+ * For example,
+ * {@code 2 * (3 + 4)}
+ * becomes
+ * {@code 2 3 4 + *}. + * + * @param expression expression + * @return expression in RPN + * @since 2019-03-17 + * @since v0.2.0 + */ + private String convertExpressionToReversePolish(final String expression) { + Objects.requireNonNull(expression, "expression must not be null."); + + final List components = new ArrayList<>(); + + // the part of the expression remaining to parse + String partialExpression = expression; + + // find and deal with brackets + while (partialExpression.indexOf(OPENING_BRACKET) != -1) { + final int openingBracketPosition = partialExpression + .indexOf(OPENING_BRACKET); + final int closingBracketPosition = findBracketPair(partialExpression, + openingBracketPosition); + + // check for function + if (openingBracketPosition > 0 + && partialExpression.charAt(openingBracketPosition - 1) != ' ') { + // function like sin(2) or tempF(32) + // find the position of the last space + int spacePosition = openingBracketPosition; + while (spacePosition >= 0 + && partialExpression.charAt(spacePosition) != ' ') { + spacePosition--; + } + // then split the function into pre-function and function, using the + // space position + components.addAll(Arrays.asList(partialExpression + .substring(0, spacePosition + 1).split(" "))); + components.add(partialExpression.substring(spacePosition + 1, + closingBracketPosition + 1)); + partialExpression = partialExpression + .substring(closingBracketPosition + 1); + } else { + // normal brackets like (1 + 2) * (3 / 5) + components.addAll(Arrays.asList(partialExpression + .substring(0, openingBracketPosition).split(" "))); + components.add(this.convertExpressionToReversePolish( + partialExpression.substring(openingBracketPosition + 1, + closingBracketPosition))); + partialExpression = partialExpression + .substring(closingBracketPosition + 1); + } + } + + // add everything else + components.addAll(Arrays.asList(partialExpression.split(" "))); + + // remove empty entries + while (components.contains("")) { + components.remove(""); + } + + // deal with space multiplication (x y) + if (this.spaceOperator != null) { + for (int i = 0; i < components.size() - 1; i++) { + if (this.getTokenType(components.get(i)) == TokenType.OBJECT && this + .getTokenType(components.get(i + 1)) == TokenType.OBJECT) { + components.add(++i, this.spaceOperator); + } + } + } + + // turn the expression into reverse Polish + while (true) { + final int highestPriorityOperatorPosition = this + .findHighestPriorityOperatorPosition(components); + if (highestPriorityOperatorPosition == -1) { + break; + } + + // swap components based on what kind of operator there is + // 1 + 2 becomes 2 1 + + // - 1 becomes 1 - + switch (this + .getTokenType(components.get(highestPriorityOperatorPosition))) { + case UNARY_OPERATOR: + final String unaryOperator = components + .remove(highestPriorityOperatorPosition); + final String operand = components + .remove(highestPriorityOperatorPosition); + components.add(highestPriorityOperatorPosition, + operand + " " + unaryOperator); + break; + case BINARY_OPERATOR: + final String binaryOperator = components + .remove(highestPriorityOperatorPosition); + final String operand1 = components + .remove(highestPriorityOperatorPosition - 1); + final String operand2 = components + .remove(highestPriorityOperatorPosition - 1); + components.add(highestPriorityOperatorPosition - 1, + operand2 + " " + operand1 + " " + binaryOperator); + break; + default: + throw new AssertionError("Expected operator, found non-operator."); + } + } + + // join all of the components together, then ensure there is only one + // space in a row + String expressionRPN = String.join(" ", components).replaceAll(" +", " "); + + while (expressionRPN.charAt(0) == ' ') { + expressionRPN = expressionRPN.substring(1); + } + while (expressionRPN.charAt(expressionRPN.length() - 1) == ' ') { + expressionRPN = expressionRPN.substring(0, expressionRPN.length() - 1); + } + return expressionRPN; + } + + /** + * Finds the position of the highest-priority operator in a list + * + * @param components components to test + * @param blacklist positions of operators that should be ignored + * @return position of highest priority, or -1 if the list contains no + * operators + * @throws NullPointerException if components is null + * @since 2019-03-22 + * @since v0.2.0 + */ + private int findHighestPriorityOperatorPosition( + final List components) { + Objects.requireNonNull(components, "components must not be null."); + // find highest priority + int maxPriority = Integer.MIN_VALUE; + int maxPriorityPosition = -1; + + // go over components one by one + // if it is an operator, test its priority to see if it's max + // if it is, update maxPriority and maxPriorityPosition + for (int i = 0; i < components.size(); i++) { + + switch (this.getTokenType(components.get(i))) { + case UNARY_OPERATOR: + final PriorityUnaryOperator unaryOperator = this.unaryOperators + .get(components.get(i)); + final int unaryPriority = unaryOperator.getPriority(); + + if (unaryPriority > maxPriority) { + maxPriority = unaryPriority; + maxPriorityPosition = i; + } + break; + case BINARY_OPERATOR: + final PriorityBinaryOperator binaryOperator = this.binaryOperators + .get(components.get(i)); + final int binaryPriority = binaryOperator.getPriority(); + + if (binaryPriority > maxPriority) { + maxPriority = binaryPriority; + maxPriorityPosition = i; + } + break; + default: + break; + } + } + + // max priority position found + return maxPriorityPosition; + } + + /** + * Determines whether an inputted string is an object or an operator + * + * @param token string to input + * @return type of token it is + * @throws NullPointerException if {@code expression} is null + * @since 2019-03-14 + * @since v0.2.0 + */ + private TokenType getTokenType(final String token) { + Objects.requireNonNull(token, "token must not be null."); + + if (this.unaryOperators.containsKey(token)) + return TokenType.UNARY_OPERATOR; + else if (this.binaryOperators.containsKey(token)) + return TokenType.BINARY_OPERATOR; + else + return TokenType.OBJECT; + } + + /** + * Parses an expression. + * + * @param expression expression to parse + * @return result + * @throws NullPointerException if {@code expression} is null + * @since 2019-03-14 + * @since v0.2.0 + */ + public T parseExpression(final String expression) { + return this.parseReversePolishExpression( + this.convertExpressionToReversePolish(expression)); + } + + /** + * Parses an expression expressed in reverse Polish notation. + * + * @param expression expression to parse + * @return result + * @throws NullPointerException if {@code expression} is null + * @since 2019-03-14 + * @since v0.2.0 + */ + private T parseReversePolishExpression(final String expression) { + Objects.requireNonNull(expression, "expression must not be null."); + + final Deque stack = new ArrayDeque<>(); + + // iterate over every item in the expression, then + for (final String item : expression.split(" ")) { + // choose a path based on what kind of thing was just read + switch (this.getTokenType(item)) { + + case BINARY_OPERATOR: + if (stack.size() < 2) + throw new IllegalStateException(String.format( + "Attempted to call binary operator %s with only %d arguments.", + item, stack.size())); + + // get two arguments and operator, then apply! + final T o1 = stack.pop(); + final T o2 = stack.pop(); + final BinaryOperator binaryOperator = this.binaryOperators + .get(item); + + stack.push(binaryOperator.apply(o1, o2)); + break; + + case OBJECT: + // just add it to the stack + stack.push(this.objectObtainer.apply(item)); + break; + + case UNARY_OPERATOR: + if (stack.size() < 1) + throw new IllegalStateException(String.format( + "Attempted to call unary operator %s with only %d arguments.", + item, stack.size())); + + // get one argument and operator, then apply! + final T o = stack.pop(); + final UnaryOperator unaryOperator = this.unaryOperators + .get(item); + + stack.push(unaryOperator.apply(o)); + break; + default: + throw new AssertionError( + String.format("Internal error: Invalid token type %s.", + this.getTokenType(item))); + + } + } + + // return answer, or throw an exception if I can't + if (stack.size() > 1) + throw new IllegalStateException( + "Computation ended up with more than one answer."); + else if (stack.size() == 0) + throw new IllegalStateException( + "Computation ended up without an answer."); + return stack.pop(); + } +} diff --git a/src/main/java/sevenUnits/utils/ObjectProduct.java b/src/main/java/sevenUnits/utils/ObjectProduct.java new file mode 100644 index 0000000..1dacb7d --- /dev/null +++ b/src/main/java/sevenUnits/utils/ObjectProduct.java @@ -0,0 +1,284 @@ +/** + * 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 . + */ +package sevenUnits.utils; + +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 { + /** + * Returns an empty ObjectProduct of a certain type + * + * @param + * type of objects that can be multiplied + * @return empty product + * @since 2019-10-16 + */ + public static final ObjectProduct empty() { + return new ObjectProduct<>(new HashMap<>()); + } + + /** + * Gets an {@code ObjectProduct} from an object-to-integer mapping + * + * @param + * type of object in product + * @param map + * map mapping objects to exponents + * @return object product + * @since 2019-10-16 + */ + public static final ObjectProduct fromExponentMapping(final Map 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 ObjectProduct oneOf(final T object) { + Objects.requireNonNull(object, "object must not be null."); + final Map 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 exponents; + + /** + * Creates the {@code ObjectProduct}. + * + * @param exponents + * objects that make up this product + * @since 2019-10-16 + */ + private ObjectProduct(final Map exponents) { + this.exponents = Collections.unmodifiableMap(ConditionalExistenceCollections.conditionalExistenceMap(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 dividedBy(final ObjectProduct other) { + Objects.requireNonNull(other, "other must not be null."); + // get a list of all objects in both sets + final Set objects = new HashSet<>(); + objects.addAll(this.getBaseSet()); + objects.addAll(other.getBaseSet()); + + // get a list of all exponents + final Map 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 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 getBaseSet() { + final Set 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 times(final ObjectProduct other) { + Objects.requireNonNull(other, "other must not be null."); + // get a list of all objects in both sets + final Set objects = new HashSet<>(); + objects.addAll(this.getBaseSet()); + objects.addAll(other.getBaseSet()); + + // get a list of all exponents + final Map 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 toExponent(final int exponent) { + final Map map = new HashMap<>(this.exponents); + for (final T key : this.exponents.keySet()) { + map.put(key, this.getExponent(key) * exponent); + } + return new ObjectProduct<>(map); + } + + /** + * Converts this product to a string using the objects' {@link Object#toString()} method. If objects have a long + * toString representation, it is recommended to use {@link #toString(Function)} instead to shorten the returned + * string. + * + *

+ * {@inheritDoc} + */ + @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 objectToString) { + final List positiveStringComponents = new ArrayList<>(); + final List 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/main/java/sevenUnits/utils/UncertainDouble.java b/src/main/java/sevenUnits/utils/UncertainDouble.java new file mode 100644 index 0000000..8fe4b31 --- /dev/null +++ b/src/main/java/sevenUnits/utils/UncertainDouble.java @@ -0,0 +1,419 @@ +/** + * Copyright (C) 2020 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 . + */ +package sevenUnits.utils; + +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.util.Objects; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * A double with an associated uncertainty value. For example, 3.2 ± 0.2. + *

+ * All methods in this class throw a NullPointerException if any of their + * arguments is null. + * + * @since 2020-09-07 + */ +public final class UncertainDouble implements Comparable { + /** + * The exact value 0 + */ + public static final UncertainDouble ZERO = UncertainDouble.of(0, 0); + + /** + * A regular expression that can recognize toString forms + */ + private static final Pattern TO_STRING = Pattern + .compile("([a-zA-Z_0-9\\.\\,]+)" // a number + // optional "± [number]" + + "(?:\\s*(?:±|\\+-)\\s*([a-zA-Z_0-9\\.\\,]+))?"); + + /** + * Parses a string in the form of {@link UncertainDouble#toString(boolean)} + * and returns the corresponding {@code UncertainDouble} instance. + *

+ * This method allows some alternative forms of the string representation, + * such as using "+-" instead of "±". + * + * @param s string to parse + * @return {@code UncertainDouble} instance + * @throws IllegalArgumentException if the string is invalid + * @since 2020-09-07 + */ + public static final UncertainDouble fromString(String s) { + Objects.requireNonNull(s, "s may not be null"); + final Matcher matcher = TO_STRING.matcher(s); + + double value, uncertainty; + try { + value = Double.parseDouble(matcher.group(1)); + } catch (IllegalStateException | NumberFormatException e) { + throw new IllegalArgumentException( + "String " + s + " not in correct format."); + } + + final String uncertaintyString = matcher.group(2); + if (uncertaintyString == null) { + uncertainty = 0; + } else { + try { + uncertainty = Double.parseDouble(uncertaintyString); + } catch (final NumberFormatException e) { + throw new IllegalArgumentException( + "String " + s + " not in correct format."); + } + } + + return UncertainDouble.of(value, uncertainty); + } + + /** + * Gets an {@code UncertainDouble} from its value and absolute + * uncertainty. + * + * @since 2020-09-07 + */ + public static final UncertainDouble of(double value, double uncertainty) { + return new UncertainDouble(value, uncertainty); + } + + /** + * Gets an {@code UncertainDouble} from its value and relative + * uncertainty. + * + * @since 2020-09-07 + */ + public static final UncertainDouble ofRelative(double value, + double relativeUncertainty) { + return new UncertainDouble(value, value * relativeUncertainty); + } + + private final double value; + + private final double uncertainty; + + /** + * @param value + * @param uncertainty + * @since 2020-09-07 + */ + private UncertainDouble(double value, double uncertainty) { + this.value = value; + // uncertainty should only ever be positive + this.uncertainty = Math.abs(uncertainty); + } + + /** + * Compares this {@code UncertainDouble} with another + * {@code UncertainDouble}. + *

+ * This method only compares the values, not the uncertainties. So 3.1 ± 0.5 + * is considered less than 3.2 ± 0.5, even though they are equivalent. + *

+ * Note: The natural ordering of this class is inconsistent with + * equals. Specifically, if two {@code UncertainDouble} instances {@code a} + * and {@code b} have the same value but different uncertainties, + * {@code a.compareTo(b)} will return 0 but {@code a.equals(b)} will return + * {@code false}. + */ + @Override + public final int compareTo(UncertainDouble o) { + return Double.compare(this.value, o.value); + } + + /** + * Returns the quotient of {@code this} and {@code other}. + * + * @since 2020-09-07 + */ + public final UncertainDouble dividedBy(UncertainDouble other) { + Objects.requireNonNull(other, "other may not be null"); + return UncertainDouble.ofRelative(this.value / other.value, Math + .hypot(this.relativeUncertainty(), other.relativeUncertainty())); + } + + /** + * Returns the quotient of {@code this} and the exact value {@code other}. + * + * @since 2020-09-07 + */ + public final UncertainDouble dividedByExact(double other) { + return UncertainDouble.of(this.value / other, this.uncertainty / other); + } + + @Override + public final boolean equals(Object obj) { + if (this == obj) + return true; + if (!(obj instanceof UncertainDouble)) + return false; + final UncertainDouble other = (UncertainDouble) obj; + if (Double.compare(this.value, other.value) != 0) + return false; + if (Double.compare(this.uncertainty, other.uncertainty) != 0) + return false; + return true; + } + + /** + * @param other another {@code UncertainDouble} + * @return true iff this and {@code other} are within each other's + * uncertainty range. + * @since 2020-09-07 + */ + public final boolean equivalent(UncertainDouble other) { + Objects.requireNonNull(other, "other may not be null"); + return Math.abs(this.value - other.value) <= Math.min(this.uncertainty, + other.uncertainty); + } + + /** + * Gets the preferred scale for rounding a value for toString. + * + * @since 2020-09-07 + */ + private final int getDisplayScale() { + // round based on uncertainty + // if uncertainty starts with 1 (ignoring zeroes and the decimal + // point), rounds + // so that uncertainty has 2 significant digits. + // otherwise, rounds so that uncertainty has 1 significant digits. + // the value is rounded to the same number of decimal places as the + // uncertainty. + final BigDecimal bigUncertainty = BigDecimal.valueOf(this.uncertainty); + + // the scale that will give the uncertainty two decimal places + final int twoDecimalPlacesScale = bigUncertainty.scale() + - bigUncertainty.precision() + 2; + final BigDecimal roundedUncertainty = bigUncertainty + .setScale(twoDecimalPlacesScale, RoundingMode.HALF_EVEN); + + if (roundedUncertainty.unscaledValue().intValue() >= 20) + return twoDecimalPlacesScale - 1; // one decimal place + else + return twoDecimalPlacesScale; + } + + @Override + public final int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + Double.hashCode(this.value); + result = prime * result + Double.hashCode(this.uncertainty); + return result; + } + + /** + * @return true iff the value has no uncertainty + * + * @since 2020-09-07 + */ + public final boolean isExact() { + return this.uncertainty == 0; + } + + /** + * Returns the difference of {@code this} and {@code other}. + * + * @since 2020-09-07 + */ + public final UncertainDouble minus(UncertainDouble other) { + Objects.requireNonNull(other, "other may not be null"); + return UncertainDouble.of(this.value - other.value, + Math.hypot(this.uncertainty, other.uncertainty)); + } + + /** + * Returns the difference of {@code this} and the exact value {@code other}. + * + * @since 2020-09-07 + */ + public final UncertainDouble minusExact(double other) { + return UncertainDouble.of(this.value - other, this.uncertainty); + } + + /** + * Returns the sum of {@code this} and {@code other}. + * + * @since 2020-09-07 + */ + public final UncertainDouble plus(UncertainDouble other) { + Objects.requireNonNull(other, "other may not be null"); + return UncertainDouble.of(this.value + other.value, + Math.hypot(this.uncertainty, other.uncertainty)); + } + + /** + * Returns the sum of {@code this} and the exact value {@code other}. + * + * @since 2020-09-07 + */ + public final UncertainDouble plusExact(double other) { + return UncertainDouble.of(this.value + other, this.uncertainty); + } + + /** + * @return relative uncertainty + * @since 2020-09-07 + */ + public final double relativeUncertainty() { + return this.uncertainty / this.value; + } + + /** + * Returns the product of {@code this} and {@code other}. + * + * @since 2020-09-07 + */ + public final UncertainDouble times(UncertainDouble other) { + Objects.requireNonNull(other, "other may not be null"); + return UncertainDouble.ofRelative(this.value * other.value, Math + .hypot(this.relativeUncertainty(), other.relativeUncertainty())); + } + + /** + * Returns the product of {@code this} and the exact value {@code other}. + * + * @since 2020-09-07 + */ + public final UncertainDouble timesExact(double other) { + return UncertainDouble.of(this.value * other, this.uncertainty * other); + } + + /** + * Returns the result of {@code this} raised to the exponent {@code other}. + * + * @since 2020-09-07 + */ + public final UncertainDouble toExponent(UncertainDouble other) { + Objects.requireNonNull(other, "other may not be null"); + + final double result = Math.pow(this.value, other.value); + final double relativeUncertainty = Math.hypot( + other.value * this.relativeUncertainty(), + Math.log(this.value) * other.uncertainty); + + return UncertainDouble.ofRelative(result, relativeUncertainty); + } + + /** + * Returns the result of {@code this} raised the exact exponent + * {@code other}. + * + * @since 2020-09-07 + */ + public final UncertainDouble toExponentExact(double other) { + return UncertainDouble.ofRelative(Math.pow(this.value, other), + this.relativeUncertainty() * other); + } + + /** + * Returns a string representation of this {@code UncertainDouble}. + *

+ * This method returns the same value as {@link #toString(boolean)}, but + * {@code showUncertainty} is true if and only if the uncertainty is + * non-zero. + * + *

+ * Examples: + * + *

+	 * UncertainDouble.of(3.27, 0.22).toString() = "3.3 ± 0.2"
+	 * UncertainDouble.of(3.27, 0.13).toString() = "3.27 ± 0.13"
+	 * UncertainDouble.of(-5.01, 0).toString() = "-5.01"
+	 * 
+ * + * @since 2020-09-07 + */ + @Override + public final String toString() { + return this.toString(!this.isExact()); + } + + /** + * Returns a string representation of this {@code UncertainDouble}. + *

+ * If {@code showUncertainty} is true, the string will be of the form "VALUE + * ± UNCERTAINTY", and if it is false the string will be of the form "VALUE" + *

+ * VALUE represents a string representation of this {@code UncertainDouble}'s + * value. If the uncertainty is non-zero, the string will be rounded to the + * same precision as the uncertainty, otherwise it will not be rounded. The + * string is still rounded if {@code showUncertainty} is false.
+ * UNCERTAINTY represents a string representation of this + * {@code UncertainDouble}'s uncertainty. If the uncertainty ends in 1X + * (where X represents any digit) it will be rounded to two significant + * digits otherwise it will be rounded to one significant digit. + *

+ * Examples: + * + *

+	 * UncertainDouble.of(3.27, 0.22).toString(false) = "3.3"
+	 * UncertainDouble.of(3.27, 0.22).toString(true) = "3.3 ± 0.2"
+	 * UncertainDouble.of(3.27, 0.13).toString(false) = "3.27"
+	 * UncertainDouble.of(3.27, 0.13).toString(true) = "3.27 ± 0.13"
+	 * UncertainDouble.of(-5.01, 0).toString(false) = "-5.01"
+	 * UncertainDouble.of(-5.01, 0).toString(true) = "-5.01 ± 0.0"
+	 * 
+ * + * @since 2020-09-07 + */ + public final String toString(boolean showUncertainty) { + String valueString, uncertaintyString; + + // generate the string representation of value and uncertainty + if (this.isExact()) { + uncertaintyString = "0.0"; + valueString = Double.toString(this.value); + + } else { + // round the value and uncertainty according to getDisplayScale() + final BigDecimal bigValue = BigDecimal.valueOf(this.value); + final BigDecimal bigUncertainty = BigDecimal.valueOf(this.uncertainty); + + final int displayScale = this.getDisplayScale(); + final BigDecimal roundedUncertainty = bigUncertainty + .setScale(displayScale, RoundingMode.HALF_EVEN); + final BigDecimal roundedValue = bigValue.setScale(displayScale, + RoundingMode.HALF_EVEN); + + valueString = roundedValue.toString(); + uncertaintyString = roundedUncertainty.toString(); + } + + // return "value" or "value ± uncertainty" depending on showUncertainty + return valueString + (showUncertainty ? " ± " + uncertaintyString : ""); + } + + /** + * @return absolute uncertainty + * @since 2020-09-07 + */ + public final double uncertainty() { + return this.uncertainty; + } + + /** + * @return value without uncertainty + * @since 2020-09-07 + */ + public final double value() { + return this.value; + } +} diff --git a/src/main/java/sevenUnits/utils/package-info.java b/src/main/java/sevenUnits/utils/package-info.java new file mode 100644 index 0000000..350c62d --- /dev/null +++ b/src/main/java/sevenUnits/utils/package-info.java @@ -0,0 +1,25 @@ +/** + * Copyright (C) 2018-2020 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 . + */ +/** + * 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 sevenUnits.utils; \ No newline at end of file -- cgit v1.2.3