All Downloads are FREE. Search and download functionalities are using the official Maven repository.

net.snowflake.common.core.SqlFormat Maven / Gradle / Ivy

There is a newer version: 5.1.4
Show newest version
/*
 * 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 } ; } ;





© 2015 - 2024 Weber Informatics LLC | Privacy Policy