> unitSets;
/**
* The rule that specifies when prefix repetition is allowed. It takes in one
* argument: a list of the prefixes being applied to the unit
*
* The prefixes are inputted in application order. This means that
* testing whether "kilomegagigametre" is a valid unit is equivalent to
* running the following code (assuming all variables are defined correctly):
*
* {@code prefixRepetitionRule.test(Arrays.asList(giga, mega, kilo))}
*/
private Predicate> prefixRepetitionRule;
/**
* 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("+", LinearUnit::plus, 0)
.addBinaryOperator("-", LinearUnit::minus, 0)
.addBinaryOperator("*", LinearUnit::times, 1)
.addBinaryOperator("space_times", LinearUnit::times, 2)
.addSpaceFunction("space_times")
.addBinaryOperator("/", LinearUnit::dividedBy, 1)
.addBinaryOperator("|", LinearUnit::dividedBy, 4)
.addBinaryOperator("^", UnitDatabase::exponentiateUnits, 3).build();
/**
* A parser that can parse unit value expressions.
*
* @since 2020-08-04
* @since v0.3.0
*/
private final ExpressionParser unitValueExpressionParser = new ExpressionParser.Builder<>(
this::getLinearUnitValue)
.addBinaryOperator("+", LinearUnitValue::plus, 0)
.addBinaryOperator("-", LinearUnitValue::minus, 0)
.addBinaryOperator("*", LinearUnitValue::times, 1)
.addBinaryOperator("space_times", LinearUnitValue::times, 2)
.addSpaceFunction("space_times")
.addBinaryOperator("/", LinearUnitValue::dividedBy, 1)
.addBinaryOperator("|", LinearUnitValue::dividedBy, 4)
.addBinaryOperator("^", UnitDatabase::exponentiateUnitValues, 3)
.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("+", UnitPrefix::plus, 0)
.addBinaryOperator("-", UnitPrefix::minus, 0)
.addBinaryOperator("*", UnitPrefix::times, 1)
.addSpaceFunction("*")
.addBinaryOperator("/", UnitPrefix::dividedBy, 1)
.addBinaryOperator("|", UnitPrefix::dividedBy, 3)
.addBinaryOperator("^", (o1, o2) -> o1.toExponent(o2.getMultiplier()),
2)
.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("*", ObjectProduct::times, 0)
.addSpaceFunction("*")
.addBinaryOperator("/", ObjectProduct::dividedBy, 0)
.addBinaryOperator("|", ObjectProduct::dividedBy, 2)
.addNumericOperator("^", (o1, o2) -> {
final var exponent = (int) Math.round(o2.value());
return o1.toExponent(exponent);
}, 1).build();
/**
* Creates the {@code UnitsDatabase}.
*
* @since 2019-01-10
* @since v0.1.0
*/
public UnitDatabase() {
this(prefixes -> true);
}
/**
* Creates the {@code UnitsDatabase}
*
* @param prefixRepetitionRule the rule that determines when prefix
* repetition is allowed
* @since 2020-08-26
* @since v0.3.0
*/
public UnitDatabase(Predicate> prefixRepetitionRule) {
this.prefixlessUnits = new HashMap<>();
this.prefixes = new HashMap<>();
this.dimensions = new HashMap<>();
this.prefixRepetitionRule = prefixRepetitionRule;
this.units = ConditionalExistenceCollections.conditionalExistenceMap(
new PrefixedUnitMap(this.prefixlessUnits, this.prefixes),
entry -> this.prefixRepetitionRule
.test(this.getPrefixesFromName(entry.getKey())));
this.unitSets = new HashMap<>();
}
/**
* 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) {
Objects.requireNonNull(name, "name may not be null");
Objects.requireNonNull(dimension, "dimension may not be null");
final var namedDimension = dimension
.withName(dimension.getNameSymbol().withExtraName(name));
this.dimensions.put(name, namedDimension);
}
/**
* 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 var 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 var name = lineMatcher.group(1);
final var 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 {
this.addDimension(name, this.getDimensionFromExpression(expression));
}
}
/**
* 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) {
Objects.requireNonNull(prefix, "prefix may not be null");
final var namedPrefix = prefix
.withName(prefix.getNameSymbol().withExtraName(name));
this.prefixes.put(Objects.requireNonNull(name, "name must not be null."),
namedPrefix);
}
/**
* 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) {
Objects.requireNonNull(unit, "unit may not be null");
final var namedUnit = unit
.withName(unit.getNameSymbol().withExtraName(name));
this.prefixlessUnits.put(
Objects.requireNonNull(name, "name must not be null."), namedUnit);
}
/**
* 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 var 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 var name = lineMatcher.group(1);
final var expression = lineMatcher.group(2);
// this code should never occur
// 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 var prefixName = name.substring(0, name.length() - 1);
this.addPrefix(prefixName, this.getPrefixFromExpression(expression));
} else if (expression.contains(";")) {
// it's a multi-unit
this.addUnitSet(name, this.getUnitSetFromExpression(expression));
} else {
// it's a unit, get the unit
this.addUnit(name, this.getUnitFromExpression(expression));
}
}
/**
* Add a unit set to the database.
*
* @param name name of unit set
* @param value unit set to add
* @since 2024-08-16
* @since v1.0.0
*/
public void addUnitSet(String name, List value) {
if (value.isEmpty())
throw new IllegalArgumentException("Unit sets must not be empty.");
for (final LinearUnit unit : value.subList(1, value.size())) {
if (!Objects.equals(unit.getDimension(), value.get(0).getDimension()))
throw new IllegalArgumentException(
"Unit sets must be all the same dimension, " + value
+ " is not.");
}
this.unitSets.put(name, value);
}
/**
* Removes all units, unit sets, prefixes and dimensions from this database.
*
* @since 2022-02-26
* @since v0.4.0
*/
public void clear() {
this.dimensions.clear();
this.prefixes.clear();
this.prefixlessUnits.clear();
this.unitSets.clear();
}
/**
* 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);
}
/**
* Returns true iff there is a unit set with this name.
*
* @param name name to check for
* @return true iff there is a unit set with this name
*
* @since 2024-08-16
* @since v1.0.0
*/
public boolean containsUnitSetName(String name) {
return this.unitSets.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
* @since v0.3.0
*/
public LinearUnitValue evaluateUnitExpression(final String expression) {
Objects.requireNonNull(expression, "expression must not be null.");
// attempt to get a unit as an alias, or a number with precision first
if (this.containsUnitName(expression))
return this.getLinearUnitValue(expression);
return this.unitValueExpressionParser
.parseExpression(formatExpression(expression));
}
/**
* Gets a unit dimension from the database using its name.
*
* @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.");
final var dimension = this.dimensions.get(name);
if (dimension == null)
throw new NoSuchElementException(
"No dimension with name \"" + name + "\".");
return dimension;
}
/**
* 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
* @return parsed unit dimension
* @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);
return this.unitDimensionParser
.parseExpression(formatExpression(expression));
}
/**
* 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
*/
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 var unit = this.getUnit(parts.get(0));
final var value = Double.parseDouble(
parts.get(1).substring(0, parts.get(1).length() - 1));
return LinearUnit.fromUnitValue(unit, value);
}
// get a linear unit
final var 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
* @since v0.3.0
*/
LinearUnitValue getLinearUnitValue(final String name) {
try {
// try to parse it as a number - otherwise it is not a number!
return LinearUnitValue.of(Metric.ONE,
UncertainDouble.fromRoundedString(name));
} catch (final NumberFormatException e) {
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) {
final var prefix = this.prefixes.get(name);
if (prefix == null)
throw new NoSuchElementException(
"No prefix with name \"" + name + "\".");
return prefix;
}
}
/**
* Gets all of the prefixes that are on a unit name, in application order.
*
* @param unitName name of unit
* @return prefixes
* @since 2020-08-26
* @since v0.3.0
*/
List getPrefixesFromName(final String unitName) {
final List prefixes = new ArrayList<>();
var name = unitName;
while (!this.prefixlessUnits.containsKey(name)) {
// find the longest prefix
String longestPrefixName = null;
var longestLength = name.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);
}
}
// longest prefix found!
final var prefix = this.getPrefix(longestPrefixName);
prefixes.add(0, prefix);
name = name.substring(longestLength);
}
return prefixes;
}
/**
* 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);
return this.prefixExpressionParser
.parseExpression(formatExpression(expression));
}
/**
* @return the prefixRepetitionRule
* @since 2020-08-26
* @since v0.3.0
*/
public Predicate> getPrefixRepetitionRule() {
return this.prefixRepetitionRule;
}
/**
* 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 var value = Double.parseDouble(name);
return Metric.ONE.times(value);
} catch (final NumberFormatException e) {
final var unit = this.units.get(name);
if (unit == null)
throw new NoSuchElementException("No unit " + name);
if (unit.getPrimaryName().isEmpty())
return unit.withName(NameSymbol.ofName(name));
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));
}
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));
}
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
* @return parsed unit
* @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);
return this.unitExpressionParser
.parseExpression(formatExpression(expression));
}
/**
* Get a unit set from its name, throwing a {@link NoSuchElementException} if
* there is none.
*
* @param name name of unit set
* @return unit set with that name
*
* @since 2024-08-16
* @since v1.0.0
*/
public List getUnitSet(String name) {
final var unitSet = this.unitSets.get(name);
if (unitSet == null)
throw new NoSuchElementException("No unit set with name " + name);
return unitSet;
}
/**
* Parses a semicolon-separated expression to get the unit set being used.
*
* @since 2024-08-22
* @since v1.0.0
*/
List getUnitSetFromExpression(String expression) {
final var parts = expression.split(";");
final List units = new ArrayList<>(parts.length);
for (final String unitName : parts) {
final var unit = this.getUnitFromExpression(unitName.trim());
if (!(unit instanceof LinearUnit))
throw new IllegalArgumentException(String.format(
"Unit '%s' is in a unit-set expression, but is not linear.",
unitName));
if (units.size() > 0 && !unit.canConvertTo(units.get(0)))
throw new IllegalArgumentException(String.format(
"Units in expression '%s' have different dimensions.",
expression));
units.add((LinearUnit) unit);
}
return units;
}
/**
* 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 NullPointerException if file is null
* @return list of errors that happened when loading file
* @since 2019-01-13
* @since v0.1.0
*/
public List loadDimensionFile(final Path file) {
Objects.requireNonNull(file, "file must not be null.");
final List errors = new ArrayList<>();
try {
var lineCounter = 0L;
for (final String line : Files.readAllLines(file)) {
try {
this.addDimensionFromLine(line, ++lineCounter);
} catch (IllegalArgumentException | NoSuchElementException e) {
errors.add(new LoadingException(lineCounter, line, file,
LoadingException.FileType.DIMENSION, e));
}
}
} 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 errors;
}
/**
* Adds all dimensions from a {@code InputStream}. Otherwise, works like
* {@link #loadDimensionFile}.
*
* @param stream stream to load from
* @return list of all errors that happened loading the stream
* @since 2021-03-27
* @since v0.3.0
*/
public List loadDimensionsFromStream(
final InputStream stream) {
final List errors = new ArrayList<>();
try (final var scanner = new Scanner(stream)) {
var lineCounter = 0L;
while (scanner.hasNextLine()) {
final var line = scanner.nextLine();
try {
this.addDimensionFromLine(line, ++lineCounter);
} catch (IllegalArgumentException | NoSuchElementException e) {
errors.add(new LoadingException(lineCounter, line,
LoadingException.FileType.DIMENSION, e));
}
}
}
return errors;
}
/**
* 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 NullPointerException if file is null
* @return list of errors that happened when loading file
* @since 2019-01-13
* @since v0.1.0
*/
public List loadUnitsFile(final Path file) {
Objects.requireNonNull(file, "file must not be null.");
final List errors = new ArrayList<>();
try {
var lineCounter = 0L;
for (final String line : Files.readAllLines(file)) {
try {
this.addUnitOrPrefixFromLine(line, ++lineCounter);
} catch (IllegalArgumentException | NoSuchElementException e) {
errors.add(new LoadingException(lineCounter, line, file,
LoadingException.FileType.UNIT, e));
}
}
} 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 errors;
}
/**
* Adds all units from a {@code InputStream}. Otherwise, works like
* {@link #loadUnitsFile}.
*
* @param stream stream to load from
* @return list of all errors that happened loading the stream
* @since 2021-03-27
* @since v0.3.0
*/
public List loadUnitsFromStream(InputStream stream) {
final List errors = new ArrayList<>();
try (final var scanner = new Scanner(stream)) {
var lineCounter = 0L;
while (scanner.hasNextLine()) {
final var line = scanner.nextLine();
try {
this.addUnitOrPrefixFromLine(line, ++lineCounter);
} catch (IllegalArgumentException | NoSuchElementException e) {
errors.add(new LoadingException(lineCounter, line,
LoadingException.FileType.UNIT, e));
}
}
}
return errors;
}
/**
* @param includeDuplicates if false, duplicates are removed from the map
* @return a map mapping prefix names to prefixes
* @since 2022-04-18
* @since v0.4.0
*/
public Map prefixMap(boolean includeDuplicates) {
if (includeDuplicates)
return Collections.unmodifiableMap(this.prefixes);
return Collections.unmodifiableMap(ConditionalExistenceCollections
.conditionalExistenceMap(this.prefixes,
entry -> !isRemovableDuplicate(this.prefixes, entry)));
}
/**
* @param prefixRepetitionRule the prefixRepetitionRule to set
* @since 2020-08-26
* @since v0.3.0
*/
public void setPrefixRepetitionRule(
Predicate> prefixRepetitionRule) {
this.prefixRepetitionRule = prefixRepetitionRule;
}
/**
* @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.
}
/**
* @param includeDuplicates if true, duplicate units will all exist in the
* map; if false, only one of each unit will exist,
* even if the names are different
* @return a map mapping unit names to units, ignoring prefixes
* @since 2019-04-13
* @since v0.2.0
*/
public Map unitMapPrefixless(boolean includeDuplicates) {
if (includeDuplicates)
return Collections.unmodifiableMap(this.prefixlessUnits);
return Collections.unmodifiableMap(ConditionalExistenceCollections
.conditionalExistenceMap(this.prefixlessUnits,
entry -> !isRemovableDuplicate(this.prefixlessUnits, entry)));
}
/**
* @return an unmodifiable map mapping names to unit sets
* @since 2024-08-16
* @since v1.0.0
*/
public Map> unitSetMap() {
return Collections.unmodifiableMap(this.unitSets);
}
}