package org.basex.query.util.format;
import static org.basex.query.QueryText.*;
import static org.basex.query.util.Err.*;
import static org.basex.util.Token.*;
import java.util.HashMap;
import java.util.Map.Entry;
import org.basex.query.QueryException;
import org.basex.query.expr.Calc;
import org.basex.query.func.FNNum;
import org.basex.query.item.Item;
import org.basex.query.item.Int;
import org.basex.util.Array;
import org.basex.util.InputInfo;
import org.basex.util.TokenBuilder;
import org.basex.util.hash.IntSet;
import org.basex.util.list.TokenList;
/**
* Formatter for decimal numbers.
*
* @author BaseX Team 2005-12, BSD License
* @author Christian Gruen
*/
public final class DecFormatter extends FormatUtil {
/** Decimal-digit-family (mandatory-digit-sign). */
private final String digits;
/** Active characters. */
private final String active;
/** Infinity. */
private String inf = "Infinity";
/** NaN. */
private String nan = "NaN";
/** Pattern-separator sign. */
private int pattern = ';';
/** Decimal-separator sign. */
private int decimal = '.';
/** Grouping-separator sign. */
private int grouping = ',';
/** Optional-digit sign. */
private int optional = '#';
/** Minus sign. */
private int minus = '-';
/** Percent sign. */
private int percent = '%';
/** Permille sign. */
private int permille = '\u2030';
/**
* Default constructor.
* @throws QueryException query exception
*/
public DecFormatter() throws QueryException {
this(null, null);
}
/**
* Constructor.
* @param ii input info
* @param map decimal format
* @throws QueryException query exception
*/
public DecFormatter(final InputInfo ii, final HashMap<String, String> map)
throws QueryException {
// assign map values
/* Zero-digit sign. */
int zero = '0';
if(map != null) {
for(final Entry<String, String> e : map.entrySet()) {
final String key = e.getKey(), val = e.getValue();
int cp = val.isEmpty() ? 0 : val.codePointAt(0);
if(Character.charCount(cp) != val.length()) cp = 0;
if(key.equals(DF_INF)) {
inf = val;
} else if(key.equals(DF_NAN)) {
nan = val;
} else if(cp != 0) {
if(key.equals(DF_DEC)) decimal = cp;
else if(key.equals(DF_GRP)) grouping = cp;
else if(key.equals(DF_PAT)) pattern = cp;
else if(key.equals(DF_MIN)) minus = cp;
else if(key.equals(DF_DIG)) optional = cp;
else if(key.equals(DF_PC)) percent = cp;
else if(key.equals(DF_PM)) permille = cp;
else if(key.equals(DF_ZG)) {
zero = zeroes(cp);
if(zero == -1) INVDECFORM.thrw(ii, key, val);
}
} else {
INVDECFORM.thrw(ii, key, val);
}
}
}
// check for duplicate characters
final IntSet is = new IntSet();
for(final int i : new int[] { decimal, grouping, percent, permille,
zero, optional, pattern }) {
if(is.add(i) < 0) DUPLDECFORM.thrw(ii, (char) i);
}
// create auxiliary strings
final TokenBuilder tb = new TokenBuilder();
for(int i = 0; i < 10; i++) tb.add(zero + i);
digits = tb.toString();
active = tb.add(decimal).add(grouping).add(optional).toString();
}
/**
* Returns a formatted number.
* @param ii input info
* @param number number to be formatted
* @param picture picture
* @return string representation
* @throws QueryException query exception
*/
public byte[] format(final InputInfo ii, final Item number,
final String picture) throws QueryException {
// find pattern separator and sub-patterns
final TokenList tl = new TokenList();
String pic = picture;
final int i = pic.indexOf(pattern);
if(i == -1) {
tl.add(pic);
} else {
tl.add(pic.substring(0, i));
pic = pic.substring(i + 1);
if(pic.indexOf(pattern) != -1) PICNUM.thrw(ii, picture);
tl.add(pic);
}
final byte[][] patterns = tl.toArray();
// check and analyze patterns
if(!check(patterns)) PICNUM.thrw(ii, picture);
final Picture[] pics = analyze(patterns);
// return formatted string
return token(format(number, pics, ii));
}
/**
* Checks the syntax of the specified patterns.
* @param patterns patterns
* @return result of check
*/
private boolean check(final byte[][] patterns) {
for(final byte[] pt : patterns) {
boolean frac = false, pas = false, act = false;
boolean dg = false, opt1 = false, opt2 = false;
int cl, pc = 0, pm = 0, ls = 0;
// loop through all characters
for(int i = 0; i < pt.length; i += cl) {
final int ch = ch(pt, i);
cl = cl(pt, i);
final boolean a = active.indexOf(ch) != -1;
if(ch == decimal) {
// more than 1 decimal sign?
if(frac) return false;
frac = true;
} else if(ch == grouping) {
// adjacent decimal sign?
if(i == 0 && frac || ls == decimal || i + cl < pt.length ?
ch(pt, i + cl) == decimal : !frac) return false;
} else if(ch == percent) {
if(++pc > 1) return false;
} else if(ch == permille) {
if(++pm > 1) return false;
} else if(ch == optional) {
if(!frac) {
// integer part, and optional sign after digit?
if(dg) return false;
opt1 = true;
} else {
opt2 = true;
}
} else if(digits.indexOf(ch) != -1) {
// fractional part, and digit after optional sign?
if(frac && opt2) return false;
dg = true;
}
// passive character with preceding and following active character?
if(a && pas && act) return false;
// will be assigned if active characters were found
if(act) pas |= !a;
act |= a;
// cache last character
ls = ch;
}
// more than 1 percent and permille sign?
if(pc + pm > 1) return false;
// no optional sign or digit?
if(!opt1 && !opt2 && !dg) return false;
}
return true;
}
/**
* Analyzes the specified patterns.
* @param patterns patterns
* @return picture variables
*/
private Picture[] analyze(final byte[][] patterns) {
// pictures
final Picture[] pics = new Picture[patterns.length];
// analyze patterns
for(int s = 0; s < patterns.length; ++s) {
final byte[] pt = patterns[s];
final Picture pic = new Picture();
// position (integer/fractional)
int p = 0;
// active character found
boolean act = false;
// number of optional characters
final int[] opt = new int[2];
// loop through all characters
for(int i = 0; i < pt.length; i += cl(pt, i)) {
final int ch = ch(pt, i);
final boolean a = active.indexOf(ch) != -1;
if(ch == decimal) {
++p;
act = false;
} else if(ch == optional) {
opt[p]++;
} else if(ch == grouping) {
if(p == 0) {
pic.group[p] = Array.add(pic.group[p], pic.min[p] + opt[p]);
}
} else if(digits.indexOf(ch) != -1) {
pic.min[p]++;
} else {
// passive characters
pic.pc |= ch == percent;
pic.pm |= ch == permille;
// prefixes/suffixes
pic.fix[p == 0 && act ? p + 1 : p].add(ch);
}
act |= a;
}
// finalize integer-part-grouping-positions
final int[] igp = pic.group[0];
final int igl = igp.length;
for(int g = 0; g < igl; ++g) igp[g] = pic.min[0] + opt[0] - igp[g];
// check if integer-part-grouping-positions are regular
// if yes, they are replaced with a single position
if(igl > 1) {
boolean reg = true;
final int i = igp[igl - 1];
for(int g = igl - 2; g >= 0; --g) reg &= i * igl == igp[g];
if(reg) pic.group[0] = new int[] { i };
}
pic.maxFrac = pic.min[1] + opt[1];
pics[s] = pic;
}
return pics;
}
/**
* Formats the specified number and returns a string representation.
* @param it item
* @param pics pictures
* @param ii input info
* @return picture variables
* @throws QueryException query exception
*/
private String format(final Item it, final Picture[] pics,
final InputInfo ii) throws QueryException {
// return results for NaN
final double d = it.dbl(ii);
if(Double.isNaN(d)) return nan;
// return infinite results
final Picture pic = pics[d < 0 && pics.length == 2 ? 1 : 0];
if(d == Double.POSITIVE_INFINITY) return pic.fix[0] + inf + pic.fix[1];
if(d == Double.NEGATIVE_INFINITY) return new TokenBuilder(
pic.fix[0].finish()).add(minus) + inf + pic.fix[1];
// convert and round number
Item num = it;
if(pic.pc) num = Calc.MULT.ev(ii, num, Int.get(100));
if(pic.pm) num = Calc.MULT.ev(ii, num, Int.get(1000));
num = FNNum.round(num, num.dbl(ii), pic.maxFrac, true, ii);
// remove sign: num = FNNum.abs(num);
// convert to string representation
String str = num.toString();
if(str.startsWith("0.")) str = str.substring(1);
// integer/fractional separator
final int sp = str.indexOf(decimal);
// create integer part
final TokenBuilder pre = new TokenBuilder();
final int il = sp == -1 ? str.length() : sp;
for(int i = il; i < pic.min[0]; ++i) pre.add('0');
pre.add(str.substring(0, il));
// squeeze in grouping separators
if(pic.group[0].length == 1) {
// regular pattern with repeating separators
final int pos = pic.group[0][0];
for(int p = pre.size() - 1; p > 0; --p) {
if(p % pos == 0) pre.insert(pre.size() - p, grouping);
}
} else {
// irregular pattern, or no separators at all
for(int i = 0; i < pic.group[0].length; ++i) {
final int pos = pre.size() - pic.group[0][i];
if(pos > 0) pre.insert(pos, grouping);
}
}
// create fractional part
final TokenBuilder suf = new TokenBuilder();
final int fl = sp == -1 ? 0 : str.length() - il - 1;
if(fl != 0) suf.add(str.substring(sp + 1));
for(int i = fl; i < pic.min[1]; ++i) suf.add('0');
// squeeze in grouping separators in a reverse manner
final int sl = suf.size();
for(int i = pic.group[1].length - 1; i >= 0; i--) {
final int pos = pic.group[1][i];
if(pos < sl) suf.insert(pos, grouping);
}
final TokenBuilder res = new TokenBuilder(pic.fix[0].finish());
res.add(pre.finish());
if(suf.size() != 0) res.add(decimal).add(suf.finish());
return res.add(pic.fix[1].finish()).toString();
}
/** Picture variables. */
static final class Picture {
/** prefix/suffix. */
final TokenBuilder[] fix = { new TokenBuilder(), new TokenBuilder() };
/** integer/fractional-part-grouping-positions. */
final int[][] group = { {}, {} };
/** minimum-integer/fractional-part-size. */
final int[] min = { 0, 0 };
/** maximum-fractional-part-size. */
int maxFrac;
/** percent flag. */
boolean pc;
/** per-mille flag. */
boolean pm;
}
}