package org.basex.query.util.format;
import static org.basex.query.util.Err.*;
import static org.basex.util.Token.*;
import java.util.Calendar;
import java.util.HashMap;
import java.util.Locale;
import javax.xml.datatype.XMLGregorianCalendar;
import org.basex.query.QueryException;
import org.basex.query.item.AtomType;
import org.basex.query.item.Date;
import org.basex.util.InputInfo;
import org.basex.util.Reflect;
import org.basex.util.TokenBuilder;
import org.basex.util.Util;
import org.basex.util.list.IntList;
/**
* Abstract class for formatting data in different languages.
*
* @author BaseX Team 2005-12, BSD License
* @author Christian Gruen
*/
public abstract class Formatter extends FormatUtil {
/** Language code: English. */
private static final String EN = "en";
/** Formatter instances. */
private static final HashMap<String, Formatter> MAP =
new HashMap<String, Formatter>();
// initialize hash map with English formatter as default
static { MAP.put(EN, new FormatterEN()); }
/**
* Returns a formatter for the specified language.
* @param ln language
* @return formatter instance
*/
public static Formatter get(final String ln) {
// check if formatter has already been created
Formatter form = MAP.get(ln);
if(form == null) {
final String clz = Util.name(Formatter.class) +
ln.toUpperCase(Locale.ENGLISH);
form = (Formatter) Reflect.get(Reflect.find(clz));
// instantiation not successful: return default formatter
if(form == null) form = MAP.get(EN);
}
return form;
}
/**
* Returns a word representation for the specified number.
* @param n number to be formatted
* @param ord ordinal suffix
* @return token
*/
protected abstract byte[] word(final long n, final byte[] ord);
/**
* Returns an ordinal representation for the specified number.
* @param n number to be formatted
* @param ord ordinal suffix
* @return ordinal
*/
protected abstract byte[] ordinal(final long n, final byte[] ord);
/**
* Returns the specified month (0-11).
* @param n number to be formatted
* @param min minimum length
* @param max maximum length
* @return month
*/
protected abstract byte[] month(final int n, final int min, final int max);
/**
* Returns the specified day of the week (0-6, Sunday-Saturday).
* @param n number to be formatted
* @param min minimum length
* @param max maximum length
* @return day of week
*/
protected abstract byte[] day(final int n, final int min, final int max);
/**
* Returns the am/pm marker.
* @param am am flag
* @return am/pm marker
*/
protected abstract byte[] ampm(final boolean am);
/**
* Returns the calendar.
* @return calendar
*/
protected abstract byte[] calendar();
/**
* Returns the era.
* @param year year
* @return era
*/
protected abstract byte[] era(final int year);
/**
* Formats the specified date.
* @param date date to be formatted
* @param pic picture
* @param cal calendar
* @param plc place
* @param ii input info
* @return formatted string
* @throws QueryException query exception
*/
public final byte[] formatDate(final Date date, final byte[] pic,
final byte[] cal, final byte[] plc, final InputInfo ii)
throws QueryException {
// [CG] XQuery/Formatter: currently, calendars and places are ignored
if(cal != null || plc != null);
final TokenBuilder tb = new TokenBuilder();
final DateParser dp = new DateParser(ii, pic);
while(dp.more()) {
final int ch = dp.next();
if(ch != 0) {
// print literal
tb.add(ch);
} else {
byte[] p = dp.marker();
if(p.length == 0) PICDATE.thrw(ii, pic);
final int spec = ch(p, 0);
p = substring(p, cl(p, 0));
byte[] pres = ONE;
boolean max = false;
long num = 0;
final boolean dat = date.type == AtomType.DAT;
final boolean tim = date.type == AtomType.TIM;
final XMLGregorianCalendar gc = date.xc;
boolean err = false;
switch(spec) {
case 'Y':
num = Math.abs(gc.getYear());
max = true;
err = tim;
break;
case 'M':
num = gc.getMonth();
err = tim;
break;
case 'D':
num = gc.getDay();
err = tim;
break;
case 'd':
num = Date.days(0, gc.getMonth(), gc.getDay());
err = tim;
break;
case 'F':
num = gc.toGregorianCalendar().get(Calendar.DAY_OF_WEEK) - 1;
pres = new byte[] { 'n' };
err = tim;
break;
case 'W':
num = gc.toGregorianCalendar().get(Calendar.WEEK_OF_YEAR);
err = tim;
break;
case 'w':
num = gc.toGregorianCalendar().get(Calendar.WEEK_OF_MONTH);
err = tim;
break;
case 'H':
num = gc.getHour();
err = dat;
break;
case 'h':
num = gc.getHour() % 12;
if(num == 0) num = 12;
err = dat;
break;
case 'P':
num = gc.getHour() / 12;
pres = new byte[] { 'n' };
err = dat;
break;
case 'm':
num = gc.getMinute();
pres = token("01");
err = dat;
break;
case 's':
num = gc.getSecond();
pres = token("01");
err = dat;
break;
case 'f':
num = gc.getMillisecond();
err = dat;
break;
case 'Z':
case 'z':
num = gc.getTimezone();
pres = token("01:01");
break;
case 'C':
pres = new byte[] { 'n' };
break;
case 'E':
num = gc.getYear();
pres = new byte[] { 'n' };
break;
default:
err = true;
break;
}
if(err) PICCOMP.thrw(ii, pic);
final FormatParser fp = new FormatParser(ii, p, pres);
if(max) {
// limit maximum length of numeric output
int mx = 0;
for(int s = 0; s < fp.primary.length; s += cl(fp.primary, s)) mx++;
if(mx > 1) fp.max = mx;
}
if(fp.digit == 'n') {
byte[] in = EMPTY;
if(spec == 'M') {
in = month((int) num - 1, fp.min, fp.max);
} else if(spec == 'F') {
in = day((int) num, fp.min, fp.max);
} else if(spec == 'P') {
in = ampm(num == 0);
} else if(spec == 'C') {
in = calendar();
} else if(spec == 'E') {
in = era((int) num);
}
if(fp.cs == Case.LOWER) in = lc(in);
if(fp.cs == Case.UPPER) in = uc(in);
tb.add(in);
} else {
tb.add(formatInt(num, fp));
}
}
}
return tb.finish();
}
/**
* Returns a formatted integer.
* @param num integer to be formatted
* @param fp format parser
* @return string representation
*/
public final byte[] formatInt(final long num, final FormatParser fp) {
// choose sign
long n = num;
final boolean sign = n < 0;
if(sign) n = -n;
final TokenBuilder tb = new TokenBuilder();
final int ch = fp.digit;
final boolean single = fp.primary.length == cl(fp.primary, 0);
if(ch == 'w') {
tb.add(word(n, fp.ordinal));
} else if(ch == KANJI[1]) {
japanese(tb, n);
} else if(single && ch == 'i') {
roman(tb, n);
} else if(ch >= '\u2460' && ch <= '\u249b') {
if(num < 1 || num > 20) tb.addLong(num);
else tb.add((int) (ch + num - 1));
} else {
final String seq = sequence(ch);
if(seq != null) alpha(tb, num, seq);
else tb.add(number(n, fp, zeroes(ch)));
}
// finalize formatted string
byte[] in = tb.finish();
if(fp.cs == Case.LOWER) in = lc(in);
if(fp.cs == Case.UPPER) in = uc(in);
return sign ? concat(new byte[] { '-' }, in) : in;
}
/**
* Returns a character sequence based on the specified alphabet.
* @param tb token builder
* @param n number to be formatted
* @param a alphabet
*/
private static void alpha(final TokenBuilder tb, final long n,
final String a) {
final int al = a.length();
if(n > al) alpha(tb, (n - 1) / al, a);
if(n > 0) tb.add(a.charAt((int) ((n - 1) % al)));
else tb.add(ZERO);
}
/**
* Adds a Roman character sequence.
* @param tb token builder
* @param n number to be formatted
*/
private static void roman(final TokenBuilder tb, final long n) {
if(n > 0 && n < 4000) {
final int v = (int) n;
tb.add(ROMANM[v / 1000]);
tb.add(ROMANC[v / 100 % 10]);
tb.add(ROMANX[v / 10 % 10]);
tb.add(ROMANI[v % 10]);
} else {
tb.addLong(n);
}
}
/**
* Adds a Japanese character sequence.
* @param tb token builder
* @param n number to be formatted
*/
private static void japanese(final TokenBuilder tb, final long n) {
if(n == 0) {
tb.add(KANJI[0]);
} else {
jp(tb, n, false);
}
}
/**
* Recursively adds a Japanese character sequence.
* @param tb token builder
* @param n number to be formatted
* @param i initial call
*/
private static void jp(final TokenBuilder tb, final long n, final boolean i) {
if(n == 0) {
} else if(n <= 9) {
if(n != 1 || !i) tb.add(KANJI[(int) n]);
} else if(n == 10) {
tb.add(KANJI[10]);
} else if(n <= 99) {
jp(tb, n, 10, 10);
} else if(n <= 999) {
jp(tb, n, 100, 11);
} else if(n <= 9999) {
jp(tb, n, 1000, 12);
} else if(n <= 99999999) {
jp(tb, n, 10000, 13);
} else if(n <= 999999999999L) {
jp(tb, n, 100000000, 14);
} else if(n <= 9999999999999999L) {
jp(tb, n, 1000000000000L, 15);
} else {
tb.addLong(n);
}
}
/**
* Recursively adds a Japanese character sequence.
* @param tb token builder
* @param n number to be formatted
* @param f factor
* @param o kanji offset
*/
private static void jp(final TokenBuilder tb, final long n, final long f,
final int o) {
jp(tb, n / f, true);
tb.add(KANJI[o]);
jp(tb, n % f, false);
}
/**
* Creates a number character sequence.
* @param num number to be formatted
* @param fp format parser
* @param z zero digit
* @return number character sequence
*/
private byte[] number(final long num, final FormatParser fp, final int z) {
// cache characters of presentation modifier
final IntList pr = new IntList(fp.primary.length);
for(int p = 0; p < fp.primary.length; p += cl(fp.primary, p)) {
pr.add(cp(fp.primary, p));
}
// check for a regular separator pattern
int rp = -1;
boolean reg = false;
for(int p = pr.size() - 1; p >= 0; --p) {
final int ch = pr.get(p);
if(ch == '#' || ch >= z && ch <= z + 9) continue;
if(rp == -1) rp = pr.size() - p;
reg = (pr.size() - p) % rp == 0;
}
final int rc = reg ? pr.get(pr.size() - rp) : 0;
if(!reg) rp = Integer.MAX_VALUE;
// build string representation in a reverse order
final IntList cache = new IntList();
final byte[] n = token(num);
int b = n.length - 1, p = pr.size() - 1;
// add numbers and separators
int mn = fp.min;
int mx = fp.max;
while((--mn >= 0 || b >= 0 || p >= 0) && --mx >= 0) {
final boolean sep = cache.size() % rp == rp - 1;
if(p >= 0) {
final int c = pr.get(p--);
if(b >= 0) {
if(c == '#' && sep) cache.add(rc);
cache.add(c == '#' || c >= z && c <= z + 9 ? n[b--] - '0' + z : c);
} else {
// add remaining modifiers
if(c == '#') break;
cache.add(c >= z && c <= z + 9 ? z : c);
}
} else if(b >= 0) {
// add remaining numbers
if(sep) cache.add(rc);
cache.add(n[b--] - '0' + z);
} else {
// add minimum numbers
cache.add(z);
}
}
// reverse result and add ordinal suffix
final TokenBuilder tb = new TokenBuilder();
for(int c = cache.size() - 1; c >= 0; --c) tb.add(cache.get(c));
return tb.add(ordinal(num, fp.ordinal)).finish();
}
}