/**
* 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 org.unitConverter.unit;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;
import java.util.AbstractSet;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.NoSuchElementException;
import java.util.Objects;
import java.util.Set;
import java.util.function.BiFunction;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.unitConverter.math.DecimalComparison;
import org.unitConverter.math.ExpressionParser;
import org.unitConverter.math.ObjectProduct;
/**
* A database of units, prefixes and dimensions, and their names.
*
* @author Adrien Hopkins
* @since 2019-01-07
* @since v0.1.0
*/
public final class UnitDatabase {
/**
* A map for units that allows the use of prefixes.
*
* As this map implementation is intended to be used as a sort of "augmented
* view" of a unit and prefix map, it is unmodifiable but instead reflects
* the changes to the maps passed into it. Do not edit this map, instead edit
* the maps that were passed in during construction.
*
*
* The rules for applying prefixes onto units are the following:
*
* - Prefixes can only be applied to linear units.
* - Before attempting to search for prefixes in a unit name, this map will
* first search for a unit name. So, if there are two units, "B" and "AB",
* and a prefix "A", this map will favour the unit "AB" over the unit "B"
* with the prefix "A", even though they have the same string.
* - Longer prefixes are preferred to shorter prefixes. So, if you have
* units "BC" and "C", and prefixes "AB" and "A", inputting "ABC" will return
* the unit "C" with the prefix "AB", not "BC" with the prefix "A".
*
*
*
* This map is infinite in size if there is at least one unit and at least
* one prefix. If it is infinite, some operations that only work with finite
* collections, like converting name/entry sets to arrays, will throw an
* {@code IllegalStateException}.
*
*
* Because of ambiguities between prefixes (i.e. kilokilo = mega),
* {@link #containsValue} and {@link #values()} currently ignore prefixes.
*
*
* @author Adrien Hopkins
* @since 2019-04-13
* @since v0.2.0
*/
private static final class PrefixedUnitMap implements Map {
/**
* The class used for entry sets.
*
*
* If the map that created this set is infinite in size (has at least one
* unit and at least one prefix), this set is infinite as well. If this
* set is infinite in size, {@link #toArray} will fail with a
* {@code IllegalStateException} instead of creating an infinite-sized
* array.
*
*
* @author Adrien Hopkins
* @since 2019-04-13
* @since v0.2.0
*/
private static final class PrefixedUnitEntrySet
extends AbstractSet> {
/**
* The entry for this set.
*
* @author Adrien Hopkins
* @since 2019-04-14
* @since v0.2.0
*/
private static final class PrefixedUnitEntry
implements Entry {
private final String key;
private final Unit value;
/**
* Creates the {@code PrefixedUnitEntry}.
*
* @param key key
* @param value value
* @since 2019-04-14
* @since v0.2.0
*/
public PrefixedUnitEntry(final String key, final Unit value) {
this.key = key;
this.value = value;
}
/**
* @since 2019-05-03
*/
@Override
public boolean equals(final Object o) {
if (!(o instanceof Map.Entry))
return false;
final Map.Entry, ?> other = (Map.Entry, ?>) o;
return Objects.equals(this.getKey(), other.getKey())
&& Objects.equals(this.getValue(), other.getValue());
}
@Override
public String getKey() {
return this.key;
}
@Override
public Unit getValue() {
return this.value;
}
/**
* @since 2019-05-03
*/
@Override
public int hashCode() {
return (this.getKey() == null ? 0 : this.getKey().hashCode())
^ (this.getValue() == null ? 0
: this.getValue().hashCode());
}
@Override
public Unit setValue(final Unit value) {
throw new UnsupportedOperationException(
"Cannot set value in an immutable entry");
}
/**
* Returns a string representation of the entry. The format of the
* string is the string representation of the key, then the equals
* ({@code =}) character, then the string representation of the
* value.
*
* @since 2019-05-03
*/
@Override
public String toString() {
return this.getKey() + "=" + this.getValue();
}
}
/**
* An iterator that iterates over the units of a
* {@code PrefixedUnitNameSet}.
*
* @author Adrien Hopkins
* @since 2019-04-14
* @since v0.2.0
*/
private static final class PrefixedUnitEntryIterator
implements Iterator> {
// position in the unit list
private int unitNamePosition = 0;
// the indices of the prefixes attached to the current unit
private final List prefixCoordinates = new ArrayList<>();
// values from the unit entry set
private final Map map;
private transient final List unitNames;
private transient final List prefixNames;
/**
* Creates the
* {@code UnitsDatabase.PrefixedUnitMap.PrefixedUnitNameSet.PrefixedUnitNameIterator}.
*
* @since 2019-04-14
* @since v0.2.0
*/
public PrefixedUnitEntryIterator(final PrefixedUnitMap map) {
this.map = map;
this.unitNames = new ArrayList<>(map.units.keySet());
this.prefixNames = new ArrayList<>(map.prefixes.keySet());
}
/**
* @return current unit name
* @since 2019-04-14
* @since v0.2.0
*/
private String getCurrentUnitName() {
final StringBuilder unitName = new StringBuilder();
for (final int i : this.prefixCoordinates) {
unitName.append(this.prefixNames.get(i));
}
unitName.append(this.unitNames.get(this.unitNamePosition));
return unitName.toString();
}
@Override
public boolean hasNext() {
if (this.unitNames.isEmpty())
return false;
else {
if (this.prefixNames.isEmpty())
return this.unitNamePosition >= this.unitNames.size() - 1;
else
return true;
}
}
/**
* Changes this iterator's position to the next available one.
*
* @since 2019-04-14
* @since v0.2.0
*/
private void incrementPosition() {
this.unitNamePosition++;
if (this.unitNamePosition >= this.unitNames.size()) {
// we have used all of our units, go to a different prefix
this.unitNamePosition = 0;
// if the prefix coordinates are empty, then set it to [0]
if (this.prefixCoordinates.isEmpty()) {
this.prefixCoordinates.add(0, 0);
} else {
// get the prefix coordinate to increment, then increment
int i = this.prefixCoordinates.size() - 1;
this.prefixCoordinates.set(i,
this.prefixCoordinates.get(i) + 1);
// fix any carrying errors
while (i >= 0 && this.prefixCoordinates
.get(i) >= this.prefixNames.size()) {
// carry over
this.prefixCoordinates.set(i--, 0); // null and
// decrement at the
// same time
if (i < 0) { // we need to add a new coordinate
this.prefixCoordinates.add(0, 0);
} else { // increment an existing one
this.prefixCoordinates.set(i,
this.prefixCoordinates.get(i) + 1);
}
}
}
}
}
@Override
public Entry next() {
// get next element
final Entry nextEntry = this.peek();
// iterate to next position
this.incrementPosition();
return nextEntry;
}
/**
* @return the next element in the iterator, without iterating over
* it
* @since 2019-05-03
*/
private Entry peek() {
if (!this.hasNext())
throw new NoSuchElementException("No units left!");
// if I have prefixes, ensure I'm not using a nonlinear unit
// since all of the unprefixed stuff is done, just remove
// nonlinear units
if (!this.prefixCoordinates.isEmpty()) {
while (this.unitNamePosition < this.unitNames.size()
&& !(this.map.get(this.unitNames.get(
this.unitNamePosition)) instanceof LinearUnit)) {
this.unitNames.remove(this.unitNamePosition);
}
}
final String nextName = this.getCurrentUnitName();
return new PrefixedUnitEntry(nextName, this.map.get(nextName));
}
/**
* Returns a string representation of the object. The exact details
* of the representation are unspecified and subject to change.
*
* @since 2019-05-03
*/
@Override
public String toString() {
return String.format(
"Iterator iterating over name-unit entries; next value is \"%s\"",
this.peek());
}
}
// the map that created this set
private final PrefixedUnitMap map;
/**
* Creates the {@code PrefixedUnitNameSet}.
*
* @param map map that created this set
* @since 2019-04-13
* @since v0.2.0
*/
public PrefixedUnitEntrySet(final PrefixedUnitMap map) {
this.map = map;
}
@Override
public boolean add(final Map.Entry e) {
throw new UnsupportedOperationException(
"Cannot add to an immutable set");
}
@Override
public boolean addAll(
final Collection extends Map.Entry> c) {
throw new UnsupportedOperationException(
"Cannot add to an immutable set");
}
@Override
public void clear() {
throw new UnsupportedOperationException(
"Cannot clear an immutable set");
}
@Override
public boolean contains(final Object o) {
// get the entry
final Entry entry;
try {
// This is OK because I'm in a try-catch block, catching the
// exact exception that would be thrown.
@SuppressWarnings("unchecked")
final Entry tempEntry = (Entry) o;
entry = tempEntry;
} catch (final ClassCastException e) {
throw new IllegalArgumentException(
"Attempted to test for an entry using a non-entry.");
}
return this.map.containsKey(entry.getKey())
&& this.map.get(entry.getKey()).equals(entry.getValue());
}
@Override
public boolean containsAll(final Collection> c) {
for (final Object o : c)
if (!this.contains(o))
return false;
return true;
}
@Override
public boolean isEmpty() {
return this.map.isEmpty();
}
@Override
public Iterator> iterator() {
return new PrefixedUnitEntryIterator(this.map);
}
@Override
public boolean remove(final Object o) {
throw new UnsupportedOperationException(
"Cannot remove from an immutable set");
}
@Override
public boolean removeAll(final Collection> c) {
throw new UnsupportedOperationException(
"Cannot remove from an immutable set");
}
@Override
public boolean removeIf(
final Predicate super Entry> filter) {
throw new UnsupportedOperationException(
"Cannot remove from an immutable set");
}
@Override
public boolean retainAll(final Collection> c) {
throw new UnsupportedOperationException(
"Cannot remove from an immutable set");
}
@Override
public int size() {
if (this.map.units.isEmpty())
return 0;
else {
if (this.map.prefixes.isEmpty())
return this.map.units.size();
else
// infinite set
return Integer.MAX_VALUE;
}
}
/**
* @throws IllegalStateException if the set is infinite in size
*/
@Override
public Object[] toArray() {
if (this.map.units.isEmpty() || this.map.prefixes.isEmpty())
return super.toArray();
else
// infinite set
throw new IllegalStateException(
"Cannot make an infinite set into an array.");
}
/**
* @throws IllegalStateException if the set is infinite in size
*/
@Override
public T[] toArray(final T[] a) {
if (this.map.units.isEmpty() || this.map.prefixes.isEmpty())
return super.toArray(a);
else
// infinite set
throw new IllegalStateException(
"Cannot make an infinite set into an array.");
}
@Override
public String toString() {
if (this.map.units.isEmpty() || this.map.prefixes.isEmpty())
return super.toString();
else
return String.format(
"Infinite set of name-unit entries created from units %s and prefixes %s",
this.map.units, this.map.prefixes);
}
}
/**
* The class used for unit name sets.
*
*
* If the map that created this set is infinite in size (has at least one
* unit and at least one prefix), this set is infinite as well. If this
* set is infinite in size, {@link #toArray} will fail with a
* {@code IllegalStateException} instead of creating an infinite-sized
* array.
*
*
* @author Adrien Hopkins
* @since 2019-04-13
* @since v0.2.0
*/
private static final class PrefixedUnitNameSet
extends AbstractSet {
/**
* An iterator that iterates over the units of a
* {@code PrefixedUnitNameSet}.
*
* @author Adrien Hopkins
* @since 2019-04-14
* @since v0.2.0
*/
private static final class PrefixedUnitNameIterator
implements Iterator {
// position in the unit list
private int unitNamePosition = 0;
// the indices of the prefixes attached to the current unit
private final List prefixCoordinates = new ArrayList<>();
// values from the unit name set
private final Map map;
private transient final List unitNames;
private transient final List prefixNames;
/**
* Creates the
* {@code UnitsDatabase.PrefixedUnitMap.PrefixedUnitNameSet.PrefixedUnitNameIterator}.
*
* @since 2019-04-14
* @since v0.2.0
*/
public PrefixedUnitNameIterator(final PrefixedUnitMap map) {
this.map = map;
this.unitNames = new ArrayList<>(map.units.keySet());
this.prefixNames = new ArrayList<>(map.prefixes.keySet());
}
/**
* @return current unit name
* @since 2019-04-14
* @since v0.2.0
*/
private String getCurrentUnitName() {
final StringBuilder unitName = new StringBuilder();
for (final int i : this.prefixCoordinates) {
unitName.append(this.prefixNames.get(i));
}
unitName.append(this.unitNames.get(this.unitNamePosition));
return unitName.toString();
}
@Override
public boolean hasNext() {
if (this.unitNames.isEmpty())
return false;
else {
if (this.prefixNames.isEmpty())
return this.unitNamePosition >= this.unitNames.size() - 1;
else
return true;
}
}
/**
* Changes this iterator's position to the next available one.
*
* @since 2019-04-14
* @since v0.2.0
*/
private void incrementPosition() {
this.unitNamePosition++;
if (this.unitNamePosition >= this.unitNames.size()) {
// we have used all of our units, go to a different prefix
this.unitNamePosition = 0;
// if the prefix coordinates are empty, then set it to [0]
if (this.prefixCoordinates.isEmpty()) {
this.prefixCoordinates.add(0, 0);
} else {
// get the prefix coordinate to increment, then increment
int i = this.prefixCoordinates.size() - 1;
this.prefixCoordinates.set(i,
this.prefixCoordinates.get(i) + 1);
// fix any carrying errors
while (i >= 0 && this.prefixCoordinates
.get(i) >= this.prefixNames.size()) {
// carry over
this.prefixCoordinates.set(i--, 0); // null and
// decrement at the
// same time
if (i < 0) { // we need to add a new coordinate
this.prefixCoordinates.add(0, 0);
} else { // increment an existing one
this.prefixCoordinates.set(i,
this.prefixCoordinates.get(i) + 1);
}
}
}
}
}
@Override
public String next() {
final String nextName = this.peek();
this.incrementPosition();
return nextName;
}
/**
* @return the next element in the iterator, without iterating over
* it
* @since 2019-05-03
*/
private String peek() {
if (!this.hasNext())
throw new NoSuchElementException("No units left!");
// if I have prefixes, ensure I'm not using a nonlinear unit
// since all of the unprefixed stuff is done, just remove
// nonlinear units
if (!this.prefixCoordinates.isEmpty()) {
while (this.unitNamePosition < this.unitNames.size()
&& !(this.map.get(this.unitNames.get(
this.unitNamePosition)) instanceof LinearUnit)) {
this.unitNames.remove(this.unitNamePosition);
}
}
return this.getCurrentUnitName();
}
/**
* Returns a string representation of the object. The exact details
* of the representation are unspecified and subject to change.
*
* @since 2019-05-03
*/
@Override
public String toString() {
return String.format(
"Iterator iterating over unit names; next value is \"%s\"",
this.peek());
}
}
// the map that created this set
private final PrefixedUnitMap map;
/**
* Creates the {@code PrefixedUnitNameSet}.
*
* @param map map that created this set
* @since 2019-04-13
* @since v0.2.0
*/
public PrefixedUnitNameSet(final PrefixedUnitMap map) {
this.map = map;
}
@Override
public boolean add(final String e) {
throw new UnsupportedOperationException(
"Cannot add to an immutable set");
}
@Override
public boolean addAll(final Collection extends String> c) {
throw new UnsupportedOperationException(
"Cannot add to an immutable set");
}
@Override
public void clear() {
throw new UnsupportedOperationException(
"Cannot clear an immutable set");
}
@Override
public boolean contains(final Object o) {
return this.map.containsKey(o);
}
@Override
public boolean containsAll(final Collection> c) {
for (final Object o : c)
if (!this.contains(o))
return false;
return true;
}
@Override
public boolean isEmpty() {
return this.map.isEmpty();
}
@Override
public Iterator iterator() {
return new PrefixedUnitNameIterator(this.map);
}
@Override
public boolean remove(final Object o) {
throw new UnsupportedOperationException(
"Cannot remove from an immutable set");
}
@Override
public boolean removeAll(final Collection> c) {
throw new UnsupportedOperationException(
"Cannot remove from an immutable set");
}
@Override
public boolean removeIf(final Predicate super String> filter) {
throw new UnsupportedOperationException(
"Cannot remove from an immutable set");
}
@Override
public boolean retainAll(final Collection> c) {
throw new UnsupportedOperationException(
"Cannot remove from an immutable set");
}
@Override
public int size() {
if (this.map.units.isEmpty())
return 0;
else {
if (this.map.prefixes.isEmpty())
return this.map.units.size();
else
// infinite set
return Integer.MAX_VALUE;
}
}
/**
* @throws IllegalStateException if the set is infinite in size
*/
@Override
public Object[] toArray() {
if (this.map.units.isEmpty() || this.map.prefixes.isEmpty())
return super.toArray();
else
// infinite set
throw new IllegalStateException(
"Cannot make an infinite set into an array.");
}
/**
* @throws IllegalStateException if the set is infinite in size
*/
@Override
public T[] toArray(final T[] a) {
if (this.map.units.isEmpty() || this.map.prefixes.isEmpty())
return super.toArray(a);
else
// infinite set
throw new IllegalStateException(
"Cannot make an infinite set into an array.");
}
@Override
public String toString() {
if (this.map.units.isEmpty() || this.map.prefixes.isEmpty())
return super.toString();
else
return String.format(
"Infinite set of name-unit entries created from units %s and prefixes %s",
this.map.units, this.map.prefixes);
}
}
/**
* The units stored in this collection, without prefixes.
*
* @since 2019-04-13
* @since v0.2.0
*/
private final Map units;
/**
* The available prefixes for use.
*
* @since 2019-04-13
* @since v0.2.0
*/
private final Map prefixes;
// caches
private transient Collection values = null;
private transient Set keySet = null;
private transient Set> entrySet = null;
/**
* Creates the {@code PrefixedUnitMap}.
*
* @param units map mapping unit names to units
* @param prefixes map mapping prefix names to prefixes
* @since 2019-04-13
* @since v0.2.0
*/
public PrefixedUnitMap(final Map units,
final Map prefixes) {
// I am making unmodifiable maps to ensure I don't accidentally make
// changes.
this.units = Collections.unmodifiableMap(units);
this.prefixes = Collections.unmodifiableMap(prefixes);
}
@Override
public void clear() {
throw new UnsupportedOperationException(
"Cannot clear an immutable map");
}
@Override
public Unit compute(final String key,
final BiFunction super String, ? super Unit, ? extends Unit> remappingFunction) {
throw new UnsupportedOperationException(
"Cannot edit an immutable map");
}
@Override
public Unit computeIfAbsent(final String key,
final Function super String, ? extends Unit> mappingFunction) {
throw new UnsupportedOperationException(
"Cannot edit an immutable map");
}
@Override
public Unit computeIfPresent(final String key,
final BiFunction super String, ? super Unit, ? extends Unit> remappingFunction) {
throw new UnsupportedOperationException(
"Cannot edit an immutable map");
}
@Override
public boolean containsKey(final Object key) {
// First, test if there is a unit with the key
if (this.units.containsKey(key))
return true;
// Next, try to cast it to String
if (!(key instanceof String))
throw new IllegalArgumentException(
"Attempted to test for a unit using a non-string name.");
final String unitName = (String) key;
// Then, look for the longest prefix that is attached to a valid unit
String longestPrefix = null;
int longestLength = 0;
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.containsKey(rest)
&& this.get(rest) instanceof LinearUnit) {
longestPrefix = prefixName;
longestLength = prefixName.length();
}
}
}
return longestPrefix != null;
}
/**
* {@inheritDoc}
*
*
* Because of ambiguities between prefixes (i.e. kilokilo = mega), this
* method only tests for prefixless units.
*
*/
@Override
public boolean containsValue(final Object value) {
return this.units.containsValue(value);
}
@Override
public Set> entrySet() {
if (this.entrySet == null) {
this.entrySet = new PrefixedUnitEntrySet(this);
}
return this.entrySet;
}
@Override
public Unit get(final Object key) {
// First, test if there is a unit with the key
if (this.units.containsKey(key))
return this.units.get(key);
// Next, try to cast it to String
if (!(key instanceof String))
throw new IllegalArgumentException(
"Attempted to obtain a unit using a non-string name.");
final String unitName = (String) key;
// Then, look for the longest prefix that is attached to a valid unit
String longestPrefix = null;
int longestLength = 0;
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.containsKey(rest)
&& this.get(rest) instanceof LinearUnit) {
longestPrefix = prefixName;
longestLength = prefixName.length();
}
}
}
// if none found, returns null
if (longestPrefix == null)
return null;
else {
// get necessary data
final String rest = unitName.substring(longestLength);
// this cast will not fail because I verified that it would work
// before selecting this prefix
final LinearUnit unit = (LinearUnit) this.get(rest);
final UnitPrefix prefix = this.prefixes.get(longestPrefix);
return unit.withPrefix(prefix);
}
}
@Override
public boolean isEmpty() {
return this.units.isEmpty();
}
@Override
public Set keySet() {
if (this.keySet == null) {
this.keySet = new PrefixedUnitNameSet(this);
}
return this.keySet;
}
@Override
public Unit merge(final String key, final Unit value,
final BiFunction super Unit, ? super Unit, ? extends Unit> remappingFunction) {
throw new UnsupportedOperationException(
"Cannot merge into an immutable map");
}
@Override
public Unit put(final String key, final Unit value) {
throw new UnsupportedOperationException(
"Cannot add entries to an immutable map");
}
@Override
public void putAll(final Map extends String, ? extends Unit> m) {
throw new UnsupportedOperationException(
"Cannot add entries to an immutable map");
}
@Override
public Unit putIfAbsent(final String key, final Unit value) {
throw new UnsupportedOperationException(
"Cannot add entries to an immutable map");
}
@Override
public Unit remove(final Object key) {
throw new UnsupportedOperationException(
"Cannot remove entries from an immutable map");
}
@Override
public boolean remove(final Object key, final Object value) {
throw new UnsupportedOperationException(
"Cannot remove entries from an immutable map");
}
@Override
public Unit replace(final String key, final Unit value) {
throw new UnsupportedOperationException(
"Cannot replace entries in an immutable map");
}
@Override
public boolean replace(final String key, final Unit oldValue,
final Unit newValue) {
throw new UnsupportedOperationException(
"Cannot replace entries in an immutable map");
}
@Override
public void replaceAll(
final BiFunction super String, ? super Unit, ? extends Unit> function) {
throw new UnsupportedOperationException(
"Cannot replace entries in an immutable map");
}
@Override
public int size() {
if (this.units.isEmpty())
return 0;
else {
if (this.prefixes.isEmpty())
return this.units.size();
else
// infinite set
return Integer.MAX_VALUE;
}
}
@Override
public String toString() {
if (this.units.isEmpty() || this.prefixes.isEmpty())
return super.toString();
else
return String.format(
"Infinite map of name-unit entries created from units %s and prefixes %s",
this.units, this.prefixes);
}
/**
* {@inheritDoc}
*
*
* Because of ambiguities between prefixes (i.e. kilokilo = mega), this
* method ignores prefixes.
*
*/
@Override
public Collection values() {
if (this.values == null) {
this.values = Collections
.unmodifiableCollection(this.units.values());
}
return this.values;
}
}
/**
* Replacements done to *all* expression types
*/
private static final Map EXPRESSION_REPLACEMENTS = new HashMap<>();
// add data to expression replacements
static {
// add spaces around operators
for (final String operator : Arrays.asList("\\*", "/", "\\^")) {
EXPRESSION_REPLACEMENTS.put(Pattern.compile(operator),
" " + operator + " ");
}
// replace multiple spaces with a single space
EXPRESSION_REPLACEMENTS.put(Pattern.compile(" +"), " ");
// place brackets around any expression of the form "number unit", with or
// without the space
EXPRESSION_REPLACEMENTS.put(Pattern.compile("((?:-?[1-9]\\d*|0)" // integer
+ "(?:\\.\\d+(?:[eE]\\d+))?)" // optional decimal point with numbers
// after it
+ "\\s*" // optional space(s)
+ "([a-zA-Z]+(?:\\^\\d+)?" // any string of letters
+ "(?:\\s+[a-zA-Z]+(?:\\^\\d+)?))" // optional other letters
+ "(?!-?\\d)" // no number directly afterwards (avoids matching
// "1e3")
), "\\($1 $2\\)");
}
/**
* A regular expression that separates names and expressions in unit files.
*/
private static final Pattern NAME_EXPRESSION = Pattern
.compile("(\\S+)\\s+(\\S.*)");
/**
* The exponent operator
*
* @param base base of exponentiation
* @param exponentUnit exponent
* @return result
* @since 2019-04-10
* @since v0.2.0
*/
private static final LinearUnit exponentiateUnits(final LinearUnit base,
final LinearUnit exponentUnit) {
// exponent function - first check if o2 is a number,
if (exponentUnit.getBase().equals(SI.ONE.getBase())) {
// then check if it is an integer,
final double exponent = exponentUnit.getConversionFactor();
if (DecimalComparison.equals(exponent % 1, 0))
// then exponentiate
return base.toExponent((int) (exponent + 0.5));
else
// not an integer
throw new UnsupportedOperationException(
"Decimal exponents are currently not supported.");
} else
// not a number
throw new IllegalArgumentException("Exponents must be numbers.");
}
/**
* The exponent operator
*
* @param base base of exponentiation
* @param exponentUnit exponent
* @return result
* @since 2020-08-04
*/
private static final LinearUnitValue exponentiateUnitValues(
final LinearUnitValue base, final LinearUnitValue exponentValue) {
// exponent function - first check if o2 is a number,
if (exponentValue.canConvertTo(SI.ONE)) {
// then check if it is an integer,
final double exponent = exponentValue.getValue();
if (DecimalComparison.equals(exponent % 1, 0))
// then exponentiate
return base.toExponent((int) (exponent + 0.5));
else
// not an integer
throw new UnsupportedOperationException(
"Decimal exponents are currently not supported.");
} else
// not a number
throw new IllegalArgumentException("Exponents must be numbers.");
}
/**
* The units in this system, excluding prefixes.
*
* @since 2019-01-07
* @since v0.1.0
*/
private final Map prefixlessUnits;
/**
* The unit prefixes in this system.
*
* @since 2019-01-14
* @since v0.1.0
*/
private final Map prefixes;
/**
* The dimensions in this system.
*
* @since 2019-03-14
* @since v0.2.0
*/
private final Map> dimensions;
/**
* A map mapping strings to units (including prefixes)
*
* @since 2019-04-13
* @since v0.2.0
*/
private final Map units;
/**
* A parser that can parse unit expressions.
*
* @since 2019-03-22
* @since v0.2.0
*/
private final ExpressionParser unitExpressionParser = new ExpressionParser.Builder<>(
this::getLinearUnit).addBinaryOperator("+", (o1, o2) -> o1.plus(o2), 0)
.addBinaryOperator("-", (o1, o2) -> o1.minus(o2), 0)
.addBinaryOperator("*", (o1, o2) -> o1.times(o2), 1)
.addSpaceFunction("*")
.addBinaryOperator("/", (o1, o2) -> o1.dividedBy(o2), 1)
.addBinaryOperator("^", UnitDatabase::exponentiateUnits, 2)
.build();
/**
* A parser that can parse unit value expressions.
*
* @since 2020-08-04
*/
private final ExpressionParser unitValueExpressionParser = new ExpressionParser.Builder<>(
this::getLinearUnitValue)
.addBinaryOperator("+", (o1, o2) -> o1.plus(o2), 0)
.addBinaryOperator("-", (o1, o2) -> o1.minus(o2), 0)
.addBinaryOperator("*", (o1, o2) -> o1.times(o2), 1)
.addSpaceFunction("*")
.addBinaryOperator("/", (o1, o2) -> o1.dividedBy(o2), 1)
.addBinaryOperator("^", UnitDatabase::exponentiateUnitValues, 2)
.build();
/**
* A parser that can parse unit prefix expressions
*
* @since 2019-04-13
* @since v0.2.0
*/
private final ExpressionParser prefixExpressionParser = new ExpressionParser.Builder<>(
this::getPrefix).addBinaryOperator("*", (o1, o2) -> o1.times(o2), 0)
.addSpaceFunction("*")
.addBinaryOperator("/", (o1, o2) -> o1.dividedBy(o2), 0)
.addBinaryOperator("^",
(o1, o2) -> o1.toExponent(o2.getMultiplier()), 1)
.build();
/**
* A parser that can parse unit dimension expressions.
*
* @since 2019-04-13
* @since v0.2.0
*/
private final ExpressionParser> unitDimensionParser = new ExpressionParser.Builder<>(
this::getDimension).addBinaryOperator("*", (o1, o2) -> o1.times(o2), 0)
.addSpaceFunction("*")
.addBinaryOperator("/", (o1, o2) -> o1.dividedBy(o2), 0).build();
/**
* Creates the {@code UnitsDatabase}.
*
* @since 2019-01-10
* @since v0.1.0
*/
public UnitDatabase() {
this.prefixlessUnits = new HashMap<>();
this.prefixes = new HashMap<>();
this.dimensions = new HashMap<>();
this.units = new PrefixedUnitMap(this.prefixlessUnits, this.prefixes);
}
/**
* Adds a unit dimension to the database.
*
* @param name dimension's name
* @param dimension dimension to add
* @throws NullPointerException if name or dimension is null
* @since 2019-03-14
* @since v0.2.0
*/
public void addDimension(final String name,
final ObjectProduct dimension) {
this.dimensions.put(
Objects.requireNonNull(name, "name must not be null."),
Objects.requireNonNull(dimension, "dimension must not be null."));
}
/**
* Adds to the list from a line in a unit dimension file.
*
* @param line line to look at
* @param lineCounter number of line, for error messages
* @since 2019-04-10
* @since v0.2.0
*/
private void addDimensionFromLine(final String line,
final long lineCounter) {
// ignore lines that start with a # sign - they're comments
if (line.isEmpty())
return;
if (line.contains("#")) {
this.addDimensionFromLine(line.substring(0, line.indexOf("#")),
lineCounter);
return;
}
// divide line into name and expression
final Matcher lineMatcher = NAME_EXPRESSION.matcher(line);
if (!lineMatcher.matches())
throw new IllegalArgumentException(String.format(
"Error at line %d: Lines of a dimension file must consist of a dimension name, then spaces or tabs, then a dimension expression.",
lineCounter));
final String name = lineMatcher.group(1);
final String expression = lineMatcher.group(2);
if (name.endsWith(" ")) {
System.err.printf("Warning - line %d's dimension name ends in a space",
lineCounter);
}
// if expression is "!", search for an existing dimension
// if no unit found, throw an error
if (expression.equals("!")) {
if (!this.containsDimensionName(name))
throw new IllegalArgumentException(String.format(
"! used but no dimension found (line %d).", lineCounter));
} else {
// it's a unit, get the unit
final ObjectProduct dimension;
try {
dimension = this.getDimensionFromExpression(expression);
} catch (final IllegalArgumentException e) {
System.err.printf("Parsing error on line %d:%n", lineCounter);
throw e;
}
this.addDimension(name, dimension);
}
}
/**
* Adds a unit prefix to the database.
*
* @param name prefix's name
* @param prefix prefix to add
* @throws NullPointerException if name or prefix is null
* @since 2019-01-14
* @since v0.1.0
*/
public void addPrefix(final String name, final UnitPrefix prefix) {
this.prefixes.put(Objects.requireNonNull(name, "name must not be null."),
Objects.requireNonNull(prefix, "prefix must not be null."));
}
/**
* Adds a unit to the database.
*
* @param name unit's name
* @param unit unit to add
* @throws NullPointerException if unit is null
* @since 2019-01-10
* @since v0.1.0
*/
public void addUnit(final String name, final Unit unit) {
this.prefixlessUnits.put(
Objects.requireNonNull(name, "name must not be null."),
Objects.requireNonNull(unit, "unit must not be null."));
}
/**
* Adds to the list from a line in a unit file.
*
* @param line line to look at
* @param lineCounter number of line, for error messages
* @since 2019-04-10
* @since v0.2.0
*/
private void addUnitOrPrefixFromLine(final String line,
final long lineCounter) {
// ignore lines that start with a # sign - they're comments
if (line.isEmpty())
return;
if (line.contains("#")) {
this.addUnitOrPrefixFromLine(line.substring(0, line.indexOf("#")),
lineCounter);
return;
}
// divide line into name and expression
final Matcher lineMatcher = NAME_EXPRESSION.matcher(line);
if (!lineMatcher.matches())
throw new IllegalArgumentException(String.format(
"Error at line %d: Lines of a unit file must consist of a unit name, then spaces or tabs, then a unit expression.",
lineCounter));
final String name = lineMatcher.group(1);
final String expression = lineMatcher.group(2);
if (name.endsWith(" ")) {
System.err.printf("Warning - line %d's unit name ends in a space",
lineCounter);
}
// if expression is "!", search for an existing unit
// if no unit found, throw an error
if (expression.equals("!")) {
if (!this.containsUnitName(name))
throw new IllegalArgumentException(String
.format("! used but no unit found (line %d).", lineCounter));
} else {
if (name.endsWith("-")) {
final UnitPrefix prefix;
try {
prefix = this.getPrefixFromExpression(expression);
} catch (final IllegalArgumentException e) {
System.err.printf("Parsing error on line %d:%n", lineCounter);
throw e;
}
this.addPrefix(name.substring(0, name.length() - 1), prefix);
} else {
// it's a unit, get the unit
final Unit unit;
try {
unit = this.getUnitFromExpression(expression);
} catch (final IllegalArgumentException e) {
System.err.printf("Parsing error on line %d:%n", lineCounter);
throw e;
}
this.addUnit(name, unit);
}
}
}
/**
* Tests if the database has a unit dimension with this name.
*
* @param name name to test
* @return if database contains name
* @since 2019-03-14
* @since v0.2.0
*/
public boolean containsDimensionName(final String name) {
return this.dimensions.containsKey(name);
}
/**
* Tests if the database has a unit prefix with this name.
*
* @param name name to test
* @return if database contains name
* @since 2019-01-13
* @since v0.1.0
*/
public boolean containsPrefixName(final String name) {
return this.prefixes.containsKey(name);
}
/**
* Tests if the database has a unit with this name, taking prefixes into
* consideration
*
* @param name name to test
* @return if database contains name
* @since 2019-01-13
* @since v0.1.0
*/
public boolean containsUnitName(final String name) {
return this.units.containsKey(name);
}
/**
* @return a map mapping dimension names to dimensions
* @since 2019-04-13
* @since v0.2.0
*/
public Map> dimensionMap() {
return Collections.unmodifiableMap(this.dimensions);
}
/**
* Evaluates a unit expression, following the same rules as
* {@link #getUnitFromExpression}.
*
* @param expression expression to parse
* @return {@code LinearUnitValue} representing value of expression
* @since 2020-08-04
*/
public LinearUnitValue evaluateUnitExpression(final String expression) {
Objects.requireNonNull(expression, "expression must not be null.");
// attempt to get a unit as an alias first
if (this.containsUnitName(expression))
return this.getLinearUnitValue(expression);
// force operators to have spaces
String modifiedExpression = expression;
modifiedExpression = modifiedExpression.replaceAll("\\+", " \\+ ");
modifiedExpression = modifiedExpression.replaceAll("-", " - ");
// format expression
for (final Entry replacement : EXPRESSION_REPLACEMENTS
.entrySet()) {
modifiedExpression = replacement.getKey().matcher(modifiedExpression)
.replaceAll(replacement.getValue());
}
// the previous operation breaks negative numbers, fix them!
// (i.e. -2 becomes - 2)
// FIXME the previous operaton also breaks stuff like "1e-5"
for (int i = 0; i < modifiedExpression.length(); i++) {
if (modifiedExpression.charAt(i) == '-'
&& (i < 2 || Arrays.asList('+', '-', '*', '/', '^')
.contains(modifiedExpression.charAt(i - 2)))) {
// found a broken negative number
modifiedExpression = modifiedExpression.substring(0, i + 1)
+ modifiedExpression.substring(i + 2);
}
}
return this.unitValueExpressionParser.parseExpression(modifiedExpression);
}
/**
* Gets a unit dimension from the database using its name.
*
*
* This method accepts exponents, like "L^3"
*
*
* @param name dimension's name
* @return dimension
* @since 2019-03-14
* @since v0.2.0
*/
public ObjectProduct getDimension(final String name) {
Objects.requireNonNull(name, "name must not be null.");
if (name.contains("^")) {
final String[] baseAndExponent = name.split("\\^");
final ObjectProduct base = this
.getDimension(baseAndExponent[0]);
final int exponent;
try {
exponent = Integer
.parseInt(baseAndExponent[baseAndExponent.length - 1]);
} catch (final NumberFormatException e2) {
throw new IllegalArgumentException("Exponent must be an integer.");
}
return base.toExponent(exponent);
}
return this.dimensions.get(name);
}
/**
* Uses the database's data to parse an expression into a unit dimension
*
* The expression is a series of any of the following:
*
* - The name of a unit dimension, which multiplies or divides the result
* based on preceding operators
* - The operators '*' and '/', which multiply and divide (note that just
* putting two unit dimensions next to each other is equivalent to
* multiplication)
* - The operator '^' which exponentiates. Exponents must be integers.
*
*
* @param expression expression to parse
* @throws IllegalArgumentException if the expression cannot be parsed
* @throws NullPointerException if expression is null
* @since 2019-04-13
* @since v0.2.0
*/
public ObjectProduct getDimensionFromExpression(
final String expression) {
Objects.requireNonNull(expression, "expression must not be null.");
// attempt to get a dimension as an alias first
if (this.containsDimensionName(expression))
return this.getDimension(expression);
// force operators to have spaces
String modifiedExpression = expression;
// format expression
for (final Entry replacement : EXPRESSION_REPLACEMENTS
.entrySet()) {
modifiedExpression = replacement.getKey().matcher(modifiedExpression)
.replaceAll(replacement.getValue());
}
modifiedExpression = modifiedExpression.replaceAll(" *\\^ *", "\\^");
return this.unitDimensionParser.parseExpression(modifiedExpression);
}
/**
* Gets a unit. If it is linear, cast it to a LinearUnit and return it.
* Otherwise, throw an {@code IllegalArgumentException}.
*
* @param name unit's name
* @return unit
* @since 2019-03-22
* @since v0.2.0
*/
private LinearUnit getLinearUnit(final String name) {
// see if I am using a function-unit like tempC(100)
Objects.requireNonNull(name, "name may not be null");
if (name.contains("(") && name.contains(")")) {
// break it into function name and value
final List parts = Arrays.asList(name.split("\\("));
if (parts.size() != 2)
throw new IllegalArgumentException(
"Format nonlinear units like: unit(value).");
// solve the function
final Unit unit = this.getUnit(parts.get(0));
final double value = Double.parseDouble(
parts.get(1).substring(0, parts.get(1).length() - 1));
return LinearUnit.fromUnitValue(unit, value);
} else {
// get a linear unit
final Unit unit = this.getUnit(name);
if (unit instanceof LinearUnit)
return (LinearUnit) unit;
else
throw new IllegalArgumentException(
String.format("%s is not a linear unit.", name));
}
}
/**
* Gets a {@code LinearUnitValue} from a unit name. Nonlinear units will be
* converted to their base units.
*
* @param name name of unit
* @return {@code LinearUnitValue} instance
* @since 2020-08-04
*/
private LinearUnitValue getLinearUnitValue(final String name) {
return LinearUnitValue.getExact(this.getLinearUnit(name), 1);
}
/**
* Gets a unit prefix from the database from its name
*
* @param name prefix's name
* @return prefix
* @since 2019-01-10
* @since v0.1.0
*/
public UnitPrefix getPrefix(final String name) {
try {
return UnitPrefix.valueOf(Double.parseDouble(name));
} catch (final NumberFormatException e) {
return this.prefixes.get(name);
}
}
/**
* Gets a unit prefix from a prefix expression
*
* Currently, prefix expressions are much simpler than unit expressions: They
* are either a number or the name of another prefix
*
*
* @param expression expression to input
* @return prefix
* @throws IllegalArgumentException if expression cannot be parsed
* @throws NullPointerException if any argument is null
* @since 2019-01-14
* @since v0.1.0
*/
public UnitPrefix getPrefixFromExpression(final String expression) {
Objects.requireNonNull(expression, "expression must not be null.");
// attempt to get a unit as an alias first
if (this.containsUnitName(expression))
return this.getPrefix(expression);
// force operators to have spaces
String modifiedExpression = expression;
// format expression
for (final Entry replacement : EXPRESSION_REPLACEMENTS
.entrySet()) {
modifiedExpression = replacement.getKey().matcher(modifiedExpression)
.replaceAll(replacement.getValue());
}
return this.prefixExpressionParser.parseExpression(modifiedExpression);
}
/**
* Gets a unit from the database from its name, looking for prefixes.
*
* @param name unit's name
* @return unit
* @since 2019-01-10
* @since v0.1.0
*/
public Unit getUnit(final String name) {
try {
final double value = Double.parseDouble(name);
return SI.ONE.times(value);
} catch (final NumberFormatException e) {
final Unit unit = this.units.get(name);
if (unit.getPrimaryName().isEmpty())
return unit.withName(NameSymbol.ofName(name));
else if (!unit.getPrimaryName().get().equals(name)) {
final Set otherNames = new HashSet<>(unit.getOtherNames());
otherNames.add(unit.getPrimaryName().get());
return unit.withName(NameSymbol.ofNullable(name,
unit.getSymbol().orElse(null), otherNames));
} else if (!unit.getOtherNames().contains(name)) {
final Set otherNames = new HashSet<>(unit.getOtherNames());
otherNames.add(name);
return unit.withName(
NameSymbol.ofNullable(unit.getPrimaryName().orElse(null),
unit.getSymbol().orElse(null), otherNames));
} else
return unit;
}
}
/**
* Uses the database's unit data to parse an expression into a unit
*
* The expression is a series of any of the following:
*
* - The name of a unit, which multiplies or divides the result based on
* preceding operators
* - The operators '*' and '/', which multiply and divide (note that just
* putting two units or values next to each other is equivalent to
* multiplication)
* - The operator '^' which exponentiates. Exponents must be integers.
* - A number which is multiplied or divided
*
* This method only works with linear units.
*
* @param expression expression to parse
* @throws IllegalArgumentException if the expression cannot be parsed
* @throws NullPointerException if expression is null
* @since 2019-01-07
* @since v0.1.0
*/
public Unit getUnitFromExpression(final String expression) {
Objects.requireNonNull(expression, "expression must not be null.");
// attempt to get a unit as an alias first
if (this.containsUnitName(expression))
return this.getUnit(expression);
// force operators to have spaces
String modifiedExpression = expression;
modifiedExpression = modifiedExpression.replaceAll("\\+", " \\+ ");
modifiedExpression = modifiedExpression.replaceAll("-", " - ");
// format expression
for (final Entry replacement : EXPRESSION_REPLACEMENTS
.entrySet()) {
modifiedExpression = replacement.getKey().matcher(modifiedExpression)
.replaceAll(replacement.getValue());
}
// the previous operation breaks negative numbers, fix them!
// (i.e. -2 becomes - 2)
for (int i = 0; i < modifiedExpression.length(); i++) {
if (modifiedExpression.charAt(i) == '-'
&& (i < 2 || Arrays.asList('+', '-', '*', '/', '^')
.contains(modifiedExpression.charAt(i - 2)))) {
// found a broken negative number
modifiedExpression = modifiedExpression.substring(0, i + 1)
+ modifiedExpression.substring(i + 2);
}
}
return this.unitExpressionParser.parseExpression(modifiedExpression);
}
/**
* Adds all dimensions from a file, using data from the database to parse
* them.
*
* Each line in the file should consist of a name and an expression (parsed
* by getDimensionFromExpression) separated by any number of tab characters.
*
*
* Allowed exceptions:
*
* - Anything after a '#' character is considered a comment and
* ignored.
* - Blank lines are also ignored
* - If an expression consists of a single exclamation point, instead of
* parsing it, this method will search the database for an existing unit. If
* no unit is found, an IllegalArgumentException is thrown. This is used to
* define initial units and ensure that the database contains them.
*
*
* @param file file to read
* @throws IllegalArgumentException if the file cannot be parsed, found or
* read
* @throws NullPointerException if file is null
* @since 2019-01-13
* @since v0.1.0
*/
public void loadDimensionFile(final File file) {
Objects.requireNonNull(file, "file must not be null.");
try (FileReader fileReader = new FileReader(file);
BufferedReader reader = new BufferedReader(fileReader)) {
// while the reader has lines to read, read a line, then parse it, then
// add it
long lineCounter = 0;
while (reader.ready()) {
this.addDimensionFromLine(reader.readLine(), ++lineCounter);
}
} catch (final FileNotFoundException e) {
throw new IllegalArgumentException("Could not find file " + file, e);
} catch (final IOException e) {
throw new IllegalArgumentException("Could not read file " + file, e);
}
}
/**
* Adds all units from a file, using data from the database to parse them.
*
* Each line in the file should consist of a name and an expression (parsed
* by getUnitFromExpression) separated by any number of tab characters.
*
*
* Allowed exceptions:
*
* - Anything after a '#' character is considered a comment and
* ignored.
* - Blank lines are also ignored
* - If an expression consists of a single exclamation point, instead of
* parsing it, this method will search the database for an existing unit. If
* no unit is found, an IllegalArgumentException is thrown. This is used to
* define initial units and ensure that the database contains them.
*
*
* @param file file to read
* @throws IllegalArgumentException if the file cannot be parsed, found or
* read
* @throws NullPointerException if file is null
* @since 2019-01-13
* @since v0.1.0
*/
public void loadUnitsFile(final File file) {
Objects.requireNonNull(file, "file must not be null.");
try (FileReader fileReader = new FileReader(file);
BufferedReader reader = new BufferedReader(fileReader)) {
// while the reader has lines to read, read a line, then parse it, then
// add it
long lineCounter = 0;
while (reader.ready()) {
this.addUnitOrPrefixFromLine(reader.readLine(), ++lineCounter);
}
} catch (final FileNotFoundException e) {
throw new IllegalArgumentException("Could not find file " + file, e);
} catch (final IOException e) {
throw new IllegalArgumentException("Could not read file " + file, e);
}
}
/**
* @return a map mapping prefix names to prefixes
* @since 2019-04-13
* @since v0.2.0
*/
public Map prefixMap() {
return Collections.unmodifiableMap(this.prefixes);
}
/**
* @return a string stating the number of units, prefixes and dimensions in
* the database
*/
@Override
public String toString() {
return String.format(
"Unit Database with %d units, %d unit prefixes and %d dimensions",
this.prefixlessUnits.size(), this.prefixes.size(),
this.dimensions.size());
}
/**
* Returns a map mapping unit names to units, including units with prefixes.
*
* The returned map is infinite in size if there is at least one unit and at
* least one prefix. If it is infinite, some operations that only work with
* finite collections, like converting name/entry sets to arrays, will throw
* an {@code IllegalStateException}.
*
*
* Specifically, the operations that will throw an IllegalStateException if
* the map is infinite in size are:
*
* - {@code unitMap.entrySet().toArray()} (either overloading)
* - {@code unitMap.keySet().toArray()} (either overloading)
*
*
*
* Because of ambiguities between prefixes (i.e. kilokilo = mega), the map's
* {@link PrefixedUnitMap#containsValue containsValue} and
* {@link PrefixedUnitMap#values() values()} methods currently ignore
* prefixes.
*
*
* @return a map mapping unit names to units, including prefixed names
* @since 2019-04-13
* @since v0.2.0
*/
public Map unitMap() {
return this.units; // PrefixedUnitMap is immutable so I don't need to make
// an unmodifiable map.
}
/**
* @return a map mapping unit names to units, ignoring prefixes
* @since 2019-04-13
* @since v0.2.0
*/
public Map unitMapPrefixless() {
return Collections.unmodifiableMap(this.prefixlessUnits);
}
}