net.snowflake.common.core.SqlFormat Maven / Gradle / Ivy
/*
* Copyright (c) 2016 Snowflake Computing Inc. All right reserved.
*/
package net.snowflake.common.core;
import java.math.BigInteger;
import java.text.*;
import java.util.*;
import net.snowflake.common.util.Power10;
/** The is the class for a parsed SQL format, a re-implementation of XP SqlFormat class */
public final class SqlFormat {
// Supported format models
public static final int INVALID = 0;
public static final int NUMERIC = 1;
public static final int DATE = 2;
public static final int TIME = 4;
public static final int TZONE = 8;
public static final int TS_NTZ = TIME | DATE;
public static final int TS_TZ = TIME | DATE | TZONE;
public static final int NETWORK = 16;
public static final int ANY = -1;
// The default century boundary
public static final int DEFAULT_CENTURY_BOUNDARY = 1970;
/** Constructor */
public SqlFormat() {
m_errorMsg = "not initialized";
m_model = INVALID;
}
/**
* Get error message
*
* @return error message
*/
public String getErrorMsg() {
return m_errorMsg;
}
/**
* Get format model
*
* @return model
*/
public int getModel() {
return m_model;
}
/**
* Get max output length
*
* @return max output length
*/
public int getMaxOutputSize() {
return m_maxOutLen;
}
/**
* Parse single format Returns remaining format string (for multiple bar-separated formats) or
* null for an error
*
* If a singular format is required, the user must check that the output is empty
*
*
Sets an error message
*
* @param model format model (see above)
* @param fmtStr is the format string
* @return null on an error the remainder of the format string (following |)
*/
public String setFormat(int model, String fmtStr) {
// Clean slate
m_model = model;
m_seen = EnumSet.noneOf(Keyword.class);
m_frags = new ArrayList();
m_maxOutLen = 0; // no trailing 0s in Java
m_precision = 0;
m_scale = 0;
m_reqDigits = 0;
m_minScale = 0;
m_tExact = false;
m_errorMsg = null;
int fmtLen = fmtStr.length();
if (fmtLen > 4096) {
m_errorMsg = "format string is too long";
return null;
}
int modelSelector = 0;
StringBuilder errMsg;
StringBuilder literal = new StringBuilder();
// The main loop
char fmt[] = fmtStr.toCharArray();
int fmtIdx = 0;
while (fmtIdx < fmtLen) {
char c = fmt[fmtIdx++];
boolean collectKw = false;
switch (c) {
default:
if (Character.isLetterOrDigit(c)) {
collectKw = true;
break;
}
errMsg = new StringBuilder("invalid character in the format string: '");
if (' ' <= c && c <= '~') errMsg.append(c);
else errMsg.append("\\u").append(String.valueOf((int) c));
errMsg.append('\'');
m_errorMsg = errMsg.toString();
return null;
case '|':
fmtLen = fmtIdx; // exit from the loop
continue;
case '.': // dot before or after a digit is the decimal dot
if (fmtIdx < fmtLen && (fmt[fmtIdx] == '0' || fmt[fmtIdx] == '9')) {
collectKw = true;
break;
}
if (m_seen.contains(Keyword.DIGIT) || m_seen.contains(Keyword.ZERO)) {
collectKw = true;
break;
}
literal.append(c);
break;
case ',': // comma after a digit is a group separator
if (m_seen.contains(Keyword.DIGIT)
|| m_seen.contains(Keyword.ZERO)
|| m_seen.contains(Keyword.X)) {
collectKw = true;
break;
}
case '-':
case '=':
case '/':
case ';':
case ':':
case '(':
case ')':
case ' ':
literal.append(c);
break;
case '$':
case '_':
collectKw = true;
break;
case '"':
{
boolean fin = false;
while (fmtIdx < fmtLen) {
c = fmt[fmtIdx++];
if (c == '"') {
if (fmtIdx >= fmtLen || fmt[fmtIdx] != '"') {
fin = true;
break;
}
fmtIdx++;
}
literal.append(c);
}
if (fin) break;
}
errMsg = new StringBuilder("missing closing \" in the literal: '");
errMsg.append(fmt, 0, fmtLen).append('\'');
m_errorMsg = errMsg.toString();
return null;
}
// Are we collecting the keyword?
if (collectKw) {
// Check if we had a literal prior to this keyword
if (literal.length() > 0) {
m_maxOutLen += literal.length();
m_frags.add(new Fragment(literal.toString()));
literal = new StringBuilder();
}
// Find the keyword
fmtIdx--;
Keyword kw = findKeyword(fmt, fmtLen, fmtIdx, model);
if (kw == null) {
// Collect the format keyword
int maxLen = fmtLen - fmtIdx;
if (maxLen > Keyword.MAX_KW_LEN) maxLen = Keyword.MAX_KW_LEN;
int l = 0;
while (l < maxLen) {
char xc = fmt[l + fmtIdx];
if (!Character.isLetterOrDigit(xc) && xc != '$' && xc != '_') break;
++l;
}
String mt = "";
switch (model) {
default:
break;
case NUMERIC:
mt = "numeric ";
break;
case DATE:
mt = "date ";
break;
case TIME:
mt = "time ";
break;
case TS_NTZ:
mt = "timestamp_ntz ";
break;
case TS_TZ:
mt = "timestamp ";
break;
}
errMsg = new StringBuilder("invalid ");
errMsg.append(mt).append("format keyword: '").append(fmt, fmtIdx, l).append('\'');
m_errorMsg = errMsg.toString();
return null;
}
int kwLen = kw.str.length();
// If this is a parametrized keyword, collect the parameter
int param = 0;
if (kw.maxParam > 0) {
int oldKwLen = kwLen;
while (fmtIdx + kwLen < fmtLen) {
char xc = fmt[fmtIdx + kwLen];
if ('0' > xc || xc > '9') break;
++kwLen;
if (param >= 10000) break;
param = param * 10 + (xc - '0');
}
if (param == 0 && oldKwLen < kwLen) {
errMsg = new StringBuilder("zero is not allowed as format parameter value: '");
errMsg.append(fmt, fmtIdx, kwLen).append('\'');
m_errorMsg = errMsg.toString();
return null;
}
if (param > kw.maxParam) {
errMsg = new StringBuilder("format parameter value too large: '");
errMsg.append(fmt, fmtIdx, kwLen).append('\'');
m_errorMsg = errMsg.toString();
return null;
}
}
// Check if this element has been seen already
if (kw.repeat) {
// Special case for digits
if (m_seen.contains(Keyword.DIGIT)
|| m_seen.contains(Keyword.ZERO)
|| m_seen.contains(Keyword.X)) {
// Check exponent placement
if (m_seen.contains(Keyword.EE)
|| m_seen.contains(Keyword.EEE)
|| m_seen.contains(Keyword.EEEE)
|| m_seen.contains(Keyword.EEEEE)) {
errMsg = new StringBuilder("digit position after an exponent format element: '");
errMsg.append(fmt, 0, fmtLen).append('\'');
m_errorMsg = errMsg.toString();
return null;
}
++m_precision;
if (m_seen.contains(Keyword.D) || m_seen.contains(Keyword.DOT)) {
++m_scale;
if (kw == Keyword.ZERO) m_minScale = m_scale;
} else if (kw == Keyword.ZERO || m_seen.contains(Keyword.ZERO)) ++m_reqDigits;
}
// Special case for FX
else if (kw == Keyword.FX) m_tExact = !m_tExact;
} else {
if (m_seen.contains(kw)) {
errMsg = new StringBuilder("format element occurs more than once: '");
errMsg.append(fmt, fmtIdx, kwLen).append('\'');
m_errorMsg = errMsg.toString();
return null;
}
EnumSet cset = keywordConflicts.get(kw);
if (cset != null) {
EnumSet dups = EnumSet.copyOf(cset);
dups.retainAll(m_seen);
if (!dups.isEmpty()) {
errMsg = new StringBuilder("format element conflicts with preceding element(s): '");
errMsg.append(fmt, fmtIdx, kwLen).append('\'');
m_errorMsg = errMsg.toString();
return null;
}
}
}
m_seen.add(kw);
// Update length estimate
m_maxOutLen += (kw == Keyword.FF && param > 0) ? param : kw.maxLen;
// If the keyword selects a specific model, add to the model list
int modelMask = kw.model & m_model;
if (((modelMask - 1) & modelMask) == 0) modelSelector |= modelMask;
// Compute case indicator
int cc = 0;
if (kw.caseSens) {
char xc = fmt[fmtIdx];
if ('a' <= xc && xc <= 'z') cc = 1;
if (kw.str.length() > 1) {
xc = fmt[fmtIdx + 1];
if ('a' <= xc && xc <= 'z') cc |= 2;
}
}
// Save the fragment
m_frags.add(new Fragment(kw, cc, param));
fmtIdx += kwLen;
}
}
// Check if we have a final literal
if (literal.length() > 0) {
m_maxOutLen += literal.length();
m_frags.add(new Fragment(literal.toString()));
}
// Pure literal format is acceptable
if (m_seen.isEmpty()) {
m_model = ANY;
return fmtStr.substring(fmtLen); // truncated format string...
}
// Is requested model allows both numeric and time/date formats?
if (modelSelector == INVALID) {
if ((m_model & NUMERIC) != 0 && (m_model & TS_TZ) != 0) {
errMsg = new StringBuilder("ambiguous format string: '");
errMsg.append(fmt, 0, fmtLen).append('\'');
m_errorMsg = errMsg.toString();
return null;
}
modelSelector = m_model;
}
// Check the format compatibility...
switch (modelSelector) {
case NUMERIC:
if (m_seen.contains(Keyword.X)) {
if (m_seen.contains(Keyword.DIGIT)
|| m_seen.contains(Keyword.TM)
|| m_seen.contains(Keyword.TM9)
|| m_seen.contains(Keyword.TME)) {
errMsg = new StringBuilder("cannot mix hexadecimal and decimal format elements: '");
errMsg.append(fmt, 0, fmtLen).append('\'');
m_errorMsg = errMsg.toString();
return null;
}
if (m_seen.contains(Keyword.D) || m_seen.contains(Keyword.DOT)) {
errMsg = new StringBuilder("hexadecimal fractions are not supported: '");
errMsg.append(fmt, 0, fmtLen).append('\'');
m_errorMsg = errMsg.toString();
return null;
}
if (m_seen.contains(Keyword.G) || m_seen.contains(Keyword.GROUP)) {
errMsg = new StringBuilder("hexadecimal digit group separators are not supported: '");
errMsg.append(fmt, 0, fmtLen).append('\'');
m_errorMsg = errMsg.toString();
return null;
}
if (m_seen.contains(Keyword.EE)
|| m_seen.contains(Keyword.EEE)
|| m_seen.contains(Keyword.EEEE)
|| m_seen.contains(Keyword.EEEEE)) {
errMsg = new StringBuilder("hexadecimal exponents are not supported: '");
errMsg.append(fmt, 0, fmtLen).append('\'');
m_errorMsg = errMsg.toString();
return null;
}
m_maxOutLen += 1; // always reserve one for the negative sign
} else if (m_seen.contains(Keyword.TM)
|| m_seen.contains(Keyword.TM9)
|| m_seen.contains(Keyword.TME)) {
if (m_seen.contains(Keyword.DIGIT)
|| m_seen.contains(Keyword.ZERO)
|| m_seen.contains(Keyword.DOT)
|| m_seen.contains(Keyword.D)
|| m_seen.contains(Keyword.GROUP)
|| m_seen.contains(Keyword.G)
|| m_seen.contains(Keyword.EE)
|| m_seen.contains(Keyword.EEE)
|| m_seen.contains(Keyword.EEEE)
|| m_seen.contains(Keyword.EEEEE)) {
errMsg = new StringBuilder("cannot mix TM and digit-based numeric format elements: '");
errMsg.append(fmt, 0, fmtLen).append('\'');
m_errorMsg = errMsg.toString();
return null;
}
} else if (!m_seen.contains(Keyword.DIGIT) && !m_seen.contains(Keyword.ZERO)) {
errMsg = new StringBuilder("no digit format elements in a numeric format: '");
errMsg.append(fmt, 0, fmtLen).append('\'');
m_errorMsg = errMsg.toString();
return null;
} else m_maxOutLen += 1; // always reserve one for the negative sign in decimals
if (m_precision >= 39) {
errMsg = new StringBuilder("too many digits in numeric format: '");
errMsg.append(fmt, 0, fmtLen).append('\'');
m_errorMsg = errMsg.toString();
return null;
}
// one integer digit is always required, unless there's B
if (m_reqDigits == 0 && !m_seen.contains(Keyword.ZERO)) m_reqDigits = 1;
// For decimal positional formats, enforce integrity of literals
if (m_seen.contains(Keyword.DIGIT)
|| m_seen.contains(Keyword.ZERO)
|| m_seen.contains(Keyword.X)) {
// Find the last significant non-literal
int last = m_frags.size();
while (last > 0) {
Fragment f = m_frags.get(--last);
if (f.m_elem != Keyword.FM
&& f.m_elem != Keyword.FX
&& f.m_elem != Keyword.OPTSP
&& f.m_elem != Keyword.LITERAL) break;
}
// Skip leading literals
int i = 0;
while (i < last) {
Fragment f = m_frags.get(i);
if (f.m_elem != Keyword.B
&& f.m_elem != Keyword.FM
&& f.m_elem != Keyword.FX
&& f.m_elem != Keyword.OPTSP
&& f.m_elem != Keyword.LITERAL) break;
++i;
}
// For all literals in between check that they do not
// contain digits and/or special signs
while (i < last) {
Fragment f = m_frags.get(i++);
if (f.m_elem == Keyword.LITERAL) {
if (m_seen.contains(Keyword.X)) {
if (f.m_literal.matches(".*[0-9a-fA-F].*")) {
errMsg =
new StringBuilder(
"literals within hexadecimal numbers cannot contain hex digits: '");
errMsg.append(fmt, 0, fmtLen).append('\'');
m_errorMsg = errMsg.toString();
return null;
}
} else {
if (f.m_literal.matches(".*[0-9.,eE].*")) {
errMsg =
new StringBuilder(
"literals within decimal numbers cannot contain digits, e/E, dot, and"
+ " group separator: '");
errMsg.append(fmt, 0, fmtLen).append('\'');
m_errorMsg = errMsg.toString();
return null;
}
}
}
}
}
case TIME:
case DATE:
case TZONE:
case TS_NTZ:
case TS_TZ:
m_model = modelSelector;
break;
default:
errMsg = new StringBuilder("format string includes incompatible elements: '");
errMsg.append(fmt, 0, fmtLen).append('\'');
m_errorMsg = errMsg.toString();
return null;
}
return fmtStr.substring(fmtLen); // truncated format string...
}
;
/**
* Reconstruct the format string (prints the canonical form)
*
* @return format string
*/
public String reconstructFormat() {
StringBuilder out = new StringBuilder();
for (Fragment it : m_frags) {
if (it.m_elem != Keyword.LITERAL) {
if (it.m_elem.caseSens && it.m_case != 0) {
if ((it.m_case & 1) != 0) out.append(Character.toLowerCase(it.m_elem.str.charAt(0)));
else out.append(it.m_elem.str.charAt(0));
if (it.m_elem.str.length() > 1) {
if ((it.m_case & 2) != 0) out.append(it.m_elem.str.substring(1).toLowerCase());
else out.append(it.m_elem.str.substring(1));
}
} else out.append(it.m_elem.str);
// Check for parameters
if (it.m_param != 0) out.append(String.valueOf(it.m_param));
} else // LITERAL
{
// Check if the literal need not be quoted
StringCharacterIterator ci = new StringCharacterIterator(it.m_literal);
boolean needToQuote = false;
for (char c = ci.first(); c != CharacterIterator.DONE; c = ci.next()) {
switch (c) {
case '-':
case '/':
case ',':
case '.':
case ';':
case ':':
case ' ':
case '(':
case ')':
continue;
default:
needToQuote = true;
break;
}
break;
}
if (!needToQuote) out.append(it.m_literal);
else {
out.append('"');
out.append(it.m_literal.replace("\"", "\"\""));
out.append('"');
}
}
}
return out.toString();
}
/**
* Check that this format can be used for printing with model
*
* @param model model id
* @return true if printing otherwise false
*/
public boolean checkPrintModel(int model) {
return m_model == ANY || ((m_model & model) != 0 && (m_model & ~model) == 0);
}
/**
* Print the decomposed timezone value using this format
*
* @param val TmExt value
* @return a timezone value
*/
public String printTm(TmExt val) {
assert (m_model & ~TS_TZ) == 0 || m_model == ANY;
boolean fill = true;
StringBuilder out = new StringBuilder();
for (Fragment it : m_frags) {
int i;
String s;
switch (it.m_elem) {
default:
assert false;
case LITERAL:
out.append(it.m_literal);
break;
case OPTSP:
break;
//
// Modifiers
//
case FM: // fill mode switch
fill = !fill;
break;
case FX: // exact mode switch (no effect on printing)
break;
//
// Date components
//
case D: // 1 digit day of the week (1-7) 1=MON (ISO-8601)
i = val.tm_wday;
assert 0 <= i && i < TmExt.DAYS_IN_WEEK;
i = 1 + (i + TmExt.DAYS_IN_WEEK - 1) % TmExt.DAYS_IN_WEEK;
out.append(Character.forDigit(i, 10));
break;
case DAY: // full name of the day of the week
i = val.tm_wday;
assert 0 <= i && i < TmExt.DAYS_IN_WEEK;
s = s_dayNames[i];
if (it.m_case == 0) out.append(s);
else {
if ((it.m_case & 1) != 0) out.append(Character.toLowerCase(s.charAt(0)));
else out.append(s.charAt(0));
if ((it.m_case & 2) != 0) out.append(s.substring(1).toLowerCase());
else out.append(s.substring(1));
}
// Fill mode?
if (fill && (i = s_maxDayNameLen - s.length()) > 0) out.append(s_spaces, 0, i);
break;
case DD: // 2 digit day of the month (1-31)
i = val.tm_mday;
assert 0 < i && i <= TmExt.MAX_DAYS_IN_MONTH;
if (i >= 10 || fill) out.append(Character.forDigit(i / 10, 10));
out.append(Character.forDigit(i % 10, 10));
break;
case DDD: // 3-digit day of the year (1-366)
i = val.tm_yday + 1;
assert 1 <= i && i <= TmExt.MAX_DAYS_IN_YEAR;
if (fill) {
out.append(Character.forDigit(i / 100, 10));
out.append(Character.forDigit((i / 10) % 10, 10));
} else {
if (i >= 100) out.append(Character.forDigit(i / 100, 10));
if (i >= 10) out.append(Character.forDigit((i / 10) % 10, 10));
}
out.append(Character.forDigit((i % 10), 10));
break;
case DY: // 3-letter abbreviated day name
i = val.tm_wday;
assert 0 <= i && i < TmExt.DAYS_IN_WEEK;
s = s_dayNames[i];
if ((it.m_case & 1) != 0) out.append(Character.toLowerCase(s.charAt(0)));
else out.append(s.charAt(0));
if ((it.m_case & 2) != 0) {
out.append(Character.toLowerCase(s.charAt(1)));
out.append(Character.toLowerCase(s.charAt(2)));
} else {
out.append(s.charAt(1));
out.append(s.charAt(2));
}
break;
case MM: // 2 digit month (1-12)
i = val.tm_mon + 1;
assert 0 < i && i <= TmExt.MONTHS_IN_YEAR;
if (i >= 10 || fill) out.append(Character.forDigit(i / 10, 10));
out.append(Character.forDigit(i % 10, 10));
break;
case MON: // 3-letter month name abbreviation
i = val.tm_mon;
assert 0 <= i && i < TmExt.MONTHS_IN_YEAR;
s = s_monthNames[i];
if ((it.m_case & 1) != 0) out.append(Character.toLowerCase(s.charAt(0)));
else out.append(s.charAt(0));
if ((it.m_case & 2) != 0) {
out.append(Character.toLowerCase(s.charAt(1)));
out.append(Character.toLowerCase(s.charAt(2)));
} else {
out.append(s.charAt(1));
out.append(s.charAt(2));
}
break;
case MONTH: // full name of the month
i = val.tm_mon;
assert 0 <= i && i < TmExt.MONTHS_IN_YEAR;
s = s_monthNames[i];
if (it.m_case == 0) out.append(s);
else {
if ((it.m_case & 1) != 0) out.append(Character.toLowerCase(s.charAt(0)));
else out.append(s.charAt(0));
if ((it.m_case & 2) != 0) out.append(s.substring(1).toLowerCase());
else out.append(s.substring(1));
}
// Fill mode?
if (fill && (i = s_maxMonthNameLen - s.length()) > 0) out.append(s_spaces, 0, i);
break;
case AD: // 2-letter AD/BC indicator
case BC:
i = val.tm_year + 1900;
if (i <= 0) {
out.append((it.m_case & 1) == 0 ? 'B' : 'b');
out.append((it.m_case & 2) == 0 ? 'C' : 'c');
} else {
out.append((it.m_case & 1) == 0 ? 'A' : 'a');
out.append((it.m_case & 2) == 0 ? 'D' : 'd');
}
break;
case CE: // 3-letter CE/BCE indicator
case BCE:
i = val.tm_year + 1900;
if (i <= 0) {
out.append((it.m_case & 1) == 0 ? 'B' : 'b');
out.append((it.m_case & 2) == 0 ? 'C' : 'c');
out.append((it.m_case & 2) == 0 ? 'E' : 'e');
} else {
out.append((it.m_case & 1) == 0 ? 'C' : 'c');
out.append((it.m_case & 2) == 0 ? 'E' : 'e');
if (fill) out.append(' ');
}
break;
case YY: // 2-digit year
i = val.tm_year + 1900;
if (i <= 0) i = 1 - i; // year 0 = year 1 BC
i %= 100;
out.append(Character.forDigit(i / 10, 10));
out.append(Character.forDigit(i % 10, 10));
break;
case SYYYY: // ISO 8601 signed year (+0000 = 1BC, -0001 = 2BC)
case YYYY: // 4-digit year (never zero)
i = val.tm_year + 1900;
if (it.m_elem == Keyword.YYYY) {
if (i <= 0) {
if (!m_seen.contains(Keyword.AD)
&& !m_seen.contains(Keyword.BC)
&& !m_seen.contains(Keyword.BCE)
&& !m_seen.contains(Keyword.CE)) i = TmExt.MAX_YEAR + 1; // print overflow
else i = 1 - i; // year 0 = year 1 BC
}
} else if (i < 0) {
i = -i;
out.append('-');
} else out.append('+');
if (i > TmExt.MAX_YEAR) {
out.append("####");
break;
}
if (fill) {
out.append(Character.forDigit(i / 1000, 10));
out.append(Character.forDigit((i / 100) % 10, 10));
out.append(Character.forDigit((i / 10) % 10, 10));
} else {
if (i >= 1000) out.append(Character.forDigit(i / 1000, 10));
if (i >= 100) out.append(Character.forDigit((i / 100) % 10, 10));
if (i >= 10) out.append(Character.forDigit((i / 10) % 10, 10));
}
out.append(Character.forDigit((i % 10), 10));
break;
//
// Time components
//
case AM: // 2-letter meridian indicator
case PM:
i = val.tm_hour;
assert 0 <= i && i < TmExt.HOURS_IN_DAY;
if (i < 12) out.append((it.m_case & 1) == 0 ? 'A' : 'a');
else out.append((it.m_case & 1) == 0 ? 'P' : 'p');
out.append((it.m_case & 2) == 0 ? 'M' : 'm');
break;
case HH: // 2-digit hour (0-23)
case HH24:
i = val.tm_hour;
assert 0 <= i && i < TmExt.HOURS_IN_DAY;
if (i >= 10 || fill) out.append(Character.forDigit(i / 10, 10));
out.append(Character.forDigit(i % 10, 10));
break;
case HH12: // 2-digit hour (0-12)
i = val.tm_hour;
assert 0 <= i && i < TmExt.HOURS_IN_DAY;
i %= 12;
if (i == 0) i = 12;
if (i >= 10 || fill) out.append(Character.forDigit(i / 10, 10));
out.append(Character.forDigit(i % 10, 10));
break;
case MI: // 2-digit minute (0-59)
i = val.tm_min;
assert 0 <= i && i < TmExt.MINUTES_IN_HOUR;
if (i >= 10 || fill) out.append(Character.forDigit(i / 10, 10));
out.append(Character.forDigit(i % 10, 10));
break;
case SS: // 2-digit second (0-59)
i = val.tm_sec;
assert 0 <= i && i < TmExt.SECONDS_IN_MINUTE;
if (i >= 10 || fill) out.append(Character.forDigit(i / 10, 10));
out.append(Character.forDigit(i % 10, 10));
break;
case ES: // Epoch seconds
case ESA: // Epoch seconds (auto-scaling)
int escale = (it.m_elem == Keyword.ESA) ? val.tm_sec_scale : it.m_param;
long es = val.tm_epochSec;
assert 0 <= escale && escale <= TmExt.MAX_SCALE;
if (escale != 0) {
if (escale > 6) // need to use SB16 arithmetic
{
// rescale and add nanoseconds
BigInteger li = BigInteger.valueOf(es).multiply(Power10.sb16Table[escale]);
li =
li.add(
BigInteger.valueOf(val.tm_nsec / Power10.intTable[TmExt.MAX_SCALE - escale]));
out.append(li.toString());
break;
}
// rescale and add nanoseconds
es *= Power10.intTable[escale];
es += val.tm_nsec / Power10.intTable[TmExt.MAX_SCALE - escale];
}
out.append(String.valueOf(es));
break;
case FF: // fractional seconds
i = val.tm_nsec;
assert 0 <= i && i < TmExt.FRAC_SECONDS;
assert 0 <= val.tm_sec_scale && val.tm_sec_scale <= TmExt.MAX_SCALE;
{
int scale = (it.m_param != 0) ? it.m_param : val.tm_sec_scale;
if (scale < TmExt.MAX_SCALE) i /= Power10.intTable[TmExt.MAX_SCALE - scale];
while (scale-- > 0)
out.append(Character.forDigit((i / Power10.intTable[scale]) % 10, 10));
}
break;
//
// Timezone components
//
case TZD: // Daylight time indicator (up to 5 letters)
s = val.tm_zone;
if (s == null) s = "GMT";
i = s.length();
assert 0 < i && i <= TmExt.MAX_ZONE_LEN;
out.append(s);
if (fill) out.append(s_spaces, 0, TmExt.MAX_ZONE_LEN - i);
break;
case TZH: // Time zone offset from GMT (hours)
i = val.tm_gmtoff / TmExt.SECONDS_IN_HOUR;
assert -TmExt.HOURS_IN_DAY < i && i < TmExt.HOURS_IN_DAY;
if (i < 0) {
i = -i;
out.append('-');
} else out.append('+');
if (i >= 10 || fill) out.append(Character.forDigit(i / 10, 10));
out.append(Character.forDigit(i % 10, 10));
break;
case TZHTZM: // Time zone offset from GMT (hours + minutes)
i = val.tm_gmtoff / TmExt.SECONDS_IN_MINUTE;
assert -TmExt.MINUTES_IN_DAY < i && i < TmExt.MINUTES_IN_DAY;
if (i < 0) {
i = -i;
out.append('-');
} else out.append('+');
{
int c = i / TmExt.MINUTES_IN_HOUR;
if (c >= 10 || fill) out.append(Character.forDigit(c / 10, 10));
out.append(Character.forDigit(c % 10, 10));
c = i % TmExt.MINUTES_IN_HOUR;
out.append(Character.forDigit(c / 10, 10));
out.append(Character.forDigit(c % 10, 10));
}
break;
case TZIDX: // Time zone index (minutes offset + 1440)
i = val.tm_gmtoff / TmExt.SECONDS_IN_MINUTE + 1440;
if (i < 0 || i > 2880) i = 1440;
out.append(String.valueOf(i));
break;
case TZISO: // ISO-8601 time zone offset: Z or TZH:TZM
i = val.tm_gmtoff;
assert -TmExt.SECONDS_IN_DAY < i && i < TmExt.SECONDS_IN_DAY;
if (i == 0) {
out.append('Z');
if (fill) out.append(" "); // 5 spaces
break;
}
if (i < 0) {
i = -i;
out.append('-');
} else out.append('+');
i /= TmExt.SECONDS_IN_MINUTE;
out.append(Character.forDigit(i / (10 * TmExt.MINUTES_IN_HOUR), 10));
out.append(Character.forDigit((i / TmExt.MINUTES_IN_HOUR) % 10, 10));
i %= TmExt.MINUTES_IN_HOUR;
if (i != 0 || fill) {
out.append(':');
out.append(Character.forDigit(i / 10, 10));
out.append(Character.forDigit(i % 10, 10));
}
break;
case TZM: // Time zone offset from GMT (minutes)
i = val.tm_gmtoff;
if (i < 0) i = -i;
i /= TmExt.SECONDS_IN_MINUTE;
i %= TmExt.MINUTES_IN_HOUR;
out.append(Character.forDigit(i / 10, 10));
out.append(Character.forDigit(i % 10, 10));
break;
// case TZR: // Time zone region
}
}
return out.toString();
}
/**
* Print SfTimestamp value with scale
*
* @param ts the timestamp
* @param scale scale for fractional seconds
* @param tz the timezone to change the timestamp to (null to use timezone from ts)
* @return formatted text
*/
public String printTimestamp(SFTimestamp ts, int scale, TimeZone tz) {
assert checkPrintModel(TS_TZ);
TmExt tm = new TmExt();
tm.setTimestamp(ts, scale, tz);
return printTm(tm);
}
/**
* Print SfTimestamp value with scale
*
* @param ts the timestamp
* @param scale scale for fractional seconds
* @return formatted text
*/
public String printTimestamp(SFTimestamp ts, int scale) {
return printTimestamp(ts, scale, null);
}
/**
* Print SfTime value with scale
*
* @param t SFTime instance
* @param scale scale
* @return a SfTime string
*/
public String printTime(SFTime t, int scale) {
assert checkPrintModel(TIME);
TmExt tm = new TmExt();
tm.setTime(t, scale);
return printTm(tm);
}
/**
* Print SfDate value
*
* @param d SFDate instance
* @return a SfDate string
*/
public String printDate(SFDate d) {
assert checkPrintModel(DATE);
TmExt tm = new TmExt();
tm.setDate(d);
return printTm(tm);
}
/**
* Parse a number up to 2 digits
*
* @param str the string iterator... left positioned after the last char
* @param fill true if this requires matching exact number of positions
* @return the unsigned number value or -1 on an error
*/
private static int parseNum2(CharacterIterator str, boolean fill) {
int n1 = Character.digit(str.current(), 10);
if (n1 < 0) return -1;
int n2 = Character.digit(str.next(), 10);
if (n2 < 0) return fill ? -1 : n1;
str.next();
return n1 * 10 + n2;
}
/**
* Parse a number up to 3 digits
*
* @param str the string iterator... left positioned after the last char
* @param fill true if this requires matching exact number of positions
* @return the unsigned number value or -1 on an error
*/
private static int parseNum3(CharacterIterator str, boolean fill) {
int n1 = Character.digit(str.current(), 10);
if (n1 < 0) return -1;
int n2 = Character.digit(str.next(), 10);
if (n2 < 0) return fill ? -1 : n1;
n1 = n1 * 10 + n2;
n2 = Character.digit(str.next(), 10);
if (n2 < 0) return fill ? -1 : n1;
str.next();
return n1 * 10 + n2;
}
/**
* Parse a number up to 4 digits
*
* @param str the string iterator... left positioned after the last char
* @param fill true if this requires matching exact number of positions
* @return the unsigned number value or -1 on an error
*/
private static int parseNum4(CharacterIterator str, boolean fill) {
int n1 = Character.digit(str.current(), 10);
if (n1 < 0) return -1;
int n2 = Character.digit(str.next(), 10);
if (n2 < 0) return fill ? -1 : n1;
n1 = n1 * 10 + n2;
n2 = Character.digit(str.next(), 10);
if (n2 < 0) return fill ? -1 : n1;
n1 = n1 * 10 + n2;
n2 = Character.digit(str.next(), 10);
if (n2 < 0) return fill ? -1 : n1;
str.next();
return n1 * 10 + n2;
}
/**
* Check that character iterator str has string pref as its prefix, ignoring case
*
* @param str the string iterator, positioned after prefix on match
* @param pref the prefix string (ALWAYS upper-case)
* @param len the length of prefix
* @return true if pref is the prefix for str
*/
private static boolean isPrefix(CharacterIterator str, String pref, int len) {
CharacterIterator pi = new StringCharacterIterator(pref);
int idx = str.getIndex();
char c = str.current();
char pc = pi.current();
for (int i = 0; i < len; i++) {
if (Character.toUpperCase(c) != pc) {
str.setIndex(idx);
return false;
}
c = str.next();
pc = pi.next();
}
return true;
}
/**
* Parse 3-letter week day name
*
* @param str the string iterator
* @return the week dat number (0 for Sunday) or -1 on error
*/
private static int parseShortWeekDay(CharacterIterator str) {
for (int n = 0; n < TmExt.DAYS_IN_WEEK; n++) {
if (isPrefix(str, s_dayNames[n], 3)) return n;
}
return -1;
}
/**
* Parse full week day name
*
* @param str the string iterator
* @param fill check that there's enough spaces for exact match in fill mode
* @return the week day number (0 for Sunday) or -1 on error
*/
private static int parseWeekDay(CharacterIterator str, boolean fill) {
for (int n = 0; n < TmExt.DAYS_IN_WEEK; n++) {
int l = s_dayNames[n].length();
if (isPrefix(str, s_dayNames[n], l)) {
if (fill) {
char c = str.current();
while (l < s_maxDayNameLen) {
if (c != ' ') return -1;
c = str.next();
}
}
return n;
}
}
return -1;
}
/**
* Parse 3-letter month name
*
* @param str the string iterator
* @return the month number (0 for January) or -1 on error
*/
private static int parseShortMonth(CharacterIterator str) {
for (int n = 0; n < TmExt.MONTHS_IN_YEAR; n++) {
if (isPrefix(str, s_monthNames[n], 3)) return n;
}
return -1;
}
/**
* Parse full week day name
*
* @param str the string iterator
* @param fill check that there's enough spaces for exact match in fill mode
* @return the month number (0 for January) or -1 on error
*/
private static int parseMonth(CharacterIterator str, boolean fill) {
for (int n = 0; n < TmExt.MONTHS_IN_YEAR; n++) {
int l = s_monthNames[n].length();
if (isPrefix(str, s_monthNames[n], l)) {
if (fill) {
char c = str.current();
while (l < s_maxMonthNameLen) {
if (c != ' ') return -1;
c = str.next();
++l;
}
}
return n;
}
}
return -1;
}
/**
* Parse DATE/TIME string
*
* @param inStr the input string
* @param cenBound the century boundary for YY (1970-2100)
* @return the resulting structure TmExt, or null in case of error
*/
public TmExt parseTm(String inStr, int cenBound) {
if (m_model != DATE && m_model != TIME && m_model != TS_NTZ && m_model != TS_TZ) return null;
TmExt val = new TmExt();
val.tm_isdst = -1; // No daylight savings info provided
CharacterIterator str = new StringCharacterIterator(inStr);
char c; // the current character
// Skip leading whitespace if we start in lax mode
boolean spacesIgnored = false;
if (m_frags.isEmpty() || m_frags.get(0).m_elem != Keyword.FX) {
c = str.current();
while (c != CharacterIterator.DONE && Character.isWhitespace(c)) {
c = str.next();
spacesIgnored = true;
}
}
// Mode switches
boolean fill = true;
boolean exact = false;
//
// Parse the input... fragment by fragment
//
boolean bce = false;
boolean pm = false;
for (Fragment it : m_frags) {
boolean ignoreSpaces = false;
int n;
switch (it.m_elem) {
default:
assert false;
//
// Ignore spaces when in lax mode
//
case OPTSP:
if (exact) continue;
ignoreSpaces = true;
break;
//
// Match the literal
//
case LITERAL:
{
CharacterIterator fs = new StringCharacterIterator(it.m_literal);
char fc = fs.current();
c = str.current();
if (exact) {
if (spacesIgnored) {
while (fc == ' ') fc = fs.next();
}
while (fc != CharacterIterator.DONE) {
if (c == CharacterIterator.DONE
|| Character.toUpperCase(c) != Character.toUpperCase(fc)) return null;
c = str.next();
fc = fs.next();
}
} else {
while (fc != CharacterIterator.DONE) {
if (fc == ' ') {
if (!spacesIgnored) {
spacesIgnored = true;
if (c != ' ') return null;
do {
c = str.next();
} while (c == ' ');
}
fc = fs.next();
continue;
}
if (c == CharacterIterator.DONE
|| Character.toUpperCase(c) != Character.toUpperCase(fc)) return null;
c = str.next();
fc = fs.next();
spacesIgnored = false;
}
}
}
break;
//
// Modifiers
//
case FM: // fill mode switch
fill = !fill;
continue;
case FX: // exact mode switch
exact = !exact;
continue;
//
// Date components
//
case AD: // 2-letter AD/BC indicator
case BC:
c = str.current();
if (c == 'A' || c == 'a') bce = false;
else if (c == 'B' || c == 'b') bce = true;
else return null;
c = str.next();
if (c == 'D' || c == 'd') {
if (bce) return null;
} else if (c == 'C' || c == 'c') {
if (!bce) return null;
} else return null;
str.next();
break;
case CE: // 3-letter CE/BCE indicator
case BCE:
c = str.current();
bce = false;
if (c == 'B' || c == 'b') {
bce = true;
c = str.next();
}
if (c != 'C' && c != 'c') return null;
c = str.next();
if (c != 'E' && c != 'e') return null;
c = str.next();
if (!exact) {
ignoreSpaces = true;
break;
}
// Consume space
if (fill && !bce) {
if (c != ' ') return null;
str.next();
}
break;
case D: // 1 digit day of the week (1-7) 1=MON (ISO-8601)
n = Character.digit(str.current(), 10);
if (n < 1 || n > 7) return null;
str.next();
val.tm_wday = n % 7;
break;
case DAY: // full name of the day of the week
val.tm_wday = parseWeekDay(str, exact && fill);
if (val.tm_wday < 0) return null;
if (!exact) ignoreSpaces = true;
break;
case DD:
val.tm_mday = parseNum2(str, exact && fill);
if (val.tm_mday < 1 || val.tm_mday > 31) return null;
break;
case DDD:
val.tm_yday = parseNum3(str, exact && fill);
if (val.tm_yday < 1 || val.tm_yday > 366) return null;
break;
case DY: // abbreviated name of the day of the week
val.tm_wday = parseShortWeekDay(str);
if (val.tm_wday < 0) return null;
break;
case MM: // 2 digit month (1-12)
val.tm_mon = parseNum2(str, exact && fill);
if (val.tm_mon < 1 || val.tm_mon > 12) return null;
val.tm_mon--;
break;
case MON: // 3-letter month name abbreviation
val.tm_mon = parseShortMonth(str);
if (val.tm_mon < 0) return null;
break;
case MONTH: // full name of the month
val.tm_mon = parseMonth(str, exact && fill);
if (val.tm_mon < 0) return null;
if (!exact) ignoreSpaces = true;
break;
case YY: // 2-digit year
val.tm_year = parseNum2(str, true);
if (val.tm_year < 0) return null;
if (val.tm_year < (cenBound % 100)) val.tm_year += 100;
val.tm_year += (cenBound / 100) * 100;
break;
case SYYYY: // ISO 8601 signed year
c = str.current();
if (c != '+' && c != '-') return null;
str.next();
val.tm_year = parseNum4(str, exact && fill);
if (val.tm_year < 0) return null;
if (c == '-') {
if (val.tm_year >= 9999) // this is to avoid overflow with BC YYYY
return null;
val.tm_year *= -1;
}
break;
case YYYY: // 4-digit year (never zero)
val.tm_year = parseNum4(str, exact && fill);
if (val.tm_year <= 0) return null;
break;
//
// Time components
//
case AM: // 2-letter meridian indicator
case PM:
c = str.current();
if (c == 'A' || c == 'a') pm = false;
else if (c == 'P' || c == 'p') pm = true;
else return null;
c = str.next();
if (c != 'M' && c != 'm') return null;
str.next();
break;
case HH: // 2-digit hour (0-23)
case HH24:
val.tm_hour = parseNum2(str, exact && fill);
if (val.tm_hour < 0 || val.tm_hour >= TmExt.HOURS_IN_DAY) return null;
break;
case HH12: // 2-digit hour (1-12)
val.tm_hour = parseNum2(str, exact && fill);
if (val.tm_hour < 1 || val.tm_hour > 12) return null;
break;
case MI: // 2-digit minute (0-59)
val.tm_min = parseNum2(str, exact && fill);
if (val.tm_min < 0 || val.tm_min >= TmExt.MINUTES_IN_HOUR) return null;
break;
case SS: // 2-digit second (0-59)
val.tm_sec = parseNum2(str, exact && fill);
if (val.tm_sec < 0 || val.tm_sec >= TmExt.SECONDS_IN_MINUTE) return null;
break;
case ES: // Epoch seconds
case ESA:
{
int d = 0;
boolean neg = false;
c = str.current();
if (c == '-' || c == '+') {
neg = (c == '-');
c = str.next();
}
BigInteger es = BigInteger.ZERO;
while ((n = Character.digit(c, 10)) >= 0) {
es = es.multiply(BigInteger.TEN).add(BigInteger.valueOf(n));
d++;
c = str.next();
}
if (d > 38) return null;
int scale = it.m_param;
if (it.m_elem == Keyword.ESA) {
scale = 0;
BigInteger limit = BigInteger.valueOf(TmExt.EPOCH_AUTO_LIMIT);
while (es.compareTo(limit) > 0) {
scale += 3;
limit = limit.multiply(BigInteger.valueOf(1000));
}
}
if (scale > TmExt.MAX_SCALE) return null;
if (scale > 0) {
BigInteger rem = es;
BigInteger q = Power10.sb16Table[scale];
if (neg) {
rem = es.negate();
es = rem.subtract(q).add(BigInteger.ONE);
}
es = es.divide(q);
rem = rem.subtract(es.multiply(q));
val.tm_nsec = rem.intValue() * Power10.intTable[TmExt.MAX_SCALE - scale];
} else if (neg) es = es.negate();
// check for the epoch range
if (es.compareTo(BigInteger.valueOf(TmExt.EPOCH_START)) < 0
|| es.compareTo(BigInteger.valueOf(TmExt.EPOCH_END)) > 0) return null;
val.tm_epochSec = es.longValue();
val.tm_has_epochSec = true;
val.tm_sec_scale = scale;
}
break;
case FF: // fractional seconds
c = str.current();
assert it.m_param <= TmExt.MAX_SCALE;
if (exact && it.m_param != 0) {
int fsec = 0;
for (int d = 0; d < it.m_param; d++) {
n = Character.digit(c, 10);
if (n < 0) return null;
fsec = fsec * 10 + n;
c = str.next();
}
val.tm_sec_scale = it.m_param;
val.tm_nsec = fsec * Power10.intTable[TmExt.MAX_SCALE - it.m_param];
} else {
int fsec = 0;
int d = 0;
while ((n = Character.digit(c, 10)) >= 0) {
fsec = fsec * 10 + n;
d++;
c = str.next();
}
if (d > TmExt.MAX_SCALE) return null;
val.tm_sec_scale = d;
val.tm_nsec = fsec * Power10.intTable[TmExt.MAX_SCALE - d];
}
break;
//
// Timezone components
//
case TZD: // Daylight time indicator (up to 5 letters)
{
StringBuilder tzd = new StringBuilder();
c = str.current();
n = 0;
while (('A' <= c && c <= 'Z') || ('a' <= c && c <= 'z')) {
tzd.append(Character.toUpperCase(c));
c = str.next();
if (++n >= TmExt.MAX_ZONE_LEN) break;
}
if (n == 0) return null;
val.tm_zone = tzd.toString();
if (!exact) {
ignoreSpaces = true;
break;
}
// In exact mode must match the padding spaces
if (fill) {
n = TmExt.MAX_ZONE_LEN - n;
while (n > 0) {
if (c != ' ') return null;
c = str.next();
n--;
}
}
}
break;
case TZH: // Time zone offset from GMT (hours)
c = str.current();
if (c != '+' && c != '-') return null;
str.next();
n = parseNum2(str, exact && fill);
if (n < 0 || n > 23) return null;
val.tm_gmtoff += n * TmExt.SECONDS_IN_HOUR;
if (c == '-') val.tm_gmtoff *= -1;
val.tm_has_offset = true;
break;
case TZHTZM: // 4-digit time zone offset (TZH + TZM)
c = str.current();
if (c != '+' && c != '-') return null;
str.next();
n = parseNum4(str, exact && fill);
if (n < 0 || (n / 100) >= TmExt.HOURS_IN_DAY || (n % 100) >= TmExt.MINUTES_IN_HOUR)
return null;
val.tm_gmtoff = (n % 100) * TmExt.SECONDS_IN_MINUTE;
val.tm_gmtoff += (n / 100) * TmExt.SECONDS_IN_HOUR;
if (c == '-') val.tm_gmtoff *= -1;
val.tm_has_offset = true;
break;
case TZIDX: // Internal TZ index (tz offset in minutes + 1440)
n = parseNum4(str, false);
if (n < 0) return null;
if (n > 2880) n = 1440; // sanity
val.tm_gmtoff = (n - 1440) * TmExt.SECONDS_IN_MINUTE;
val.tm_has_offset = true;
val.tm_has_tzidx = true; // indicate that we got this kludge
break;
case TZISO: // ISO-8601 time zone offset: Z, TZH, TZHTZM, or TZH:TZM
c = str.current();
if (c == 'Z' || c == 'z') {
str.next();
val.tm_gmtoff = 0;
n = 5;
} else {
if (c != '+' && c != '-') return null;
str.next();
n = parseNum2(str, true);
if (n < 0 || n >= TmExt.HOURS_IN_DAY) return null;
val.tm_gmtoff = n * TmExt.SECONDS_IN_HOUR;
n = 2;
char sc = str.current();
if (sc == ':' || Character.isDigit(sc)) {
n = 1;
if (sc == ':') {
n = 0;
str.next();
}
int m = parseNum2(str, true);
if (m < 0 || m >= TmExt.MINUTES_IN_HOUR) return null;
val.tm_gmtoff += m * TmExt.SECONDS_IN_MINUTE;
}
if (c == '-') val.tm_gmtoff *= -1;
}
val.tm_has_offset = true;
if (!exact) {
ignoreSpaces = true;
break;
}
// In exact mode must match the padding spaces
if (fill) {
c = str.current();
while (n-- > 0) {
if (c != ' ') return null;
c = str.next();
}
}
break;
case TZM: // Time zone offset from GMT (minutes)
n = parseNum2(str, exact && fill);
if (n < 0 || n > 59) return null;
if (val.tm_gmtoff < 0) n = -n;
val.tm_gmtoff += n * TmExt.SECONDS_IN_MINUTE;
break;
}
if (ignoreSpaces) {
c = str.current();
while (c == ' ') {
spacesIgnored = true;
c = str.next();
}
} else spacesIgnored = false;
}
//
// If we got here, check that the rest of the string is whitespace (lax mode)
// or that we're at the end (strict mode)
//
if (exact) {
if (str.current() != CharacterIterator.DONE) return null;
} else {
c = str.current();
while (c != CharacterIterator.DONE) {
if (!Character.isWhitespace(c)) return null;
c = str.next();
}
}
// Final adjustments to hour
if (m_seen.contains(Keyword.HH12)) {
if (val.tm_hour == 12) val.tm_hour = 0;
if (pm) val.tm_hour += 12;
}
// Final adjustments to year
if (bce && val.tm_year > 0) {
val.tm_year *= -1;
val.tm_year++; // 1BC is year 0
}
val.tm_year -= 1900; // unix tm bogosity
return val;
}
/**
* Parses extended Timestamp string
*
* @param inStr a string
* @return TmExt instance
*/
public TmExt parseTm(String inStr) {
return parseTm(inStr, DEFAULT_CENTURY_BOUNDARY);
}
/**
* Parse date using this format (returns SFDate)
*
* @param str The string to parse
* @param cenBound the century boundary for YY (1970-2100)
* @return SFDate object or null on an error
*/
public SFDate parseDate(String str, int cenBound) {
TmExt tm = parseTm(str, cenBound);
if (tm == null) return null;
return tm.getDate();
}
/**
* Parses Data
*
* @param str a string
* @return SFDate instance
*/
public SFDate parseDate(String str) {
return parseDate(str, DEFAULT_CENTURY_BOUNDARY);
}
/**
* Parse time of day using this format (returns SFTime)
*
* @param str The string to parse
* @return SFTime object or null on an error
*/
public SFTime parseTime(String str) {
TmExt tm = parseTm(str, DEFAULT_CENTURY_BOUNDARY);
if (tm == null) return null;
return tm.getTime();
}
/**
* Parse timestamp using this format (returns SFTimestamp)
*
* @param str The string to parse
* @param tz the timezone to use by default
* @param cenBound the century boundary for YY (1970-2100)
* @return SFTimestamp object or null on an error
*/
public SFTimestamp parseTimestamp(String str, TimeZone tz, int cenBound) {
TmExt tm = parseTm(str, cenBound);
if (tm == null) return null;
return tm.getTimestamp(tz);
}
/**
* Parses Timestamp string
*
* @param str a string
* @param tz timezone
* @return SFTimestamp instance
*/
public SFTimestamp parseTimestamp(String str, TimeZone tz) {
return parseTimestamp(str, tz, DEFAULT_CENTURY_BOUNDARY);
}
/** Helper function used to check if time can be parsed with this format */
private boolean canScanTime() {
// Got minutes info?
if (!m_seen.contains(Keyword.MI)) return false;
// Got hours info
if (m_seen.contains(Keyword.HH) || m_seen.contains(Keyword.HH24)) return true;
if (!m_seen.contains(Keyword.HH12)) return false;
return m_seen.contains(Keyword.AM) || m_seen.contains(Keyword.PM);
}
/** Helper function used to check that if we have any time element, then we can scan time */
private boolean canScanOptionalTime() {
if (m_seen.contains(Keyword.HH)
|| m_seen.contains(Keyword.HH12)
|| m_seen.contains(Keyword.HH24)
|| m_seen.contains(Keyword.AM)
|| m_seen.contains(Keyword.PM)
|| m_seen.contains(Keyword.MI)
|| m_seen.contains(Keyword.SS)) return canScanTime();
if (m_seen.contains(Keyword.FF) && !m_seen.contains(Keyword.ES)) return false;
return true;
}
/** Helper function used to check that we have data elememts needed to scan date */
private boolean canScanDate() {
// at scan time ES is not compatible with any date/time components
// timezone components are OK
if (m_seen.contains(Keyword.ES) || m_seen.contains(Keyword.ESA)) {
return !(m_seen.contains(Keyword.YY)
|| m_seen.contains(Keyword.YYYY)
|| m_seen.contains(Keyword.SYYYY)
|| m_seen.contains(Keyword.BC)
|| m_seen.contains(Keyword.AD)
|| m_seen.contains(Keyword.BCE)
|| m_seen.contains(Keyword.CE)
|| m_seen.contains(Keyword.MONTH)
|| m_seen.contains(Keyword.MON)
|| m_seen.contains(Keyword.MM)
|| m_seen.contains(Keyword.DAY)
|| m_seen.contains(Keyword.DY)
|| m_seen.contains(Keyword.DD)
|| m_seen.contains(Keyword.DDD)
|| m_seen.contains(Keyword.HH)
|| m_seen.contains(Keyword.HH24)
|| m_seen.contains(Keyword.HH12)
|| m_seen.contains(Keyword.AM)
|| m_seen.contains(Keyword.PM)
|| m_seen.contains(Keyword.MI)
|| m_seen.contains(Keyword.SS));
}
// no ES... need at least year, month, and day
return (m_seen.contains(Keyword.YY)
|| m_seen.contains(Keyword.YYYY)
|| m_seen.contains(Keyword.SYYYY))
&& (m_seen.contains(Keyword.MONTH)
|| m_seen.contains(Keyword.MON)
|| m_seen.contains(Keyword.MM))
&& m_seen.contains(Keyword.DD);
}
/**
* Check that this format can be used for parsing with model
*
* @param model model id
* @return true if the format can be used otherwise false
*/
public boolean checkScanModel(int model) {
switch (model) {
case NUMERIC:
return m_model == NUMERIC;
case TIME:
return m_model == TIME && canScanTime();
case DATE:
return m_model == DATE && canScanDate();
case TS_TZ:
case TS_NTZ:
return (m_model & DATE) != 0
&& (m_model & ~TS_TZ) == 0
&& canScanDate()
&& canScanOptionalTime();
default:
return false;
}
}
// ----------------------PRIVATE----------------------------------
// Supported format keywords
// NB: MUST MATCH SF_SQL_FORMAT_ELEMENTS in SqlFormat.hpp
private static enum Keyword {
// id case str maxL mode repeat maxParam
DLR(false, "$", 1, NUMERIC, false),
GROUP(false, ",", 1, NUMERIC, true),
DOT(false, ".", 1, NUMERIC, false),
ZERO(false, "0", 1, NUMERIC, true),
DIGIT(false, "9", 1, NUMERIC, true),
AD(true, "AD", 2, DATE, false),
AM(true, "AM", 2, TIME, false),
B(false, "B", 0, NUMERIC, false),
BC(true, "BC", 2, DATE, false),
BCE(true, "BCE", 3, DATE, false),
CE(true, "CE", 3, DATE, false),
D(false, "D", 1, NUMERIC | DATE, false),
DAY(true, "DAY", 9, DATE, false),
DD(false, "DD", 2, DATE, false),
DDD(false, "DDD", 3, DATE, false),
DY(true, "DY", 3, DATE, false),
EE(true, "EE", 5, NUMERIC, false),
EEE(true, "EEE", 3, NUMERIC, false),
EEEE(true, "EEEE", 4, NUMERIC, false),
EEEEE(true, "EEEEE", 5, NUMERIC, false),
ES(false, "ES", 18, DATE, false, 9),
ESA(false, "ESA", 18, DATE, false),
FF(false, "FF", 9, TIME, false, 9),
FM(false, "FM", 0, ANY, true),
FX(false, "FX", 0, ANY, true),
G(false, "G", 1, NUMERIC, true),
HH(false, "HH", 2, TIME, false),
HH12(false, "HH12", 2, TIME, false),
HH24(false, "HH24", 2, TIME, false),
MI(false, "MI", 2, NUMERIC | TIME, false),
MM(false, "MM", 2, DATE, false),
MON(true, "MON", 3, DATE, false),
MONTH(true, "MONTH", 9, DATE, false),
PM(true, "PM", 2, TIME, false),
S(false, "S", 0, NUMERIC, false),
SS(false, "SS", 2, TIME, false),
SYYYY(false, "SYYYY", 5, DATE, false),
TM(true, "TM", 64, NUMERIC, false),
TM9(true, "TM9", 64, NUMERIC, false),
TME(true, "TME", 64, NUMERIC, false),
TZD(false, "TZD", 5, TZONE, false),
TZH(false, "TZH", 3, TZONE, false),
TZHTZM(false, "TZHTZM", 5, TZONE, false),
TZIDX(false, "TZIDX", 4, TZONE, false),
TZISO(false, "TZISO", 6, TZONE, false),
TZM(false, "TZM", 2, TZONE, false),
// TZR (false, "TZR", 32, TZONE, false),
X(true, "X", 1, NUMERIC, true),
YY(false, "YY", 2, DATE, false),
YYYY(false, "YYYY", 4, DATE, false),
OPTSP(false, "_", 0, ANY, true),
LITERAL(false, "", 0, ANY, true);
public static final int MAX_KW_LEN = 6; // max keyword length
//
// Constructors
// caseSens - is the keyword case-sensitive
// str - the actual format keyword string
// maxLen - max length of the output for this format element
// repeat - true if element can be repeated
// maxParam - max value for parameter (0 if none)
//
private Keyword(boolean caseSens_, String str_, int maxLen_, int model_, boolean repeat_) {
caseSens = caseSens_;
str = str_;
maxLen = maxLen_;
model = model_;
repeat = repeat_;
maxParam = 0;
}
;
private Keyword(
boolean caseSens_, String str_, int maxLen_, int model_, boolean repeat_, int maxParam_) {
caseSens = caseSens_;
str = str_;
maxLen = maxLen_;
model = model_;
repeat = repeat_;
maxParam = maxParam_;
}
;
public final boolean caseSens;
public final String str;
public final int maxLen;
public final int model;
public final boolean repeat;
public final int maxParam;
};
// Map of keyword strings
private static final HashMap kwMap;
static {
kwMap = new HashMap<>();
for (Keyword kw : Keyword.values()) {
if (kw.str.length() > 0) // exclude LITERAL
kwMap.put(kw.str, kw);
}
}
;
//
// Locate longest-matching keyword for char array of length fmtLen
// starting at fmtIdx
// Returns null if not found
//
private static Keyword findKeyword(char fmt[], int fmtLen, int fmtIdx, int model) {
int maxLen = fmtLen - fmtIdx;
if (maxLen > Keyword.MAX_KW_LEN) maxLen = Keyword.MAX_KW_LEN;
for (int l = maxLen; l > 0; l--) {
Keyword kw = kwMap.get(new String(fmt, fmtIdx, l).toUpperCase());
if (kw != null && (kw.model & model) != 0) return kw;
}
return null;
}
;
// Table of conflicts between keywords
private static final EnumMap> keywordConflicts =
new EnumMap(Keyword.class);
static {
keywordConflicts.put(Keyword.DOT, EnumSet.of(Keyword.D));
keywordConflicts.put(
Keyword.AD, EnumSet.of(Keyword.BC, Keyword.BCE, Keyword.CE, Keyword.SYYYY));
keywordConflicts.put(Keyword.AM, EnumSet.of(Keyword.PM));
keywordConflicts.put(
Keyword.BC, EnumSet.of(Keyword.AD, Keyword.BCE, Keyword.CE, Keyword.SYYYY));
keywordConflicts.put(
Keyword.BCE, EnumSet.of(Keyword.AD, Keyword.BC, Keyword.CE, Keyword.SYYYY));
keywordConflicts.put(
Keyword.CE, EnumSet.of(Keyword.AD, Keyword.BC, Keyword.BCE, Keyword.SYYYY));
keywordConflicts.put(Keyword.D, EnumSet.of(Keyword.DOT));
keywordConflicts.put(Keyword.EE, EnumSet.of(Keyword.EEE, Keyword.EEEE, Keyword.EEEEE));
keywordConflicts.put(Keyword.EEE, EnumSet.of(Keyword.EE, Keyword.EEEE, Keyword.EEEEE));
keywordConflicts.put(Keyword.EEEE, EnumSet.of(Keyword.EE, Keyword.EEE, Keyword.EEEEE));
keywordConflicts.put(Keyword.EEEEE, EnumSet.of(Keyword.EE, Keyword.EEE, Keyword.EEEE));
keywordConflicts.put(Keyword.ES, EnumSet.of(Keyword.ESA));
keywordConflicts.put(Keyword.ESA, EnumSet.of(Keyword.ES));
keywordConflicts.put(Keyword.HH, EnumSet.of(Keyword.HH12, Keyword.HH24));
keywordConflicts.put(Keyword.HH12, EnumSet.of(Keyword.HH, Keyword.HH24));
keywordConflicts.put(Keyword.HH24, EnumSet.of(Keyword.HH, Keyword.HH12));
keywordConflicts.put(Keyword.MI, EnumSet.of(Keyword.S));
keywordConflicts.put(Keyword.MON, EnumSet.of(Keyword.MONTH));
keywordConflicts.put(Keyword.MONTH, EnumSet.of(Keyword.MON));
keywordConflicts.put(Keyword.PM, EnumSet.of(Keyword.AM));
keywordConflicts.put(Keyword.S, EnumSet.of(Keyword.MI));
keywordConflicts.put(
Keyword.SYYYY,
EnumSet.of(Keyword.AD, Keyword.BC, Keyword.CE, Keyword.BCE, Keyword.YY, Keyword.YYYY));
keywordConflicts.put(Keyword.TM, EnumSet.of(Keyword.TM9, Keyword.TME));
keywordConflicts.put(Keyword.TM9, EnumSet.of(Keyword.TM, Keyword.TME));
keywordConflicts.put(Keyword.TME, EnumSet.of(Keyword.TM, Keyword.TM9));
keywordConflicts.put(Keyword.TZH, EnumSet.of(Keyword.TZHTZM, Keyword.TZIDX, Keyword.TZISO));
keywordConflicts.put(
Keyword.TZHTZM, EnumSet.of(Keyword.TZH, Keyword.TZIDX, Keyword.TZISO, Keyword.TZM));
keywordConflicts.put(
Keyword.TZIDX, EnumSet.of(Keyword.TZH, Keyword.TZHTZM, Keyword.TZISO, Keyword.TZM));
keywordConflicts.put(
Keyword.TZISO, EnumSet.of(Keyword.TZH, Keyword.TZHTZM, Keyword.TZIDX, Keyword.TZM));
keywordConflicts.put(Keyword.TZM, EnumSet.of(Keyword.TZHTZM, Keyword.TZIDX, Keyword.TZISO));
keywordConflicts.put(Keyword.YY, EnumSet.of(Keyword.SYYYY, Keyword.YYYY));
keywordConflicts.put(Keyword.YYYY, EnumSet.of(Keyword.SYYYY, Keyword.YY));
}
;
//
// A format fragment
//
private static class Fragment {
// Literal constructor
public Fragment(String str) {
m_elem = Keyword.LITERAL;
m_literal = str;
m_case = 0;
m_param = 0;
}
// Keyword constructor
public Fragment(Keyword kw, int ci, int p) {
m_elem = kw;
m_literal = null;
m_case = ci;
m_param = p;
}
public Keyword m_elem; // Fragment's keyword
public String m_literal; // Literal string
public int m_case; // Case indicators
public int m_param; // Parameter value
}
;
// Internal format state
private String m_errorMsg; // Error message from setFormat()
private EnumSet m_seen; // The set of keywords we've seen in this fmt
private ArrayList m_frags; // The format fragments
private int m_model; // Detected model for the format
private int m_maxOutLen; // Max output length
private int m_precision; // Precision for this format
private int m_scale; // Scale for this format
private int m_reqDigits; // Required # of digits for this format
private int m_minScale; // Minimal scale for this format
private boolean m_tExact; // FX state at the end
// Names of months and days of the week
private static final String[] s_dayNames;
private static final int s_maxDayNameLen = 9;
private static final String[] s_monthNames;
private static final int s_maxMonthNameLen = 9;
private static final String s_spaces;
static {
s_dayNames =
new String[] {"SUNDAY", "MONDAY", "TUESDAY", "WEDNESDAY", "THURSDAY", "FRIDAY", "SATURDAY"};
s_monthNames =
new String[] {
"JANUARY", "FEBRUARY", "MARCH", "APRIL", "MAY", "JUNE",
"JULY", "AUGUST", "SEPTEMBER", "OCTOBER", "NOVEMBER", "DECEMBER"
};
s_spaces = " "; // 9 spaces
}
;
}
;