diff options
author | Adrien Hopkins <adrien.p.hopkins@gmail.com> | 2020-08-27 07:06:35 -0500 |
---|---|---|
committer | Adrien Hopkins <adrien.p.hopkins@gmail.com> | 2020-08-27 07:06:35 -0500 |
commit | 0245594222bfa0bd9a47d8326ed323c7356ac27c (patch) | |
tree | 73134bb44ace529cdfa37f745ab2e4cf128c44ca | |
parent | 6d7d172e2e706da44c2b30177a04648671aad69e (diff) |
Added Complex Repetition.
5 files changed, 268 insertions, 177 deletions
diff --git a/src/org/unitConverter/converterGUI/DefaultPrefixRepetitionRule.java b/src/org/unitConverter/converterGUI/DefaultPrefixRepetitionRule.java index 34d8467..bdc3a2e 100644 --- a/src/org/unitConverter/converterGUI/DefaultPrefixRepetitionRule.java +++ b/src/org/unitConverter/converterGUI/DefaultPrefixRepetitionRule.java @@ -6,6 +6,7 @@ package org.unitConverter.converterGUI; import java.util.List; import java.util.function.Predicate; +import org.unitConverter.unit.SI; import org.unitConverter.unit.UnitPrefix; /** @@ -35,8 +36,60 @@ enum DefaultPrefixRepetitionRule implements Predicate<List<UnitPrefix>> { COMPLEX_REPETITION { @Override public boolean test(List<UnitPrefix> prefixes) { - // TODO method stub - return false; + // determine whether we are magnifying or reducing + final boolean magnifying; + if (prefixes.isEmpty()) + return true; + else if (prefixes.get(0).getMultiplier() > 1) { + magnifying = true; + } else { + magnifying = false; + } + + // if the first prefix is non-metric (including binary prefixes), + // assume we are using non-metric prefixes + // non-metric prefixes are allowed, but can't be repeated. + if (!SI.DECIMAL_PREFIXES.contains(prefixes.get(0))) + return NO_REPETITION.test(prefixes); + + int part = 0; // 0=yotta/yoctos, 1=kilo-zetta/milli-zepto, + // 2=deka,hecto,deci,centi + + for (final UnitPrefix prefix : prefixes) { + // check that the current prefix is metric and appropriately + // magnifying/reducing + if (!SI.DECIMAL_PREFIXES.contains(prefix)) + return false; + if (magnifying != prefix.getMultiplier() > 1) + return false; + + // check if the current prefix is correct + // since part is set *after* this check, part designates the state + // of the *previous* prefix + switch (part) { + case 0: + // do nothing, any prefix is valid after a yotta + break; + case 1: + // after a kilo-zetta, only deka/hecto are valid + if (SI.THOUSAND_PREFIXES.contains(prefix)) + return false; + break; + case 2: + // deka/hecto must be the last prefix, so this is always invalid + return false; + } + + // set part + if (SI.YOTTA.equals(prefix) || SI.YOCTO.equals(prefix)) { + part = 0; + } else if (SI.THOUSAND_PREFIXES.contains(prefix)) { + part = 1; + } else { + part = 2; + } + } + return true; } }; } diff --git a/src/org/unitConverter/converterGUI/UnitConverterGUI.java b/src/org/unitConverter/converterGUI/UnitConverterGUI.java index eff0c47..5fe4ee5 100644 --- a/src/org/unitConverter/converterGUI/UnitConverterGUI.java +++ b/src/org/unitConverter/converterGUI/UnitConverterGUI.java @@ -1024,8 +1024,10 @@ final class UnitConverterGUI { .build()); final JRadioButton customRepetition = new JRadioButton( - "Custom Repetition Rule"); - customRepetition.setEnabled(false); + "Complex Repetition"); + customRepetition.addActionListener( + e -> this.presenter.setPrefixRepetitionRule( + DefaultPrefixRepetitionRule.COMPLEX_REPETITION)); prefixRuleButtons.add(customRepetition); prefixRepetitionPanel.add(customRepetition, new GridBagBuilder(0, 2) diff --git a/src/org/unitConverter/math/ConditionalExistenceCollections.java b/src/org/unitConverter/math/ConditionalExistenceCollections.java index 9522885..ac1c0cf 100644 --- a/src/org/unitConverter/math/ConditionalExistenceCollections.java +++ b/src/org/unitConverter/math/ConditionalExistenceCollections.java @@ -30,20 +30,25 @@ import java.util.function.Predicate; /** * Elements in these wrapper collections only exist if they pass a condition. * <p> - * All of the collections in this class are "views" of the provided collections. They are mutable if the provided - * collections are mutable, they allow null if the provided collections allow null, they will reflect changes in the + * All of the collections in this class are "views" of the provided collections. + * They are mutable if the provided collections are mutable, they allow null if + * the provided collections allow null, they will reflect changes in the * provided collection, etc. * <p> - * The modification operations will always run the corresponding operations, even if the conditional existence - * collection doesn't change. For example, if you have a set that ignores even numbers, add(2) will still add a 2 to the + * The modification operations will always run the corresponding operations, + * even if the conditional existence collection doesn't change. For example, if + * you have a set that ignores even numbers, add(2) will still add a 2 to the * backing set (but the conditional existence set will say it doesn't exist). * <p> - * The returned collections do <i>not</i> pass the hashCode and equals operations through to the backing collections, - * but rely on {@code Object}'s {@code equals} and {@code hashCode} methods. This is necessary to preserve the contracts - * of these operations in the case that the backing collections are sets or lists. + * The returned collections do <i>not</i> pass the hashCode and equals + * operations through to the backing collections, but rely on {@code Object}'s + * {@code equals} and {@code hashCode} methods. This is necessary to preserve + * the contracts of these operations in the case that the backing collections + * are sets or lists. * <p> - * Other than that, <i>the only difference between the provided collections and the returned collections are that - * elements don't exist if they don't pass the provided condition</i>. + * Other than that, <i>the only difference between the provided collections and + * the returned collections are that elements don't exist if they don't pass the + * provided condition</i>. * * * @author Adrien Hopkins @@ -56,13 +61,13 @@ public final class ConditionalExistenceCollections { * * @author Adrien Hopkins * @since 2019-10-17 - * @param <E> - * type of element in collection + * @param <E> type of element in collection */ - static final class ConditionalExistenceCollection<E> extends AbstractCollection<E> { + static final class ConditionalExistenceCollection<E> + extends AbstractCollection<E> { final Collection<E> collection; final Predicate<E> existenceCondition; - + /** * Creates the {@code ConditionalExistenceCollection}. * @@ -70,67 +75,89 @@ public final class ConditionalExistenceCollections { * @param existenceCondition * @since 2019-10-17 */ - private ConditionalExistenceCollection(final Collection<E> collection, final Predicate<E> existenceCondition) { + private ConditionalExistenceCollection(final Collection<E> collection, + final Predicate<E> existenceCondition) { this.collection = collection; this.existenceCondition = existenceCondition; } - + @Override public boolean add(final E e) { return this.collection.add(e) && this.existenceCondition.test(e); } - + @Override public void clear() { this.collection.clear(); } - + @Override public boolean contains(final Object o) { if (!this.collection.contains(o)) return false; - + // this collection can only contain instances of E - // since the object is in the collection, we know that it must be an instance of E + // since the object is in the collection, we know that it must be an + // instance of E // therefore this cast will always work @SuppressWarnings("unchecked") final E e = (E) o; - + return this.existenceCondition.test(e); } - + @Override public Iterator<E> iterator() { - return conditionalExistenceIterator(this.collection.iterator(), this.existenceCondition); + 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 + // 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(); + 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> 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 <E> - * type of elements in iterator + * @param <E> type of elements in iterator */ static final class ConditionalExistenceIterator<E> implements Iterator<E> { final Iterator<E> iterator; final Predicate<E> existenceCondition; E nextElement; boolean hasNext; - + /** * Creates the {@code ConditionalExistenceIterator}. * @@ -138,12 +165,13 @@ public final class ConditionalExistenceCollections { * @param condition * @since 2019-10-17 */ - private ConditionalExistenceIterator(final Iterator<E> iterator, final Predicate<E> condition) { + private ConditionalExistenceIterator(final Iterator<E> iterator, + final Predicate<E> condition) { this.iterator = iterator; this.existenceCondition = condition; this.getAndSetNextElement(); } - + /** * Gets the next element, and sets nextElement and hasNext accordingly. * @@ -160,12 +188,12 @@ public final class ConditionalExistenceCollections { } while (!this.existenceCondition.test(this.nextElement)); this.hasNext = true; } - + @Override public boolean hasNext() { return this.hasNext; } - + @Override public E next() { if (this.hasNext()) { @@ -175,27 +203,25 @@ public final class ConditionalExistenceCollections { } else throw new NoSuchElementException(); } - + @Override public void remove() { this.iterator.remove(); } } - + /** * Mappings in this map only exist if the entry passes some condition. * * @author Adrien Hopkins * @since 2019-10-17 - * @param <K> - * key type - * @param <V> - * value type + * @param <K> key type + * @param <V> value type */ static final class ConditionalExistenceMap<K, V> extends AbstractMap<K, V> { Map<K, V> map; Predicate<Entry<K, V>> entryExistenceCondition; - + /** * Creates the {@code ConditionalExistenceMap}. * @@ -203,205 +229,240 @@ public final class ConditionalExistenceCollections { * @param entryExistenceCondition * @since 2019-10-17 */ - private ConditionalExistenceMap(final Map<K, V> map, final Predicate<Entry<K, V>> entryExistenceCondition) { + private ConditionalExistenceMap(final Map<K, V> map, + final Predicate<Entry<K, V>> entryExistenceCondition) { this.map = map; this.entryExistenceCondition = entryExistenceCondition; } - + @Override public boolean containsKey(final Object key) { if (!this.map.containsKey(key)) return false; - + // only instances of K have mappings in the backing map // since we know that key is a valid key, it must be an instance of K @SuppressWarnings("unchecked") final K keyAsK = (K) key; - + // get and test entry final V value = this.map.get(key); final Entry<K, V> entry = new SimpleEntry<>(keyAsK, value); return this.entryExistenceCondition.test(entry); } - + @Override public Set<Entry<K, V>> entrySet() { - return conditionalExistenceSet(this.map.entrySet(), this.entryExistenceCondition); + 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<K, V> getEntry(K key) { + return new Entry<K, V>() { + @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<K> keySet() { - // maybe change this to use ConditionalExistenceSet - return super.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<K, V> entry = new SimpleEntry<>(key, oldValue); return this.entryExistenceCondition.test(entry) ? oldValue : null; } - + @Override public V remove(final Object key) { final V oldValue = this.map.remove(key); return this.containsKey(key) ? oldValue : null; } - + @Override public Collection<V> values() { // maybe change this to use ConditionalExistenceCollection return super.values(); } - } - + /** * Elements in this set only exist if a certain condition is true. * * @author Adrien Hopkins * @since 2019-10-17 - * @param <E> - * type of element in set + * @param <E> type of element in set */ static final class ConditionalExistenceSet<E> extends AbstractSet<E> { private final Set<E> set; private final Predicate<E> existenceCondition; - + /** * Creates the {@code ConditionalNonexistenceSet}. * - * @param set - * set to use - * @param existenceCondition - * condition where element exists + * @param set set to use + * @param existenceCondition condition where element exists * @since 2019-10-17 */ - private ConditionalExistenceSet(final Set<E> set, final Predicate<E> existenceCondition) { + private ConditionalExistenceSet(final Set<E> set, + final Predicate<E> existenceCondition) { this.set = set; this.existenceCondition = existenceCondition; } - + /** * {@inheritDoc} * <p> - * Note that this method returns {@code false} if {@code e} does not pass the existence condition. + * 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 + // since the object is in the set, we know that it must be an instance + // of E // therefore this cast will always work @SuppressWarnings("unchecked") final E e = (E) o; - + return this.existenceCondition.test(e); } - + @Override public Iterator<E> iterator() { - return conditionalExistenceIterator(this.set.iterator(), this.existenceCondition); + 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 + // 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> 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. + * Elements in the returned wrapper collection are ignored if they don't pass + * a condition. * - * @param <E> - * type of elements in collection - * @param collection - * collection to wrap - * @param existenceCondition - * elements only exist if this returns true + * @param <E> type of elements in collection + * @param collection collection to wrap + * @param existenceCondition elements only exist if this returns true * @return wrapper collection * @since 2019-10-17 */ - public static final <E> Collection<E> conditionalExistenceCollection(final Collection<E> collection, + public static final <E> Collection<E> conditionalExistenceCollection( + final Collection<E> collection, final Predicate<E> existenceCondition) { - return new ConditionalExistenceCollection<>(collection, existenceCondition); + return new ConditionalExistenceCollection<>(collection, + existenceCondition); } - + /** - * Elements in the returned wrapper iterator are ignored if they don't pass a condition. + * Elements in the returned wrapper iterator are ignored if they don't pass a + * condition. * - * @param <E> - * type of elements in iterator - * @param iterator - * iterator to wrap - * @param existenceCondition - * elements only exist if this returns true + * @param <E> type of elements in iterator + * @param iterator iterator to wrap + * @param existenceCondition elements only exist if this returns true * @return wrapper iterator * @since 2019-10-17 */ - public static final <E> Iterator<E> conditionalExistenceIterator(final Iterator<E> iterator, - final Predicate<E> existenceCondition) { + public static final <E> Iterator<E> conditionalExistenceIterator( + final Iterator<E> iterator, final Predicate<E> existenceCondition) { return new ConditionalExistenceIterator<>(iterator, existenceCondition); } - + /** - * Mappings in the returned wrapper map are ignored if the corresponding entry doesn't pass a condition + * Mappings in the returned wrapper map are ignored if the corresponding + * entry doesn't pass a condition * - * @param <K> - * type of key in map - * @param <V> - * type of value in map - * @param map - * map to wrap - * @param entryExistenceCondition - * mappings only exist if this returns true + * @param <K> type of key in map + * @param <V> type of value in map + * @param map map to wrap + * @param entryExistenceCondition mappings only exist if this returns true * @return wrapper map * @since 2019-10-17 */ - public static final <K, V> Map<K, V> conditionalExistenceMap(final Map<K, V> map, + public static final <K, V> Map<K, V> conditionalExistenceMap( + final Map<K, V> map, final Predicate<Entry<K, V>> entryExistenceCondition) { return new ConditionalExistenceMap<>(map, entryExistenceCondition); } - + /** - * Elements in the returned wrapper set are ignored if they don't pass a condition. + * Elements in the returned wrapper set are ignored if they don't pass a + * condition. * - * @param <E> - * type of elements in set - * @param set - * set to wrap - * @param existenceCondition - * elements only exist if this returns true + * @param <E> type of elements in set + * @param set set to wrap + * @param existenceCondition elements only exist if this returns true * @return wrapper set * @since 2019-10-17 */ - public static final <E> Set<E> conditionalExistenceSet(final Set<E> set, final Predicate<E> existenceCondition) { + public static final <E> Set<E> conditionalExistenceSet(final Set<E> set, + final Predicate<E> existenceCondition) { return new ConditionalExistenceSet<>(set, existenceCondition); } } diff --git a/src/org/unitConverter/unit/UnitDatabase.java b/src/org/unitConverter/unit/UnitDatabase.java index 56846a1..9812bd0 100644 --- a/src/org/unitConverter/unit/UnitDatabase.java +++ b/src/org/unitConverter/unit/UnitDatabase.java @@ -1680,37 +1680,30 @@ public final class UnitDatabase { * @since 2020-08-26 */ List<UnitPrefix> getPrefixesFromName(final String unitName) { - if (this.prefixlessUnits.containsKey(unitName)) - return new ArrayList<>(); - else { + final List<UnitPrefix> prefixes = new ArrayList<>(); + String name = unitName; + + while (!this.prefixlessUnits.containsKey(name)) { // find the longest prefix - String longestPrefix = null; - int longestLength = 0; + String longestPrefixName = null; + int longestLength = name.length(); - for (final String prefixName : this.prefixes.keySet()) { - // a prefix name is valid if: - // - it is prefixed (i.e. the unit name starts with it) - // - it is longer than the existing largest prefix (since I am - // looking for the longest valid prefix) - // - the part after the prefix is a valid unit name - // - the unit described that name is a linear unit (since only - // linear units can have prefixes) - if (unitName.startsWith(prefixName) - && prefixName.length() > longestLength) { - final String rest = unitName.substring(prefixName.length()); - if (this.containsUnitName(rest) - && this.getUnit(rest) instanceof LinearUnit) { - longestPrefix = prefixName; - longestLength = prefixName.length(); - } + while (longestPrefixName == null) { + longestLength--; + if (longestLength <= 0) + throw new AssertionError( + "No prefix found in " + name + ", but it is not a unit!"); + if (this.prefixes.containsKey(name.substring(0, longestLength))) { + longestPrefixName = name.substring(0, longestLength); } } - final List<UnitPrefix> prefixes = this - .getPrefixesFromName(unitName.substring(longestLength)); - prefixes.add(this.getPrefix(longestPrefix)); - return prefixes; + // longest prefix found! + final UnitPrefix prefix = this.getPrefix(longestPrefixName); + prefixes.add(0, prefix); + name = name.substring(longestLength); } + return prefixes; } /** diff --git a/src/org/unitConverter/unit/UnitDatabaseTest.java b/src/org/unitConverter/unit/UnitDatabaseTest.java index 96a0b83..2b981b6 100644 --- a/src/org/unitConverter/unit/UnitDatabaseTest.java +++ b/src/org/unitConverter/unit/UnitDatabaseTest.java @@ -20,7 +20,6 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; -import static org.junit.jupiter.api.Assertions.fail; import java.util.Arrays; import java.util.Iterator; @@ -28,6 +27,7 @@ import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.NoSuchElementException; +import java.util.Set; import org.junit.jupiter.api.Test; @@ -55,9 +55,12 @@ class UnitDatabaseTest { .fromConversionFunctions(SI.METRE.getBase(), o -> o + 1, o -> o - 1); // make the prefix values prime so I can tell which multiplications were made - private static final UnitPrefix A = UnitPrefix.valueOf(2); - private static final UnitPrefix B = UnitPrefix.valueOf(3); - private static final UnitPrefix C = UnitPrefix.valueOf(5); + private static final UnitPrefix A = UnitPrefix.valueOf(2) + .withName(NameSymbol.ofName("A")); + private static final UnitPrefix B = UnitPrefix.valueOf(3) + .withName(NameSymbol.ofName("B")); + private static final UnitPrefix C = UnitPrefix.valueOf(5) + .withName(NameSymbol.ofName("C")); private static final UnitPrefix AB = UnitPrefix.valueOf(7); private static final UnitPrefix BC = UnitPrefix.valueOf(11); @@ -68,6 +71,7 @@ class UnitDatabaseTest { * @since 2019-05-03 */ @Test + // @Timeout(value = 5, unit = TimeUnit.SECONDS) public void testInfiniteSetExceptions() { // load units final UnitDatabase infiniteDatabase = new UnitDatabase(); @@ -79,33 +83,11 @@ class UnitDatabaseTest { infiniteDatabase.addPrefix("B", B); infiniteDatabase.addPrefix("C", C); - { - boolean exceptionThrown = false; - try { - infiniteDatabase.unitMap().entrySet().toArray(); - } catch (final IllegalStateException e) { - exceptionThrown = true; - // pass! - } finally { - if (!exceptionThrown) { - fail("No IllegalStateException thrown"); - } - } - } - - { - boolean exceptionThrown = false; - try { - infiniteDatabase.unitMap().keySet().toArray(); - } catch (final IllegalStateException e) { - exceptionThrown = true; - // pass! - } finally { - if (!exceptionThrown) { - fail("No IllegalStateException thrown"); - } - } - } + final Set<Entry<String, Unit>> entrySet = infiniteDatabase.unitMap() + .entrySet(); + final Set<String> keySet = infiniteDatabase.unitMap().keySet(); + assertThrows(IllegalStateException.class, () -> entrySet.toArray()); + assertThrows(IllegalStateException.class, () -> keySet.toArray()); } /** |