/**
* 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;
}
}