
net.snowflake.common.core.SqlFormat Maven / Gradle / Ivy
/*
* Copyright (c) 2016 Snowflake Computing Inc. All right reserved.
*/
package net.snowflake.common.core;
import java.util.*;
import java.util.logging.Level;
import java.lang.*;
import java.lang.invoke.MethodHandles;
import java.text.*;
import java.math.BigInteger;
import net.snowflake.common.util.GSCommonLogUtil;
import net.snowflake.common.util.GenericGSCommonLogger;
import net.snowflake.common.util.power10;
/**
* The is the class for a parsed SQL format, a re-implementation of XP
* SqlFormat class
*/
public class SqlFormat
{
static GenericGSCommonLogger LOGGER =
GSCommonLogUtil.getLogger(MethodHandles.lookup().lookupClass());
// 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)
{
LOGGER.log(Level.INFO, String.format(
"SqlFormat::setFormat(%d, %s)", model, 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 boolean caseSens;
public String str;
public int maxLen;
public int model;
public boolean repeat;
public 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
};
};
© 2015 - 2025 Weber Informatics LLC | Privacy Policy