/** * Copyright (C) 2018 Adrien Hopkins * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ package sevenUnits.utils; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Set; import java.util.function.Function; /** * An immutable product of multiple objects of a type, such as base units. The * objects can be multiplied and exponentiated. * * @author Adrien Hopkins * @since 2019-10-16 */ public class ObjectProduct implements Nameable { /** * Returns an empty ObjectProduct of a certain type * * @param type of objects that can be multiplied * @return empty product * @since 2019-10-16 */ public static final ObjectProduct empty() { return new ObjectProduct<>(new HashMap<>()); } /** * Gets an {@code ObjectProduct} from an object-to-integer mapping * * @param type of object in product * @param map map mapping objects to exponents * @return object product * @since 2019-10-16 */ public static final ObjectProduct fromExponentMapping( final Map map) { return new ObjectProduct<>(new HashMap<>(map)); } /** * Gets an ObjectProduct that has one of the inputted argument, and nothing * else. * * @param object object that will be in the product * @return product * @since 2019-10-16 * @throws NullPointerException if object is null */ public static final ObjectProduct oneOf(final T object) { Objects.requireNonNull(object, "object must not be null."); final Map map = new HashMap<>(); map.put(object, 1); return new ObjectProduct<>(map); } /** * The objects that make up the product, mapped to their exponents. This map * treats zero as null, and is immutable. * * @since 2019-10-16 */ final Map exponents; /** * The object's name and symbol */ private final NameSymbol nameSymbol; /** * Creates a {@code ObjectProduct} without a name/symbol. * * @param exponents objects that make up this product * @since 2019-10-16 */ ObjectProduct(final Map exponents) { this(exponents, NameSymbol.EMPTY); } /** * Creates the {@code ObjectProduct}. * * @param exponents objects that make up this product * @param nameSymbol name and symbol of object product * @since 2019-10-16 */ ObjectProduct(final Map exponents, NameSymbol nameSymbol) { this.exponents = Collections.unmodifiableMap( ConditionalExistenceCollections.conditionalExistenceMap(exponents, e -> !Integer.valueOf(0).equals(e.getValue()))); this.nameSymbol = nameSymbol; } /** * Calculates the quotient of two products * * @param other other product * @return quotient of two products * @since 2019-10-16 * @throws NullPointerException if other is null */ public ObjectProduct dividedBy(final ObjectProduct other) { Objects.requireNonNull(other, "other must not be null."); // get a list of all objects in both sets final Set objects = new HashSet<>(); objects.addAll(this.getBaseSet()); objects.addAll(other.getBaseSet()); // get a list of all exponents final Map map = new HashMap<>(objects.size()); for (final T key : objects) { map.put(key, this.getExponent(key) - other.getExponent(key)); } // create the product return new ObjectProduct<>(map); } // this method relies on the use of ZeroIsNullMap @Override public boolean equals(final Object obj) { if (this == obj) return true; if (!(obj instanceof ObjectProduct)) return false; final ObjectProduct other = (ObjectProduct) obj; return Objects.equals(this.exponents, other.exponents); } /** * @return immutable map mapping objects to exponents * @since 2019-10-16 */ public Map exponentMap() { return this.exponents; } /** * @return a set of all of the base objects with non-zero exponents that make * up this dimension. * @since 2018-12-12 * @since v0.1.0 */ public final Set getBaseSet() { final Set dimensions = new HashSet<>(); // add all dimensions with a nonzero exponent - zero exponents shouldn't // be there in the first place for (final T dimension : this.exponents.keySet()) { if (!this.exponents.get(dimension).equals(0)) { dimensions.add(dimension); } } return dimensions; } /** * Gets the exponent for a specific dimension. * * @param dimension dimension to check * @return exponent for that dimension * @since 2018-12-12 * @since v0.1.0 */ public int getExponent(final T dimension) { return this.exponents.getOrDefault(dimension, 0); } @Override public NameSymbol getNameSymbol() { return this.nameSymbol; } @Override public int hashCode() { return Objects.hash(this.exponents); } /** * @return true if this product is a single object, i.e. it has one exponent * of one and no other nonzero exponents * @since 2019-10-16 */ public boolean isSingleObject() { int oneCount = 0; boolean twoOrMore = false; // has exponents of 2 or more for (final T b : this.getBaseSet()) { if (this.getExponent(b) == 1) { oneCount++; } else if (this.getExponent(b) != 0) { twoOrMore = true; } } return oneCount == 1 && !twoOrMore; } /** * Multiplies this product by another * * @param other other product * @return product of two products * @since 2019-10-16 * @throws NullPointerException if other is null */ public ObjectProduct times(final ObjectProduct other) { Objects.requireNonNull(other, "other must not be null."); // get a list of all objects in both sets final Set objects = new HashSet<>(); objects.addAll(this.getBaseSet()); objects.addAll(other.getBaseSet()); // get a list of all exponents final Map map = new HashMap<>(objects.size()); for (final T key : objects) { map.put(key, this.getExponent(key) + other.getExponent(key)); } // create the product return new ObjectProduct<>(map); } /** * Returns this product, but to an exponent * * @param exponent exponent * @return result of exponentiation * @since 2019-10-16 */ public ObjectProduct toExponent(final int exponent) { final Map map = new HashMap<>(this.exponents); for (final T key : this.exponents.keySet()) { map.put(key, this.getExponent(key) * exponent); } return new ObjectProduct<>(map); } /** * Converts this product to a string using the objects' * {@link Object#toString()} method (or {@link Nameable#getShortName} if * available). If objects have a long toString representation, it is * recommended to use {@link #toString(Function)} instead to shorten the * returned string. * *

* {@inheritDoc} */ @Override public String toString() { return this .toString(o -> o instanceof Nameable ? ((Nameable) o).getShortName() : o.toString()); } /** * Converts this product to a string. The objects that make up this product * are represented by {@code objectToString} * * @param objectToString function to convert objects to strings * @return string representation of product * @since 2019-10-16 */ public String toString(final Function objectToString) { final List positiveStringComponents = new ArrayList<>(); final List negativeStringComponents = new ArrayList<>(); // for each base object that makes up this object, add it and its exponent for (final T object : this.getBaseSet()) { final int exponent = this.exponents.get(object); if (exponent > 1) { positiveStringComponents.add(String.format("%s^%d", objectToString.apply(object), exponent)); } else if (exponent == 1) { positiveStringComponents.add(objectToString.apply(object)); } else if (exponent < 0) { negativeStringComponents.add(String.format("%s^%d", objectToString.apply(object), -exponent)); } } final String positiveString = positiveStringComponents.isEmpty() ? "1" : String.join(" * ", positiveStringComponents); final String negativeString = negativeStringComponents.isEmpty() ? "" : " / " + String.join(" * ", negativeStringComponents); return positiveString + negativeString; } /** * @return named version of this {@code ObjectProduct}, using data from * {@code nameSymbol} * @since 2021-12-15 */ public ObjectProduct withName(NameSymbol nameSymbol) { return new ObjectProduct<>(this.exponents, nameSymbol); } }