package net.sf.openrocket.unit;
import java.text.DecimalFormat;
import java.text.NumberFormat;
import java.util.ArrayList;
import net.sf.openrocket.util.Chars;
public class FractionalUnit extends Unit {
private final static char FRACTION = Chars.FRACTION;
private final static String[] NUMERATOR = {
"\u2070", // 0
"\u00B9", // 1
"\u00B2", // 2
"\u00B3", // 3
"\u2074", // 4
"\u2075", // 5
"\u2076", // 6
"\u2077", // 7
"\u2078", // 8
"\u2079" // 9
};
private final static String[] DENOMINATOR = {
"\u2080", // 0
"\u2081", // 1
"\u2082", // 2
"\u2083", // 3
"\u2084", // 4
"\u2085", // 5
"\u2086", // 6
"\u2087", // 7
"\u2088", // 8
"\u2089" // 9
};
// This is the base of the fractions. ie, 16d for 1/16ths.
private final int fractionBase;
// This is 1d/fractionBase;
private final double fractionValue;
// This is the value used when incrementing/decrementing.
private final double incrementValue;
// If the actual value differs from the decimal representation by more than this,
// we display as decimals.
private final double epsilon;
private final String unitLabel;
public FractionalUnit(double multiplier, String unit, String unitLabel, int fractionBase, double incrementValue) {
this(multiplier, unit, unitLabel, fractionBase, incrementValue, 0.1d / fractionBase);
}
public FractionalUnit(double multiplier, String unit, String unitLabel, int fractionBase, double incrementValue, double epsilon) {
super(multiplier, unit);
this.unitLabel = unitLabel;
this.fractionBase = fractionBase;
this.fractionValue = 1.0d / fractionBase;
this.incrementValue = incrementValue;
this.epsilon = epsilon;
}
@Override
public double round(double value) {
return roundTo(value, fractionValue);
}
private double roundTo(double value, double fraction) {
double remainder = Math.IEEEremainder(value, fraction);
return value - remainder;
}
@Override
public double getNextValue(double value) {
double rounded = roundTo(value, incrementValue);
if (rounded <= value + epsilon) {
rounded += incrementValue;
}
return rounded;
}
@Override
public double getPreviousValue(double value) {
double rounded = roundTo(value, incrementValue);
if (rounded >= value - epsilon) {
rounded -= incrementValue;
}
return rounded;
}
@Override
public Tick[] getTicks(double start, double end, double minor, double major) {
// Convert values
start = toUnit(start);
end = toUnit(end);
minor = toUnit(minor);
major = toUnit(major);
if (minor <= 0 || major <= 0 || major < minor) {
throw new IllegalArgumentException("getTicks called with minor=" + minor + " major=" + major);
}
ArrayList<Tick> ticks = new ArrayList<Tick>();
int mod2, mod3, mod4; // Moduli for minor-notable, major-nonnotable, major-notable
double minstep;
// Find the smallest possible step size
double one = 1;
while (one > minor)
one /= 2;
while (one < minor)
one *= 2;
minstep = one;
mod2 = 16;
// Find step size for major ticks
one = 1;
while (one > major)
one /= 10;
while (one < major)
one *= 10;
if (one / 2 >= major) {
// major step is round-five, major-notable is next round-ten
double majorstep = one / 2;
mod3 = (int) Math.round(majorstep / minstep);
mod4 = mod3 * 2;
} else {
// major step is round-ten, major-notable is next round-ten
mod3 = (int) Math.round(one / minstep);
mod4 = mod3 * 10;
}
// Check for clashes between minor-notable and major-nonnotable
if (mod3 == mod2) {
if (mod2 == 2)
mod2 = 1; // Every minor tick is notable
else
mod2 = 5; // Every fifth minor tick is notable
}
// Calculate starting position
int pos = (int) Math.ceil(start / minstep);
// System.out.println("mod2="+mod2+" mod3="+mod3+" mod4="+mod4);
while (pos * minstep <= end) {
double unitValue = pos * minstep;
double value = fromUnit(unitValue);
if (pos % mod4 == 0)
ticks.add(new Tick(value, unitValue, true, true));
else if (pos % mod3 == 0)
ticks.add(new Tick(value, unitValue, true, false));
else if (pos % mod2 == 0)
ticks.add(new Tick(value, unitValue, false, true));
else
ticks.add(new Tick(value, unitValue, false, false));
pos++;
}
return ticks.toArray(new Tick[0]);
}
@Override
public String toString(double value) {
double correctVal = toUnit(value);
double val = round(correctVal);
if (Math.abs(val - correctVal) > epsilon) {
NumberFormat decFormat = new DecimalFormat("#.###");
return decFormat.format(correctVal);
}
NumberFormat intFormat = new DecimalFormat("#");
double sign = Math.signum(val);
double posValue = sign * val;
double intPart = Math.floor(posValue);
double frac = Math.rint((posValue - intPart) / fractionValue);
double fracBase = fractionBase;
// Reduce fraction.
while (frac > 0 && fracBase > 2 && frac % 2 == 0) {
frac /= 2.0;
fracBase /= 2.0;
}
posValue *= sign;
if (frac == 0.0) {
return intFormat.format(posValue);
} else if (intPart == 0.0) {
return (sign < 0 ? "-" : "") + numeratorString(Double.valueOf(frac).intValue())
+ FRACTION + denominatorString(Double.valueOf(fracBase).intValue());
} else {
return intFormat.format(sign * intPart) + " " + numeratorString(Double.valueOf(frac).intValue())
+ FRACTION + denominatorString(Double.valueOf(fracBase).intValue());
}
}
private String numeratorString(int value) {
String rep = "";
if (value == 0) {
return "0";
}
while (value > 0) {
rep = NUMERATOR[value % 10] + rep;
value = value / 10;
}
return rep;
}
private String denominatorString(int value) {
String rep = "";
if (value == 0) {
return "0";
}
while (value > 0) {
rep = DENOMINATOR[value % 10] + rep;
value = value / 10;
}
return rep;
}
@Override
public String toStringUnit(double value) {
if (Double.isNaN(value))
return "N/A";
String s = toString(value);
s += " " + unitLabel;
return s;
}
}