/** * 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.math; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Set; import java.util.function.Function; /** * An immutable product of multiple objects of a type, such as base units. The objects can be multiplied and * exponentiated. * * @author Adrien Hopkins * @since 2019-10-16 */ public final class ObjectProduct { /** * Returns an empty ObjectProduct of a certain type * * @param * type of objects that can be multiplied * @return empty product * @since 2019-10-16 */ public static final ObjectProduct empty() { return new ObjectProduct<>(new HashMap<>()); } /** * Gets an {@code ObjectProduct} from an object-to-integer mapping * * @param * type of object in product * @param map * map mapping objects to exponents * @return object product * @since 2019-10-16 */ public static final ObjectProduct fromExponentMapping(final Map map) { return new ObjectProduct<>(new HashMap<>(map)); } /** * Gets an ObjectProduct that has one of the inputted argument, and nothing else. * * @param object * object that will be in the product * @return product * @since 2019-10-16 * @throws NullPointerException * if object is null */ public static final ObjectProduct oneOf(final T object) { Objects.requireNonNull(object, "object must not be null."); final Map map = new HashMap<>(); map.put(object, 1); return new ObjectProduct<>(map); } /** * The objects that make up the product, mapped to their exponents. This map treats zero as null, and is immutable. * * @since 2019-10-16 */ final Map exponents; /** * Creates the {@code ObjectProduct}. * * @param exponents * objects that make up this product * @since 2019-10-16 */ private ObjectProduct(final Map exponents) { this.exponents = Collections.unmodifiableMap(ZeroIsNullMap.create(new HashMap<>(exponents))); } /** * Calculates the quotient of two products * * @param other * other product * @return quotient of two products * @since 2019-10-16 * @throws NullPointerException * if other is null */ public ObjectProduct dividedBy(final ObjectProduct other) { Objects.requireNonNull(other, "other must not be null."); // get a list of all objects in both sets final Set objects = new HashSet<>(); objects.addAll(this.getBaseSet()); objects.addAll(other.getBaseSet()); // get a list of all exponents final Map map = new HashMap<>(objects.size()); for (final T key : objects) { map.put(key, this.getExponent(key) - other.getExponent(key)); } // create the product return new ObjectProduct<>(map); } // this method relies on the use of ZeroIsNullMap @Override public boolean equals(final Object obj) { if (this == obj) return true; if (!(obj instanceof ObjectProduct)) return false; final ObjectProduct other = (ObjectProduct) obj; return Objects.equals(this.exponents, other.exponents); } /** * @return immutable map mapping objects to exponents * @since 2019-10-16 */ public Map exponentMap() { return this.exponents; } /** * @return a set of all of the base objects with non-zero exponents that make up this dimension. * @since 2018-12-12 * @since v0.1.0 */ public final Set getBaseSet() { final Set dimensions = new HashSet<>(); // add all dimensions with a nonzero exponent - zero exponents shouldn't be there in the first place for (final T dimension : this.exponents.keySet()) { if (!this.exponents.get(dimension).equals(0)) { dimensions.add(dimension); } } return dimensions; } /** * Gets the exponent for a specific dimension. * * @param dimension * dimension to check * @return exponent for that dimension * @since 2018-12-12 * @since v0.1.0 */ public int getExponent(final T dimension) { return this.exponents.getOrDefault(dimension, 0); } @Override public int hashCode() { return Objects.hash(this.exponents); } /** * @return true if this product is a single object, i.e. it has one exponent of one and no other nonzero exponents * @since 2019-10-16 */ public boolean isSingleObject() { int oneCount = 0; boolean twoOrMore = false; // has exponents of 2 or more for (final T b : this.getBaseSet()) { if (this.getExponent(b) == 1) { oneCount++; } else if (this.getExponent(b) != 0) { twoOrMore = true; } } return oneCount == 1 && !twoOrMore; } /** * Multiplies this product by another * * @param other * other product * @return product of two products * @since 2019-10-16 * @throws NullPointerException * if other is null */ public ObjectProduct times(final ObjectProduct other) { Objects.requireNonNull(other, "other must not be null."); // get a list of all objects in both sets final Set objects = new HashSet<>(); objects.addAll(this.getBaseSet()); objects.addAll(other.getBaseSet()); // get a list of all exponents final Map map = new HashMap<>(objects.size()); for (final T key : objects) { map.put(key, this.getExponent(key) + other.getExponent(key)); } // create the product return new ObjectProduct<>(map); } /** * Returns this product, but to an exponent * * @param exponent * exponent * @return result of exponentiation * @since 2019-10-16 */ public ObjectProduct toExponent(final int exponent) { final Map map = new HashMap<>(this.exponents); for (final T key : this.exponents.keySet()) { map.put(key, this.getExponent(key) * exponent); } return new ObjectProduct<>(map); } @Override public String toString() { return this.toString(Object::toString); } /** * Converts this product to a string. The objects that make up this product are represented by * {@code objectToString} * * @param objectToString * function to convert objects to strings * @return string representation of product * @since 2019-10-16 */ public String toString(final Function objectToString) { final List positiveStringComponents = new ArrayList<>(); final List negativeStringComponents = new ArrayList<>(); // for each base object that makes up this object, add it and its exponent for (final T object : this.getBaseSet()) { final int exponent = this.exponents.get(object); if (exponent > 0) { positiveStringComponents.add(String.format("%s^%d", objectToString.apply(object), exponent)); } else if (exponent < 0) { negativeStringComponents.add(String.format("%s^%d", objectToString.apply(object), -exponent)); } } final String positiveString = positiveStringComponents.isEmpty() ? "1" : String.join(" * ", positiveStringComponents); final String negativeString = negativeStringComponents.isEmpty() ? "" : " / " + String.join(" * ", negativeStringComponents); return positiveString + negativeString; } }