Value.kt

/*
Copyright 2016 Hermann Krumrey <hermann@krumreyh.com>

This file is part of papio.

papio is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

papio 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 General Public License for more details.

You should have received a copy of the GNU General Public License
along with papio.  If not, see <http://www.gnu.org/licenses/>.
*/

package net.namibsun.papio.lib.money

import java.math.BigDecimal

/**
 * Class that models a generic monetary value.
 * A Value object is immutable.
 * All changing operations yield a new Value object and do not affect the original object.
 * @param value: The value of the Value as a BigDecimal, which allows arbitrary precision without rounding errors
 * @param currency: The currency of the Value
 */
@Suppress("EqualsOrHashCode")
data class Value(val value: BigDecimal, val currency: Currency) {

    /**
     * Generates a Value object from a String and a currency.
     * @param value: The String to parse a value from
     * @param currency: The currency of the value
     * @throws NumberFormatException: If the String does not contain a valid value
     */
    constructor(value: String, currency: Currency) : this(BigDecimal(value), currency)

    /**
     * Adds two Values and returns the sum
     * @param value: The value with which to create a sum with
     * @return The sum of the two values
     */
    operator fun plus(value: Value): Value {
        val converted = CurrencyConverter.convertValue(value.value, value.currency, this.currency)
        val sum = this.value.add(converted)
        return Value(sum, this.currency)
    }

    /**
     * Subtracts a value from this value and returns the result
     * @param value: The value to subtract from this value
     * @return: The result of the subtraction
     */
    operator fun minus(value: Value): Value {
        return this + !value
    }

    /**
     * Multiplies the value by an integer value
     * @param multiplicand: The value with which to multiply this value
     * @return: A new Value object whose value is the product of this value and the multiplicand
     */
    operator fun times(multiplicand: Int): Value {
        return Value(this.value.times(BigDecimal(multiplicand)), this.currency)
    }

    /**
     * Negates the value of this Value object. Equivalent to multiplying with -1
     * @return The negated value
     */
    operator fun not(): Value {
        return this * -1
    }

    /**
     * Overrides the data class equals method to use the compareTo instead of equals method of the BigDecimal
     * class. This ensures that 0.00 and 0.0 are treated the same.
     * @param other: The object to compare to
     * @return true if the objects are equal, false otherwise
     */
    override fun equals(other: Any?): Boolean {
        return if (other is Value) {
            this.currency == other.currency && this.value.compareTo(other.value) == 0
        } else {
            false
        }
    }

    /**
     * Converts this Value to another currency
     * @param currency: The currency to which to convert to
     * @return The converted Value
     */
    fun convert(currency: Currency? = null): Value {
        return if (currency == null || currency == this.currency) {
            Value(this.value, this.currency)
        } else {
            Value(CurrencyConverter.convertValue(this.value, this.currency, currency), currency)
        }
    }

    /**
     * Formats the value into a configurable human-readable String
     * @param useCurrencySymbol: Specifies if the currency's symbol (e.g. €) should be used or the currency's name (EUR)
     * @param decimalSymbol: The symbol to use as a decimal point. Defaults to a decimal point.
     * @param currencySymbolPositionBack: Specifies if the currency symbol is displayed before or after the value
     * @param overrideAccuracy: Overrides the inherent accuracy of a currency
     */
    fun format(
        useCurrencySymbol: Boolean = false,
        decimalSymbol: String = ".",
        currencySymbolPositionBack: Boolean = false,
        overrideAccuracy: Int? = null
    ): String {

        val currencySymbol = if (useCurrencySymbol) {
            this.currency.symbol
        } else {
            this.currency.name
        }

        val accuracy = overrideAccuracy ?: this.currency.displayAccuracy
        val formatted = this.value
                .setScale(accuracy, BigDecimal.ROUND_HALF_UP)
                .toString()
                .replace(".", decimalSymbol)

        return if (currencySymbolPositionBack) {
            "$formatted $currencySymbol"
        } else {
            "$currencySymbol $formatted"
        }
    }

    /**
     * Creates a string representation of the value that is automatically used in string interpolation
     * @return The String representation of the value
     */
    override fun toString(): String {
        return this.format()
    }

    /**
     * Serializes the value
     * @return The serialized String of the value
     */
    fun serialize(): String {
        return "${this.currency.name}:${this.value}"
    }

    /**
     * Contains static methods
     */
    companion object {

        /**
         * Generates a Value object from a serialized String
         * @param serialized: The serialized String
         * @return The generated Value object
         * @throws IllegalArgumentException If the currency does not exist
         * @throws IndexOutOfBoundsException If there aren't two individual sections for the currency and the value
         * @throws NumberFormatException If the value is not a valid number
         */
        fun deserialize(serialized: String): Value {
            val split = serialized.split(":")

            val currency = Currency.valueOf(split[0])
            val value = BigDecimal(split[1])

            return Value(value, currency)
        }
    }
}