I was looking for A FixedDecimal Class for Java because I was not wanting to deal with the small discrepancies that pop up in a system around using floating point libraries. This is especially true when all you want to do is store money amounts in a database and add them up sometimes. I cut my teeth on COBOL which had these great fixed point libraries that were EXTREMELY fast and never made a mistake. COBOL mostly added money and their tools made it easy.
I couldn’t find one, so I created one of my own. It uses all integer math under the covers, so it’s fast and accurate, without that little drift that happens with floating point numbers. I made each instance immutable, so there’s not problems with threads.
The divide is a little bit different. I chose to make division integer division into a group of buckets, so if you divide $100.00 by three, you get two buckets with $33.33 and a third bucket with $33.34. If you are doing percentages, you should use the multiply functions.
Storing the Values
To store the values, you can either use the toString() function to store a string, or you can export the value to a long with toLong() and then fromLong() to put it back. Just remember that you have to use the right number of places on both ends for that to work, but this will be extremely fast to put in a database.
The Code
So, without further adieu, here’s the code.
package com.heavyweightsoftware.util;
public class FixedDecimal {
public static final int DEFAULT_PLACES = 2;
private static String decimalMark = ".";
private int places;
private long value;
/**
* Not recommended for normal use, this converts the value returned from toLong() back into a fixed decimal.
* @param exportedValue the exported value
* @param places the number of places
* @return a fixed decimal
*/
public static FixedDecimal fromLong(long exportedValue, int places) {
FixedDecimal result = new FixedDecimal(exportedValue, places);
return result;
}
public FixedDecimal(double value, int places) {
this.value = toLong(value, places);
setPlaces(places);
}
protected FixedDecimal(long value, int places) {
this.value = value;
setPlaces(places);
}
public FixedDecimal add(double addendum) {
long addVal = Math.round(addendum * Math.pow(10, places));
long newVal = value + addVal;
FixedDecimal result = new FixedDecimal(newVal, places);
return result;
}
public FixedDecimal add(long addendum) {
long addVal = addendum * (long) Math.pow(10, places);
long newVal = value + addVal;
FixedDecimal result = new FixedDecimal(newVal, places);
return result;
}
public FixedDecimal add(FixedDecimal addendum) {
int diff = places - addendum.places;
int newPlaces;
long longVal;
if (diff == 0) {
newPlaces = places;
longVal = value + addendum.value;
} else if (diff > 0) {
newPlaces = places;
long addVal = addendum.value * (long) Math.pow(10, diff);
longVal = value + addVal;
} else {
newPlaces = addendum.places;
long addVal = value * (long) Math.pow(10, Math.abs(diff));
longVal = addVal + addendum.value;
}
FixedDecimal result = new FixedDecimal(longVal, newPlaces);
return result;
}
public FixedDecimal[] divide(int buckets) {
long valueEach = value / buckets;
double each = valueEach / Math.pow(10, places);
long sum = 0;
FixedDecimal[] result = new FixedDecimal[buckets];
for (int ix = 0; ix < buckets; ++ix) {
if (ix < buckets - 1) {
result[ix] = new FixedDecimal(each, places);
sum += valueEach;
} else {
double remainder = (value - sum) / Math.pow(10, places);
result[ix] = new FixedDecimal(remainder, places);
}
}
return result;
}
public FixedDecimal multiplyBy(double factor) {
long multiplier = (long) Math.floor(factor * Math.pow(10, places));
long multiplied = value * multiplier;
long newVal = (long) Math.floor(multiplied / Math.pow(10, places));
FixedDecimal result = new FixedDecimal(newVal, places);
return result;
}
public FixedDecimal multiplyBy(long factor) {
long multiplier = factor * (long) Math.pow(10, places);
long multiplied = value * multiplier;
long newVal = (long) Math.floor(multiplied / Math.pow(10, places));
FixedDecimal result = new FixedDecimal(newVal, places);
return result;
}
public FixedDecimal multiplyBy(FixedDecimal factor) {
int diff = places - factor.places;
int newPlaces;
long longVal;
if (diff == 0) {
newPlaces = places;
longVal = value * factor.value;
} else if (diff > 0) {
newPlaces = places;
long adjuster = (long) Math.pow(10, diff);
long addVal = factor.value * adjuster;
longVal = (value * addVal) / adjuster;
} else {
newPlaces = factor.places;
long addVal = value * (long) Math.pow(10, Math.abs(diff));
longVal = addVal * factor.value;
}
FixedDecimal result = new FixedDecimal(longVal, newPlaces);
return result;
}
public FixedDecimal subtract(double subtrahend) {
long addVal = Math.round(subtrahend * Math.pow(10, places));
long newVal = value - addVal;
FixedDecimal result = new FixedDecimal(newVal, places);
return result;
}
public FixedDecimal subtract(long subtrahend) {
long addVal = subtrahend * (long) Math.pow(10, places);
long newVal = value - addVal;
FixedDecimal result = new FixedDecimal(newVal, places);
return result;
}
public FixedDecimal subtract(FixedDecimal subtrahend) {
int diff = places - subtrahend.places;
int newPlaces;
long longVal;
if (diff == 0) {
newPlaces = places;
longVal = value - subtrahend.value;
} else if (diff > 0) {
newPlaces = places;
long addVal = subtrahend.value * (long) Math.pow(10, diff);
longVal = value - addVal;
} else {
newPlaces = subtrahend.places;
long addVal = value * (long) Math.pow(10, Math.abs(diff));
longVal = addVal - subtrahend.value;
}
FixedDecimal result = new FixedDecimal(longVal, newPlaces);
return result;
}
private void setPlaces(int places) {
if (places >= 0) {
this.places = places;
} else {
throw new IllegalArgumentException("Places less than zero:" + places);
}
}
private long toLong(double value, int places) {
int multiplier = (int) Math.pow(10, places);
long result = Math.round(value * multiplier);
return result;
}
/**
* Should really only store this value
* @return a long value version of this fixed decimal
*/
public long toLong() {
return value;
}
@Override
public String toString() {
String str = Long.toString(value);
int len = str.length();
StringBuilder sb = new StringBuilder();
if (len <= places) {
sb.append(decimalMark);
sb.append(StringHelper.zeroPad(value, places));
} else {
int intLength = str.length() - places;
sb.append(str.substring(0, intLength));
sb.append(decimalMark);
sb.append(str.substring(intLength, intLength + places));
}
return sb.toString();
}
@Override
public boolean equals(Object other) {
if (this == other) return true;
if (!(other instanceof FixedDecimal)) return false;
FixedDecimal that = (FixedDecimal) other;
return places == that.places &&
value == that.value;
}
@Override
public int hashCode() {
return Objects.hash(places, value);
}
}