Money.java

package sk.iway.iwcm.ebanking;

import java.util.ArrayList;
import java.util.Comparator;
import java.util.Currency;
import java.util.List;

/**
 *  Money.java
 *  
 *  Represents monetary amount. Based on {@link Long} data type,
 *  thus avoiding float/double money antipattern, notoriously
 *  known for imprecise allocation and rounding.
 *
 *  Internally, the amount is held in the smallest monetary
 *  unit of the currency: usually in cents. 
 *  
 *  Use getDouble() to obtain the amount in "dollars"
 *  Use getAmount() to obtain number of "cents"
 *  
 *  @see MoneyLongConverter for JPA transformation
 *  @see RoundingStrategy 
 *  @see CurrencyConverter
 *
 *@Title        webjet7
 *@Company      Interway s.r.o. (www.interway.sk)
 *@Copyright    Interway s.r.o. (c) 2001-2010
 *@author       $Author: marosurbanec $
 *@version      $Revision: 1.3 $
 *@created      Date: 3.11.2010 13:48:10
 *@modified     $Date: 2004/08/16 06:26:11 $
 */
public class Money implements Comparable<Money>
{
	private final long amount;
	private final Currency currency;
	
	private Money(long amount, Currency currency)
	{
		this.amount = amount;
		this.currency = currency;
	}
	
	//---------------CREATION METHODS------------------------
	
	public static final Money NOTHING = Money.fromEuroCents(0);
	
	public static Money fromCents(long amount, Currency currency)
	{
		return new Money(amount, currency);
	}
	
	public static Money fromEuroCents(long amount)
	{
		return fromCents(amount, Currency.getInstance("EUR"));
	}
	
	public static Money fromDouble(double amount, Currency currency)
	{
		if (currency.getDefaultFractionDigits() < 1)
			return new Money((long)amount, currency);
		
		long amountAsLong = (long)(Math.round(amount * Math.pow(10.0, currency.getDefaultFractionDigits())));
		return new Money(amountAsLong, currency);
	}
	
	public static Money fromEuroDouble(double amount)
	{
		return fromDouble(amount, Currency.getInstance("EUR"));
	}
	
	//---------------OPERATIONS------------------------
	
	public Money plus(Money whatToAdd)
	{
		if (whatToAdd.getCurrency() != getCurrency())
			throw new IllegalArgumentException("Cannot add "+this+" and "+whatToAdd);
		return new Money(amount + whatToAdd.getAmount(), currency);
	}
	
	public Money plus(Money whatToAdd, CurrencyConverter converter)
	{
		Money whatToAddInOutCurrency = converter.convert(whatToAdd, this.currency);
		return new Money(amount + whatToAddInOutCurrency.getAmount(), currency);
	}
	
	public Money minus(Money whatToSubtract)
	{
		return plus(new Money(-whatToSubtract.getAmount(), whatToSubtract.getCurrency()));
	}
	
	public Money minus(Money whatToSubtract, CurrencyConverter converter)
	{
		return plus(new Money(-whatToSubtract.getAmount(), whatToSubtract.getCurrency()), converter);
	}
	
	public Money times(double scale)
	{
		return fromDouble(amount*scale, currency);
	}
	
	public Money divide(double scale)
	{
		return fromDouble(amount/scale, currency);
	}
	
	public Money round()
	{
		return RoundingStrategies.getRoundingStrategy(currency).round(this);
	}
	
	public Money round(RoundingStrategy rounding)
	{
		return rounding.round(this);
	}
	
	public List<Money> splitIntoParts(int numberOfParts)
	{
		long leftToAssign = amount;
		int signum = amount > 0 ? 1 : -1;
		List<Money> split = new ArrayList<Money>();
		
		//distribute the main part
		for (int i = 0 ; i < numberOfParts ; i++)
		{
			Money part = Money.fromCents(amount / numberOfParts, currency);
			leftToAssign -= part.getAmount();
			split.add(part);
		}
		
		//distribute leftovers evenly
		int asigneeIndex = 0;
		while(leftToAssign*signum > 0)
		{
			Money newPart = split.get(asigneeIndex % numberOfParts).plus(Money.fromCents(signum, currency));
			split.set(asigneeIndex % numberOfParts, newPart);
			leftToAssign -= signum;
			asigneeIndex++;
		}
		return split;
	}
	
	//--------------GETTERS AND SETTERS-------------------

	public double getDouble()
	{
		if (currency.getDefaultFractionDigits() < 1)
			return (double)amount;
		
		return amount / Math.pow(10.0, currency.getDefaultFractionDigits());
	}

	public long getAmount()
	{
		return amount;
	}

	public Currency getCurrency()
	{
		return currency;
	}
	
	//------------UTILITY METHODS---------------
	
	/**
	 * Compares 2 {@link Money} instances. 
	 * Work if and only if both instances are of the same currency.
	 * 
	 * Use your own {@link Comparator} if currencies differ
	 */
	@Override
	public int compareTo(Money anotherMoney)
	{
		if (anotherMoney.currency != this.currency)
			throw new IllegalStateException("Comparing two money of unequal currencies: "+this+" ,"+anotherMoney);
		return Long.signum(this.amount - anotherMoney.amount);
	}
	
	@Override
	public int hashCode()
	{
		final int prime = 31;
		int result = 1;
		result = prime * result + (int) (amount ^ (amount >>> 32));
		result = prime * result + ((currency == null) ? 0 : currency.hashCode());
		return result;
	}

	@Override
	public boolean equals(Object obj)
	{
		if (this == obj)
			return true;
		if (obj == null)
			return false;
		if (getClass() != obj.getClass())
			return false;
		Money other = (Money) obj;
		if (amount != other.amount)
			return false;
		if (currency == null)
		{
			if (other.currency != null)
				return false;
		}
		else if (currency != other.currency)
			return false;
		return true;
	}

	@Override
	public String toString()
	{
		return String.format("%.2f %s", getDouble(), currency.toString());
	}
}