All Downloads are FREE. Search and download functionalities are using the official Maven repository.

sirius.kernel.commons.Amount Maven / Gradle / Ivy

/*
 * Made with all the love in the world
 * by scireum in Remshalden, Germany
 *
 * Copyright by scireum GmbH
 * http://www.scireum.de - [email protected]
 */

package sirius.kernel.commons;

import sirius.kernel.nls.NLS;

import javax.annotation.CheckReturnValue;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import javax.annotation.concurrent.Immutable;
import java.math.BigDecimal;
import java.math.MathContext;
import java.math.RoundingMode;
import java.text.DecimalFormat;
import java.util.function.Supplier;

/**
 * Provides a wrapper around BigDecimal to perform "exact" computations on numeric values.
 * 

* Adds some extended computations as well as locale aware formatting options to perform "exact" computations on * numeric value. The internal representation is BigDecimal and uses MathContext.DECIMAL128 for * numerical operations. Also the scale of each value is fixed to 5 decimal places after the comma, since this is * enough for most business applications and rounds away any rounding errors introduced by doubles. *

* A textual representation can be created by calling one of the toString methods or by supplying * a {@link NumberFormat}. *

* Being able to be empty, this class handles null values gracefully, which simplifies many operations. * * @see NumberFormat * @see BigDecimal */ @Immutable public class Amount implements Comparable { /** * Represents an missing number. This is also the result of division by 0 and other forbidden operations. */ public static final Amount NOTHING = new Amount(null); /** * Representation of 100.00 */ public static final Amount ONE_HUNDRED = new Amount(new BigDecimal(100)); /** * Representation of 0.00 */ public static final Amount ZERO = new Amount(BigDecimal.ZERO); /** * Representation of 1.00 */ public static final Amount ONE = new Amount(BigDecimal.ONE); /** * Representation of 10.00 */ public static final Amount TEN = new Amount(BigDecimal.TEN); /** * Representation of -1.00 */ public static final Amount MINUS_ONE = new Amount(new BigDecimal(-1)); /** * Defines the internal precision used for all computations. */ public static final int SCALE = 5; private static final String[] METRICS = {"f", "n", "u", "m", "", "K", "M", "G"}; private static final int NEUTRAL_METRIC = 4; private final BigDecimal value; private Amount(BigDecimal value) { if (value != null) { this.value = value.setScale(SCALE, RoundingMode.HALF_UP); } else { this.value = null; } } /** * Converts the given string into a number. If the string is empty, NOTHING is returned. * If the string is malformed an exception will be thrown. * * @param value the string value which should be converted into a numeric value. * @return an Amount representing the given input. NOTHING if the input was empty. */ @Nonnull public static Amount ofMachineString(@Nullable String value) { if (Strings.isEmpty(value)) { return NOTHING; } return of(NLS.parseMachineString(BigDecimal.class, value)); } /** * Converts the given string into a number which is formatted according the decimal symbols for the current locale. * * @param value the string value which should be converted into a numeric value. * @return an {@code Amount} representing the given input. {@code NOTHING} if the input was empty. * @see NLS */ @Nonnull public static Amount ofUserString(@Nullable String value) { if (Strings.isEmpty(value)) { return NOTHING; } return NLS.parseUserString(Amount.class, value); } /** * Converts the given value into a number. * * @param amount the value which should be converted into an Amount * @return an Amount representing the given input. NOTHING if the input was empty. */ @Nonnull public static Amount of(@Nullable BigDecimal amount) { if (amount == null) { return NOTHING; } return new Amount(amount); } /** * Converts the given value into a number. * * @param amount the value which should be converted into an Amount * @return an Amount representing the given input */ @Nonnull public static Amount of(int amount) { return of(new BigDecimal(amount)); } /** * Converts the given value into a number. * * @param amount the value which should be converted into an Amount * @return an Amount representing the given input */ @Nonnull public static Amount of(long amount) { return of(new BigDecimal(amount)); } /** * Converts the given value into a number. * * @param amount the value which should be converted into an Amount * @return an Amount representing the given input */ @Nonnull public static Amount of(double amount) { if (Double.isInfinite(amount) || Double.isNaN(amount)) { return NOTHING; } return of(BigDecimal.valueOf(amount)); } /** * Converts the given value into a number. * * @param amount the value which should be converted into an Amount * @return an Amount representing the given input. NOTHING if the input was empty. */ @Nonnull public static Amount of(@Nullable Integer amount) { if (amount == null) { return NOTHING; } return of(new BigDecimal(amount)); } /** * Converts the given value into a number. * * @param amount the value which should be converted into an Amount * @return an Amount representing the given input. NOTHING if the input was empty. */ @Nonnull public static Amount of(@Nullable Long amount) { if (amount == null) { return NOTHING; } return of(new BigDecimal(amount)); } /** * Converts the given value into a number. * * @param amount the value which should be converted into an Amount * @return an Amount representing the given input. NOTHING if the input was empty. */ @Nonnull public static Amount of(@Nullable Double amount) { if (amount == null || Double.isInfinite(amount) || Double.isNaN(amount)) { return NOTHING; } return of(BigDecimal.valueOf(amount)); } /** * Unwraps the internally used BigDecimal. May be null if this Amount is * NOTHING. * * @return the internally used BigDecimal */ @Nullable public BigDecimal getAmount() { return value; } /** * Checks if this contains no value. * * @return true if the internal value is null, false otherwise */ public boolean isEmpty() { return value == null; } /** * Checks if this actual number contains a value or not * * @return true if the internal value is a number, false otherwise */ public boolean isFilled() { return value != null; } /** * If this actual number if empty, the given value will be returned. Otherwise this will be returned. * * @param valueIfNothing the value which is used if there is no internal value * @return this if there is an internal value, valueIfNothing otherwise */ @Nonnull public Amount fill(@Nonnull Amount valueIfNothing) { if (isEmpty()) { return valueIfNothing; } else { return this; } } /** * If this actual number if empty, the given supplier is used to compute one. Otherwise this will be returned. * * @param supplier the supplier which is used to compute a value if there is no internal value * @return this if there is an internal value, the computed value of supplier otherwise */ @Nonnull public Amount computeIfNull(Supplier supplier) { if (isEmpty()) { return supplier.get(); } return this; } /** * Increases this number by the given amount in percent. If increase is 18 and the value of this is 100, * the result would by 118. * * @param increase the percent value by which the value of this will be increased * @return NOTHING if this is empty, {@code this * (1 + increase / 100)} otherwise */ @Nonnull @CheckReturnValue public Amount increasePercent(@Nonnull Amount increase) { return times(ONE.add(increase.asDecimal())); } /** * Decreases this number by the given amount in percent. If decrease is 10 and the value of this is 100, * the result would by 90. * * @param decrease the percent value by which the value of this will be decreased * @return NOTHING if this is empty, {@code this * (1 - increase / 100)} otherwise */ @Nonnull @CheckReturnValue public Amount decreasePercent(@Nonnull Amount decrease) { return times(ONE.subtract(decrease.asDecimal())); } /** * Used to multiply two percentages, like two discounts as if they where applied after each other. *

* This can be used to compute the effective discount if two discounts like 15% and 5% are applied after * each other. The result would be {@code (15 + 5) - (15 * 5 / 100)} which is 19,25 % * * @param percent the second percent value which would be applied after this percent value. * @return the effective percent value after both percentages would have been applied or NOTHING if * this is empty. */ @Nonnull @CheckReturnValue public Amount multiplyPercent(@Nonnull Amount percent) { return add(percent).subtract(this.times(percent).divideBy(ONE_HUNDRED)); } /** * Adds the given number to this, if other is not empty. Otherwise this will be * returned. * * @param other the operand to add to this. * @return an Amount representing the sum of this and other if both values were filled. * If other is empty, this is returned. If this is empty, NOTHING is returned. */ @Nonnull @CheckReturnValue public Amount add(@Nullable Amount other) { if (other == null || other.isEmpty()) { return this; } if (isEmpty()) { return NOTHING; } return Amount.of(value.add(other.value)); } /** * Subtracts the given number from this, if other is not empty. Otherwise this will be * returned. * * @param other the operand to subtract from this. * @return an Amount representing the difference of this and other if both values were * filled. * If other is empty, this is returned. If this is empty, NOTHING is returned. */ @Nonnull @CheckReturnValue public Amount subtract(@Nullable Amount other) { if (other == null || other.isEmpty()) { return this; } if (isEmpty()) { return NOTHING; } return Amount.of(value.subtract(other.value)); } /** * Multiplies the given number with this. If either of both is empty, NOTHING will be returned. * * @param other the operand to multiply with this. * @return an Amount representing the product of this and other if both values were * filled. * If other is empty or if this is empty, NOTHING is returned. */ @Nonnull @CheckReturnValue public Amount times(@Nonnull Amount other) { if (other.isEmpty() || isEmpty()) { return NOTHING; } return Amount.of(value.multiply(other.value)); } /** * Divides this by the given number. If either of both is empty, or the given number is zero, * NOTHING will be returned. The division uses {@link MathContext#DECIMAL128} * * @param other the operand to divide this by. * @return an Amount representing the division of this by other or NOTHING * if either of both is empty. */ @Nonnull @CheckReturnValue public Amount divideBy(@Nullable Amount other) { if (other == null || other.isZeroOrNull() || isEmpty()) { return NOTHING; } return Amount.of(value.divide(other.value, MathContext.DECIMAL128)); } /** * Returns the ratio in percent from this to other. * This is equivalent to {@code this / other * 100} * * @param other the base to compute the percentage from. * @return an Amount representing the ratio between this and other * or NOTHING if either of both is empty. */ @Nonnull @CheckReturnValue public Amount percentageOf(@Nullable Amount other) { return divideBy(other).toPercent(); } /** * Returns the increase in percent of this over other. * This is equivalent to {@code ((this / other) - 1) * 100} * * @param other the base to compute the increase from. * @return an Amount representing the percentage increase between this and other * or NOTHING if either of both is empty. */ @Nonnull @CheckReturnValue public Amount percentageDifferenceOf(@Nonnull Amount other) { return divideBy(other).subtract(ONE).toPercent(); } /** * Determines if the value is filled and equal to 0.00. * * @return true if this value is filled and equal to 0.00, false otherwise. */ public boolean isZero() { return value != null && value.compareTo(BigDecimal.ZERO) == 0; } /** * Determines if the value is filled and not equal to 0.00. * * @return true if this value is filled and not equal to 0.00, false otherwise. */ public boolean isNonZero() { return value != null && value.compareTo(BigDecimal.ZERO) != 0; } /** * Determines if the value is filled and greater than 0.00 * * @return true if this value is filled and greater than 0.00, false otherwise. */ public boolean isPositive() { return value != null && value.compareTo(BigDecimal.ZERO) > 0; } /** * Determines if the value is filled and less than 0.00 * * @return true if this value is filled and less than 0.00, false otherwise. */ public boolean isNegative() { return value != null && value.compareTo(BigDecimal.ZERO) < 0; } /** * Determines if the value is empty or equal to 0.00 * * @return true if this value is empty, or equal to 0.00, false otherwise. */ public boolean isZeroOrNull() { return value == null || value.compareTo(BigDecimal.ZERO) == 0; } /** * Converts a given decimal fraction into a percent value i.e. 0.34 to 34 %. * Effectively this is {@code this * 100} * * @return a percentage representation of the given decimal value. */ @Nonnull @CheckReturnValue public Amount toPercent() { return this.times(ONE_HUNDRED); } /** * Converts a percent value into a decimal fraction i.e. 34 % to 0.34 * Effectively this is {@code this / 100} * * @return a decimal representation fo the given percent value. */ @Nonnull @CheckReturnValue public Amount asDecimal() { return divideBy(ONE_HUNDRED); } /** * Compares this amount against the given one. *

* If both amounts are empty, they are considered equal, otherwise, if the given amount is empty, we consider this * amount to be larger. Likewise, if this amount is empty, we consider the given one to be larger. * * @param o the amount to compare to * @return 0 if both amounts are equal, -1 of this amount is less than the given one or 1 if this amount is greater * than the given one */ @Override @SuppressWarnings("squid:S1698") @Explain("Indentity against this is safe and a shortcut to speed up comparisons") public int compareTo(Amount o) { if (o == null) { return 1; } if (o == this) { return 0; } if (o.value == null) { return 1; } if (value == null) { return -1; } return value.compareTo(o.value); } /** * Determines if this amount is greater than the given one. *

* See {@link #compareTo(Amount)} for an explanation of how empty amounts are handled. * * @param o the other amount to compare against * @return true if this amount is greater than the given one */ public boolean isGreaterThan(Amount o) { return compareTo(o) > 0; } /** * Determines if this amount is greater than or equal to the given one. *

* See {@link #compareTo(Amount)} for an explanation of how empty amounts are handled. * * @param o the other amount to compare against * @return true if this amount is greater than or equals to the given one */ public boolean isGreaterThanOrEqual(Amount o) { return compareTo(o) >= 0; } /** * Determines if this amount is less than the given one. *

* See {@link #compareTo(Amount)} for an explanation of how empty amounts are handled. * * @param o the other amount to compare against * @return true if this amount is less than the given one */ public boolean isLessThan(Amount o) { return compareTo(o) < 0; } /** * Determines if this amount is less than or equal to the given one. *

* See {@link #compareTo(Amount)} for an explanation of how empty amounts are handled. * * @param o the other amount to compare against * @return true if this amount is less than or equals to the given one */ public boolean isLessThanOrEqual(Amount o) { return compareTo(o) <= 0; } @Override public boolean equals(Object o) { if (this == o) { return true; } if (o == null || getClass() != o.getClass()) { return false; } Amount otherAmount = (Amount) o; if (this.value == null || otherAmount.value == null) { return (this.value == null) == (otherAmount.value == null); } return this.value.compareTo(otherAmount.value) == 0; } @Override public int hashCode() { return value != null ? value.hashCode() : 0; } /** * Compares this amount against the given amount and returns the one with the lower value. *

* If either of the amounts is empty, the filled one is returned. If both are empty, an empty amount is returned. * * @param other the amount to compare against * @return the amount with the lower value or an empty amount, if both amounts are empty */ @Nonnull @SuppressWarnings("squid:S1698") @Explain("Indentity against this is safe and a shortcut to speed up comparisons") public Amount min(@Nullable Amount other) { if (other == this || other == null) { return this; } if (isEmpty()) { return other; } if (other.isEmpty()) { return this; } return this.value.compareTo(other.value) < 0 ? this : other; } /** * Compares this amount against the given amount and returns the one with the higher value. *

* If either of the amounts is empty, the filled one is returned. If both are empty, an empty amount is returned. * * @param other the amount to compare against * @return the amount with the higher value or an empty amount, if both amounts are empty */ @Nonnull @SuppressWarnings("squid:S1698") @Explain("Indentity against this is safe and a shortcut to speed up comparisons") public Amount max(@Nullable Amount other) { if (other == this || other == null) { return this; } if (isEmpty()) { return other; } if (other.isEmpty()) { return this; } return this.value.compareTo(other.value) > 0 ? this : other; } /** * Negates this amount and returns the new amount. * * @return an Amount representing the negated Amount. If this is empty, NOTHING * is returned. */ public Amount negate() { return times(MINUS_ONE); } /** * Returns a {@link Amount} whose value is {@code (this % other)}. * * @param other the divisor value by which this {@code Amount} is to be divided. * @return an {@link Amount} representing the remainder of the two amounts * @see BigDecimal#remainder(BigDecimal) */ public Amount remainder(Amount other) { if (isEmpty() || other.isZeroOrNull()) { return Amount.NOTHING; } return Amount.of(value.remainder(other.value)); } @Override public String toString() { return toSmartRoundedString(NumberFormat.TWO_DECIMAL_PLACES).toString(); } /** * Formats the represented value as percentage. Up to two digits will be shown if non zero. * Therefore 12.34 will be rendered as 12.34 % but 5.00 will be * rendered as 5 %. If the value is empty, "" will be returned. *

* A custom {@link NumberFormat} can be used by directly calling {@link #toSmartRoundedString(NumberFormat)} * or to disable smart rounding (to also show .00) {@link #toString(NumberFormat)} can be called using * {@link NumberFormat#PERCENT}. * * @return a string representation of this number using {@code NumberFormat#PERCENT} * or "" if the value is empty. */ public String toPercentString() { return toSmartRoundedString(NumberFormat.PERCENT).toString(); } /** * Tries to convert the wrapped value to a roman numeral representation. * Any fractional part of this {@code BigDecimal} will be discarded, * and if the resulting "{@code BigInteger}" is too big to fit in an {@code int}, only the low-order 32 bits are * returned. * * @return a string representation of this number as roman numeral or "" for values <= 0 and >= 4000. */ public String toRomanNumeralString() { return RomanNumeral.toRoman(value.intValue()); } /** * Formats the represented value by rounding to zero decimal places. The rounding mode is obtained from * {@link NumberFormat#NO_DECIMAL_PLACES}. * * @return a rounded representation of this number using {@code NumberFormat#NO_DECIMAL_PLACES} * or "" is the value is empty. */ public String toRoundedString() { return toSmartRoundedString(NumberFormat.NO_DECIMAL_PLACES).toString(); } /** * Rounds the number according to the given format. In contrast to only round values when displaying them as * string, this method returns a modified Amount which as potentially lost some precision. Depending on * the next computation this might return significantly different values in contrast to first performing all * computations and round at the end when rendering the values as string. *

* The number of decimal places and the rounding mode is obtained from format ({@link NumberFormat}). * * @param format the format used to determine the precision of the rounding operation * @return returns an Amount which is rounded using the given {@code NumberFormat} * or NOTHING if the value is empty. */ @Nonnull @CheckReturnValue public Amount round(@Nonnull NumberFormat format) { return round(format.getScale(), format.getRoundingMode()); } /** * Rounds the number according to the given format. In contrast to only round values when displaying them as * string, this method returns a modified Amount which as potentially lost some precision. Depending on * the next computation this might return significantly different values in contrast to first performing all * computations and round at the end when rendering the values as string. * * @param scale the precision * @param roundingMode the rounding operation * @return returns an Amount which is rounded using the given {@code RoundingMode} * or NOTHING if the value is empty. */ @Nonnull @CheckReturnValue public Amount round(int scale, @Nonnull RoundingMode roundingMode) { if (isEmpty()) { return NOTHING; } return Amount.of(value.setScale(scale, roundingMode)); } private Value convertToString(NumberFormat format, boolean smartRound) { if (isEmpty()) { return Value.of(null); } DecimalFormat df = new DecimalFormat(); df.setMinimumFractionDigits(smartRound ? 0 : format.getScale()); df.setMaximumFractionDigits(format.getScale()); df.setDecimalFormatSymbols(format.getDecimalFormatSymbols()); df.setGroupingUsed(format.isUseGrouping()); return Value.of(df.format(value)).append(" ", format.getSuffix()); } /** * Converts the number into a string according to the given format. The returned {@link Value} provides * helpful methods to pre- or append texts like units or currency symbols while gracefully handling empty values. * * @param format the {@code NumberFormat} used to obtain the number of decimal places, * the decimal format symbols and rounding mode * @return a Value containing the string representation according to the given format * or an empty Value if this is empty. * @see Value#append(String, Object) * @see Value#prepend(String, Object) */ @Nonnull public Value toString(@Nonnull NumberFormat format) { return convertToString(format, false); } /** * Converts the number into a string just like {@link #toString(NumberFormat)}. However, if the number has no * decimal places, a rounded value (without .00) will be returned. * * @param format the {@code NumberFormat} used to obtain the number of decimal places, * the decimal format symbols and rounding mode * @return a Value containing the string representation according to the given format * or an empty Value if this is empty. Omits 0 as decimal places. * @see #toString() */ @Nonnull public Value toSmartRoundedString(@Nonnull NumberFormat format) { return convertToString(format, true); } /** * Creates a "scientific" representation of the amount. *

* This representation will shift the value in the range 0..999 and represent the decimal shift by a well * known unit prefix. The following prefixes will be used: *

    *
  • f - femto
  • *
  • n - nano
  • *
  • u - micro
  • *
  • m - milli
  • *
  • K - kilo
  • *
  • M - mega
  • *
  • G - giga
  • *
*

* An input of 0.0341 V will be represented as 34.1 mV if digits was 4 or 34 mV is digits was 2 * or less. * * @param digits the number of decimal digits to display * @param unit the unit to be appended to the generated string * @return a scientific string representation of the amount. */ @Nonnull public String toScientificString(int digits, String unit) { if (isEmpty()) { return ""; } int metric = NEUTRAL_METRIC; double doubleValue = this.value.doubleValue(); while (Math.abs(doubleValue) >= 990d && metric < METRICS.length - 1) { doubleValue /= 1000d; metric += 1; } int effectiveDigits = digits; if (metric == NEUTRAL_METRIC) { while (!Doubles.isZero(doubleValue) && Math.abs(doubleValue) < 1.01 && metric > 0) { doubleValue *= 1000d; metric -= 1; // We loose accuracy, therefore we limit our decimal digits... effectiveDigits -= 3; } } DecimalFormat df = new DecimalFormat(); df.setMinimumFractionDigits(Math.max(0, effectiveDigits)); df.setMaximumFractionDigits(Math.max(0, effectiveDigits)); df.setDecimalFormatSymbols(NLS.getDecimalFormatSymbols()); df.setGroupingUsed(true); StringBuilder sb = new StringBuilder(df.format(doubleValue)); if (metric != NEUTRAL_METRIC) { sb.append(" "); sb.append(METRICS[metric]); } if (unit != null) { sb.append(unit); } return sb.toString(); } /** * Returns the number of decimal digits (ignoring decimal places after the decimal separator). * * @return the number of digits required to represent this number. Returns 0 if the value is empty. */ public long getDigits() { if (value == null) { return 0; } return Math.round(Math.floor(Math.log10(value.doubleValue()) + 1)); } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy