You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@tajo.apache.org by ji...@apache.org on 2014/05/22 03:33:40 UTC

[4/6] TAJO-825: Datetime type refactoring. (Hyoungjun Kim via jihoon)

http://git-wip-us.apache.org/repos/asf/tajo/blob/526dca28/tajo-common/src/main/java/org/apache/tajo/util/datetime/DateTimeFormat.java
----------------------------------------------------------------------
diff --git a/tajo-common/src/main/java/org/apache/tajo/util/datetime/DateTimeFormat.java b/tajo-common/src/main/java/org/apache/tajo/util/datetime/DateTimeFormat.java
new file mode 100644
index 0000000..bc8435b
--- /dev/null
+++ b/tajo-common/src/main/java/org/apache/tajo/util/datetime/DateTimeFormat.java
@@ -0,0 +1,2143 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.tajo.util.datetime;
+
+import org.apache.tajo.datum.TimestampDatum;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.concurrent.atomic.AtomicInteger;
+
+/**
+ * This class originated from src/backend/utils/adt/formatting.c of PostgreSQL
+ */
+public class DateTimeFormat {
+  /* ----------
+   * Full months_short
+   * ----------
+   */
+  static final String[] months_full = {
+    "January", "February", "March", "April", "May", "June", "July",
+        "August", "September", "October", "November", "December", null
+  };
+
+  static String[] days_short = {
+    "Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat", null
+  };
+
+  static String[] months_short = {"Jan", "Feb", "Mar", "Apr", "May", "Jun",
+      "Jul", "Aug", "Sep", "Oct", "Nov", "Dec", null};
+
+  static String[] days_full = {"Sunday", "Monday", "Tuesday", "Wednesday",
+      "Thursday", "Friday", "Saturday", null};
+
+  static int[][] ysum = {
+    {0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334, 365},
+    {0, 31, 60, 91, 121, 152, 182, 213, 244, 274, 305, 335, 366}
+  };
+
+
+  /**
+   *  AD / BC
+   * ----------
+   * There is no 0 AD.  Years go from 1 BC to 1 AD, so we make it
+   * positive and map year == -1 to year zero, and shift all negative
+   * years up one.  For interval years, we just return the year.
+   * @param year
+   * @param is_interval
+   * @return
+   */
+  static int ADJUST_YEAR(int year, boolean is_interval)	{
+    return ((is_interval) ? (year) : ((year) <= 0 ? -((year) - 1) : (year)));
+  }
+
+  static final String A_D_STR	= "A.D.";
+  static final String a_d_STR	= "a.d.";
+  static final String AD_STR = "AD";
+  static final String ad_STR = "ad";
+
+  static final String B_C_STR	= "B.C.";
+  static final String b_c_STR	= "b.c.";
+  static final String BC_STR = "BC";
+  static final String bc_STR = "bc";
+
+  /**
+   * AD / BC strings for seq_search.
+   *
+   * These are given in two variants, a long form with periods and a standard
+   * form without.
+   *
+   * The array is laid out such that matches for AD have an even index, and
+   * matches for BC have an odd index.  So the boolean value for BC is given by
+   * taking the array index of the match, modulo 2.
+   */
+  static final String[] adbc_strings = {ad_STR, bc_STR, AD_STR, BC_STR, null};
+  static final String[] adbc_strings_long = {a_d_STR, b_c_STR, A_D_STR, B_C_STR, null};
+
+  /**
+   * ----------
+   * AM / PM
+   * ----------
+   */
+  static final String A_M_STR	= "A.M.";
+  static final String a_m_STR	= "a.m.";
+  static final String AM_STR = "AM";
+  static final String am_STR = "am";
+
+  static final String P_M_STR	= "P.M.";
+  static final String p_m_STR	= "p.m.";
+  static final String PM_STR = "PM";
+  static final String pm_STR = "pm";
+
+  /**
+   * AM / PM strings for seq_search.
+   *
+   * These are given in two variants, a long form with periods and a standard
+   * form without.
+   *
+   * The array is laid out such that matches for AM have an even index, and
+   * matches for PM have an odd index.  So the boolean value for PM is given by
+   * taking the array index of the match, modulo 2.
+   */
+  static final String[] ampm_strings = {am_STR, pm_STR, AM_STR, PM_STR, null};
+  static final String[] ampm_strings_long = {a_m_STR, p_m_STR, A_M_STR, P_M_STR, null};
+
+  /**
+   * ----------
+   * Months in roman-numeral
+   * (Must be in reverse order for seq_search (in FROM_CHAR), because
+   *	'VIII' must have higher precedence than 'V')
+   * ----------
+   */
+  static final String[] rm_months_upper =
+    {"XII", "XI", "X", "IX", "VIII", "VII", "VI", "V", "IV", "III", "II", "I", null};
+
+  static final String[] rm_months_lower =
+    {"xii", "xi", "x", "ix", "viii", "vii", "vi", "v", "iv", "iii", "ii", "i", null};
+
+  /**
+   * ----------
+   * Roman numbers
+   * ----------
+   */
+  static final String[] rm1 = {"I", "II", "III", "IV", "V", "VI", "VII", "VIII", "IX", null};
+  static final String[] rm10 = {"X", "XX", "XXX", "XL", "L", "LX", "LXX", "LXXX", "XC", null};
+  static final String[] rm100 = {"C", "CC", "CCC", "CD", "D", "DC", "DCC", "DCCC", "CM", null};
+
+  /**
+   * ----------
+   * Ordinal postfixes
+   * ----------
+   */
+  static final String[] numTH = {"ST", "ND", "RD", "TH", null};
+  static final String[] numth = {"st", "nd", "rd", "th", null};
+
+  /**
+   * ----------
+   * Flags & Options:
+   * ----------
+   */
+  static final int ONE_UPPER = 1;		/* Name */
+  static final int ALL_UPPER = 2;		/* NAME */
+  static final int ALL_LOWER = 3;		/* name */
+
+  static final int MAX_MONTH_LEN = 9;
+  static final int MAX_MON_LEN = 3;
+  static final int MAX_DAY_LEN = 9;
+  static final int MAX_DY_LEN	= 3;
+  static final int MAX_RM_LEN	= 4;
+
+  static final int DCH_S_FM =	0x01;
+  static final int DCH_S_TH	= 0x02;
+  static final int DCH_S_th	= 0x04;
+  static final int DCH_S_SP	= 0x08;
+  static final int DCH_S_TM	= 0x10;
+
+  static final int NODE_TYPE_END = 1;
+  static final int NODE_TYPE_ACTION = 2;
+  static final int NODE_TYPE_CHAR = 3;
+
+  static final int SUFFTYPE_PREFIX = 1;
+  static final int SUFFTYPE_POSTFIX = 2;
+
+  static final int CLOCK_24_HOUR = 0;
+  static final int CLOCK_12_HOUR = 1;
+
+  static final int MONTHS_PER_YEAR = 12;
+  static final int HOURS_PER_DAY = 24;
+
+  /**
+   * ----------
+   * Maximal length of one node
+   * ----------
+   */
+  static final int DCH_MAX_ITEM_SIZ	= 9;		/* max julian day		*/
+  static final int NUM_MAX_ITEM_SIZ	= 8;		/* roman number (RN has 15 chars)	*/
+
+  enum FORMAT_TYPE {
+    DCH_TYPE, NUM_TYPE
+  }
+
+  /**
+   * ----------
+   * Suffixes definition for DATE-TIME TO/FROM CHAR
+   * ----------
+   */
+  static KeySuffix[] DCH_suff = {
+    new KeySuffix("FM", 2, DCH_S_FM, SUFFTYPE_PREFIX),
+    new KeySuffix("fm", 2, DCH_S_FM, SUFFTYPE_PREFIX),
+    new KeySuffix("TM", 2, DCH_S_TM, SUFFTYPE_PREFIX),
+    new KeySuffix("tm", 2, DCH_S_TM, SUFFTYPE_PREFIX),
+    new KeySuffix("TH", 2, DCH_S_TH, SUFFTYPE_POSTFIX),
+    new KeySuffix("th", 2, DCH_S_th, SUFFTYPE_POSTFIX),
+    new KeySuffix("SP", 2, DCH_S_SP, SUFFTYPE_POSTFIX),
+  };
+
+  /**
+   * ----------
+   * Format-pictures (KeyWord).
+   *
+   * The KeyWord field; alphabetic sorted, *BUT* strings alike is sorted
+   *		  complicated -to-> easy:
+   *
+   *	(example: "DDD","DD","Day","D" )
+   *
+   * (this specific sort needs the algorithm for sequential search for strings,
+   * which not has exact end; -> How keyword is in "HH12blabla" ? - "HH"
+   * or "HH12"? You must first try "HH12", because "HH" is in string, but
+   * it is not good.
+   *
+   * (!)
+   *	 - Position for the keyword is similar as position in the enum DCH/NUM_poz.
+   * (!)
+   *
+   * For fast search is used the 'int index[]', index is ascii table from position
+   * 32 (' ') to 126 (~), in this index is DCH_ / NUM_ enums for each ASCII
+   * position or -1 if char is not used in the KeyWord. Search example for
+   * string "MM":
+   *	1)	see in index to index['M' - 32],
+   *	2)	take keywords position (enum DCH_MI) from index
+   *	3)	run sequential search in keywords[] from this position
+   *
+   * ----------
+   */
+  enum DCH_poz {
+    DCH_A_D(0),
+    DCH_A_M(1),
+    DCH_AD(2),
+    DCH_AM(3),
+    DCH_B_C(4),
+    DCH_BC(5),
+    DCH_CC(6),
+    DCH_DAY(7),
+    DCH_DDD(8),
+    DCH_DD(9),
+
+    DCH_DY(10),
+    DCH_Day(11),
+    DCH_Dy(12),
+    DCH_D(13),
+    DCH_FX(14),						/* global suffix */
+    DCH_HH24(15),
+    DCH_HH12(16),
+    DCH_HH(17),
+    DCH_IDDD(18),
+    DCH_ID(19),
+
+    DCH_IW(20),
+    DCH_IYYY(21),
+    DCH_IYY(22),
+    DCH_IY(23),
+    DCH_I(24),
+    DCH_J(25),
+    DCH_MI(26),
+    DCH_MM(27),
+    DCH_MONTH(28),
+    DCH_MON(29),
+
+    DCH_MS(30),
+    DCH_Month(31),
+    DCH_Mon(32),
+    DCH_P_M(33),
+    DCH_PM(34),
+    DCH_Q(35),
+    DCH_RM(36),
+    DCH_SSSS(37),
+    DCH_SS(38),
+    DCH_TZ(39),
+
+    DCH_US(40),
+    DCH_WW(41),
+    DCH_W(42),
+    DCH_Y_YYY(43),
+    DCH_YYYY(44),
+    DCH_YYY(45),
+    DCH_YY(46),
+    DCH_Y(47),
+    DCH_a_d(48),
+    DCH_a_m(49),
+
+    DCH_ad(50),
+    DCH_am(51),
+    DCH_b_c(52),
+    DCH_bc(53),
+    DCH_cc(54),
+    DCH_day(55),
+    DCH_ddd(56),
+    DCH_dd(57),
+    DCH_dy(58),
+    DCH_d(59),
+
+    DCH_fx(60),
+    DCH_hh24(61),
+    DCH_hh12(62),
+    DCH_hh(63),
+    DCH_iddd(64),
+    DCH_id(65),
+    DCH_iw(66),
+    DCH_iyyy(67),
+    DCH_iyy(68),
+    DCH_iy(69),
+
+    DCH_i(70),
+    DCH_j(71),
+    DCH_mi(72),
+    DCH_mm(73),
+    DCH_month(74),
+    DCH_mon(75),
+    DCH_ms(76),
+    DCH_p_m(77),
+    DCH_pm(78),
+    DCH_q(79),
+
+    DCH_rm(80),
+    DCH_ssss(89),
+    DCH_ss(90),
+    DCH_tz(91),
+    DCH_us(92),
+    DCH_ww(93),
+    DCH_w(94),
+    DCH_y_yyy(95),
+    DCH_yyyy(96),
+    DCH_yyy(97),
+    DCH_yy(98),
+    DCH_y(99),
+    _DCH_last_(Integer.MAX_VALUE);
+
+    int value;
+    DCH_poz(int value) {
+      this.value = value;
+    }
+
+    public int getValue() {
+      return value;
+    }
+  }
+
+  /**
+   * ----------
+   * FromCharDateMode
+   * ----------
+   *
+   * This value is used to nominate one of several distinct (and mutually
+   * exclusive) date conventions that a keyword can belong to.
+   */
+  enum FromCharDateMode
+  {
+    FROM_CHAR_DATE_NONE,	/* Value does not affect date mode. */
+    FROM_CHAR_DATE_GREGORIAN,	/* Gregorian (day, month, year) style date */
+    FROM_CHAR_DATE_ISOWEEK		/* ISO 8601 week date */
+  }
+
+  /**
+   * ----------
+   * KeyWords for DATE-TIME version
+   * ----------
+   */
+  static final Object[][] DCH_keywordValues = {
+      /*	name, len, id, is_digit, date_mode */
+      {"A.D.", 4, DCH_poz.DCH_A_D, false, FromCharDateMode.FROM_CHAR_DATE_NONE},	/* A */
+      {"A.M.", 4, DCH_poz.DCH_A_M, false, FromCharDateMode.FROM_CHAR_DATE_NONE},
+      {"AD", 2, DCH_poz.DCH_AD, false, FromCharDateMode.FROM_CHAR_DATE_NONE},
+      {"AM", 2, DCH_poz.DCH_AM, false, FromCharDateMode.FROM_CHAR_DATE_NONE},
+      {"B.C.", 4, DCH_poz.DCH_B_C, false, FromCharDateMode.FROM_CHAR_DATE_NONE},	/* B */
+      {"BC", 2, DCH_poz.DCH_BC, false, FromCharDateMode.FROM_CHAR_DATE_NONE},
+      {"CC", 2, DCH_poz.DCH_CC, true, FromCharDateMode.FROM_CHAR_DATE_NONE},		/* C */
+      {"DAY", 3, DCH_poz.DCH_DAY, false, FromCharDateMode.FROM_CHAR_DATE_NONE},	/* D */
+      {"DDD", 3, DCH_poz.DCH_DDD, true, FromCharDateMode.FROM_CHAR_DATE_GREGORIAN},
+      {"DD", 2, DCH_poz.DCH_DD, true, FromCharDateMode.FROM_CHAR_DATE_GREGORIAN},
+      {"DY", 2, DCH_poz.DCH_DY, false, FromCharDateMode.FROM_CHAR_DATE_NONE},
+      {"Day", 3, DCH_poz.DCH_Day, false, FromCharDateMode.FROM_CHAR_DATE_NONE},
+      {"Dy", 2, DCH_poz.DCH_Dy, false, FromCharDateMode.FROM_CHAR_DATE_NONE},
+      {"D", 1, DCH_poz.DCH_D, true, FromCharDateMode.FROM_CHAR_DATE_GREGORIAN},
+      {"FX", 2, DCH_poz.DCH_FX, false, FromCharDateMode.FROM_CHAR_DATE_NONE},		/* F */
+      {"HH24", 4, DCH_poz.DCH_HH24, true, FromCharDateMode.FROM_CHAR_DATE_NONE},	/* H */
+      {"HH12", 4, DCH_poz.DCH_HH12, true, FromCharDateMode.FROM_CHAR_DATE_NONE},
+      {"HH", 2, DCH_poz.DCH_HH, true, FromCharDateMode.FROM_CHAR_DATE_NONE},
+      {"IDDD", 4, DCH_poz.DCH_IDDD, true, FromCharDateMode.FROM_CHAR_DATE_ISOWEEK},		/* I */
+      {"ID", 2, DCH_poz.DCH_ID, true, FromCharDateMode.FROM_CHAR_DATE_ISOWEEK},
+      {"IW", 2, DCH_poz.DCH_IW, true, FromCharDateMode.FROM_CHAR_DATE_ISOWEEK},
+      {"IYYY", 4, DCH_poz.DCH_IYYY, true, FromCharDateMode.FROM_CHAR_DATE_ISOWEEK},
+      {"IYY", 3, DCH_poz.DCH_IYY, true, FromCharDateMode.FROM_CHAR_DATE_ISOWEEK},
+      {"IY", 2, DCH_poz.DCH_IY, true, FromCharDateMode.FROM_CHAR_DATE_ISOWEEK},
+      {"I", 1, DCH_poz.DCH_I, true, FromCharDateMode.FROM_CHAR_DATE_ISOWEEK},
+      {"J", 1, DCH_poz.DCH_J, true, FromCharDateMode.FROM_CHAR_DATE_NONE}, /* J */
+      {"MI", 2, DCH_poz.DCH_MI, true, FromCharDateMode.FROM_CHAR_DATE_NONE},		/* M */
+      {"MM", 2, DCH_poz.DCH_MM, true, FromCharDateMode.FROM_CHAR_DATE_GREGORIAN},
+      {"MONTH", 5, DCH_poz.DCH_MONTH, false, FromCharDateMode.FROM_CHAR_DATE_GREGORIAN},
+      {"MON", 3, DCH_poz.DCH_MON, false, FromCharDateMode.FROM_CHAR_DATE_GREGORIAN},
+      {"MS", 2, DCH_poz.DCH_MS, true, FromCharDateMode.FROM_CHAR_DATE_NONE},
+      {"Month", 5, DCH_poz.DCH_Month, false, FromCharDateMode.FROM_CHAR_DATE_GREGORIAN},
+      {"Mon", 3, DCH_poz.DCH_Mon, false, FromCharDateMode.FROM_CHAR_DATE_GREGORIAN},
+      {"P.M.", 4, DCH_poz.DCH_P_M, false, FromCharDateMode.FROM_CHAR_DATE_NONE},	/* P */
+      {"PM", 2, DCH_poz.DCH_PM, false, FromCharDateMode.FROM_CHAR_DATE_NONE},
+      {"Q", 1, DCH_poz.DCH_Q, true, FromCharDateMode.FROM_CHAR_DATE_NONE}, /* Q */
+      {"RM", 2, DCH_poz.DCH_RM, false, FromCharDateMode.FROM_CHAR_DATE_GREGORIAN}, /* R */
+      {"SSSS", 4, DCH_poz.DCH_SSSS, true, FromCharDateMode.FROM_CHAR_DATE_NONE},	/* S */
+      {"SS", 2, DCH_poz.DCH_SS, true, FromCharDateMode.FROM_CHAR_DATE_NONE},
+      {"TZ", 2, DCH_poz.DCH_TZ, false, FromCharDateMode.FROM_CHAR_DATE_NONE},		/* T */
+      {"US", 2, DCH_poz.DCH_US, true, FromCharDateMode.FROM_CHAR_DATE_NONE},		/* U */
+      {"WW", 2, DCH_poz.DCH_WW, true, FromCharDateMode.FROM_CHAR_DATE_GREGORIAN},	/* W */
+      {"W", 1, DCH_poz.DCH_W, true, FromCharDateMode.FROM_CHAR_DATE_GREGORIAN},
+      {"Y,YYY", 5, DCH_poz.DCH_Y_YYY, true, FromCharDateMode.FROM_CHAR_DATE_GREGORIAN},	/* Y */
+      {"YYYY", 4, DCH_poz.DCH_YYYY, true, FromCharDateMode.FROM_CHAR_DATE_GREGORIAN},
+      {"YYY", 3, DCH_poz.DCH_YYY, true, FromCharDateMode.FROM_CHAR_DATE_GREGORIAN},
+      {"YY", 2, DCH_poz.DCH_YY, true, FromCharDateMode.FROM_CHAR_DATE_GREGORIAN},
+      {"Y", 1, DCH_poz.DCH_Y, true, FromCharDateMode.FROM_CHAR_DATE_GREGORIAN},
+      {"a.d.", 4, DCH_poz.DCH_a_d, false, FromCharDateMode.FROM_CHAR_DATE_NONE},	/* a */
+      {"a.m.", 4, DCH_poz.DCH_a_m, false, FromCharDateMode.FROM_CHAR_DATE_NONE},
+      {"ad", 2, DCH_poz.DCH_ad, false, FromCharDateMode.FROM_CHAR_DATE_NONE},
+      {"am", 2, DCH_poz.DCH_am, false, FromCharDateMode.FROM_CHAR_DATE_NONE},
+      {"b.c.", 4, DCH_poz.DCH_b_c, false, FromCharDateMode.FROM_CHAR_DATE_NONE},	/* b */
+      {"bc", 2, DCH_poz.DCH_bc, false, FromCharDateMode.FROM_CHAR_DATE_NONE},
+      {"cc", 2, DCH_poz.DCH_CC, true, FromCharDateMode.FROM_CHAR_DATE_NONE},		/* c */
+      {"day", 3, DCH_poz.DCH_day, false, FromCharDateMode.FROM_CHAR_DATE_NONE},	/* d */
+      {"ddd", 3, DCH_poz.DCH_DDD, true, FromCharDateMode.FROM_CHAR_DATE_GREGORIAN},
+      {"dd", 2, DCH_poz.DCH_DD, true, FromCharDateMode.FROM_CHAR_DATE_GREGORIAN},
+      {"dy", 2, DCH_poz.DCH_dy, false, FromCharDateMode.FROM_CHAR_DATE_NONE},
+      {"d", 1, DCH_poz.DCH_D, true, FromCharDateMode.FROM_CHAR_DATE_GREGORIAN},
+      {"fx", 2, DCH_poz.DCH_FX, false, FromCharDateMode.FROM_CHAR_DATE_NONE},		/* f */
+      {"hh24", 4, DCH_poz.DCH_HH24, true, FromCharDateMode.FROM_CHAR_DATE_NONE},	/* h */
+      {"hh12", 4, DCH_poz.DCH_HH12, true, FromCharDateMode.FROM_CHAR_DATE_NONE},
+      {"hh", 2, DCH_poz.DCH_HH, true, FromCharDateMode.FROM_CHAR_DATE_NONE},
+      {"iddd", 4, DCH_poz.DCH_IDDD, true, FromCharDateMode.FROM_CHAR_DATE_ISOWEEK},		/* i */
+      {"id", 2, DCH_poz.DCH_ID, true, FromCharDateMode.FROM_CHAR_DATE_ISOWEEK},
+      {"iw", 2, DCH_poz.DCH_IW, true, FromCharDateMode.FROM_CHAR_DATE_ISOWEEK},
+      {"iyyy", 4, DCH_poz.DCH_IYYY, true, FromCharDateMode.FROM_CHAR_DATE_ISOWEEK},
+      {"iyy", 3, DCH_poz.DCH_IYY, true, FromCharDateMode.FROM_CHAR_DATE_ISOWEEK},
+      {"iy", 2, DCH_poz.DCH_IY, true, FromCharDateMode.FROM_CHAR_DATE_ISOWEEK},
+      {"i", 1, DCH_poz.DCH_I, true, FromCharDateMode.FROM_CHAR_DATE_ISOWEEK},
+      {"j", 1, DCH_poz.DCH_J, true, FromCharDateMode.FROM_CHAR_DATE_NONE}, /* j */
+      {"mi", 2, DCH_poz.DCH_MI, true, FromCharDateMode.FROM_CHAR_DATE_NONE},		/* m */
+      {"mm", 2, DCH_poz.DCH_MM, true, FromCharDateMode.FROM_CHAR_DATE_GREGORIAN},
+      {"month", 5, DCH_poz.DCH_month, false, FromCharDateMode.FROM_CHAR_DATE_GREGORIAN},
+      {"mon", 3, DCH_poz.DCH_mon, false, FromCharDateMode.FROM_CHAR_DATE_GREGORIAN},
+      {"ms", 2, DCH_poz.DCH_MS, true, FromCharDateMode.FROM_CHAR_DATE_NONE},
+      {"p.m.", 4, DCH_poz.DCH_p_m, false, FromCharDateMode.FROM_CHAR_DATE_NONE},	/* p */
+      {"pm", 2, DCH_poz.DCH_pm, false, FromCharDateMode.FROM_CHAR_DATE_NONE},
+      {"q", 1, DCH_poz.DCH_Q, true, FromCharDateMode.FROM_CHAR_DATE_NONE}, /* q */
+      {"rm", 2, DCH_poz.DCH_rm, false, FromCharDateMode.FROM_CHAR_DATE_GREGORIAN}, /* r */
+      {"ssss", 4, DCH_poz.DCH_SSSS, true, FromCharDateMode.FROM_CHAR_DATE_NONE},	/* s */
+      {"ss", 2, DCH_poz.DCH_SS, true, FromCharDateMode.FROM_CHAR_DATE_NONE},
+      {"tz", 2, DCH_poz.DCH_tz, false, FromCharDateMode.FROM_CHAR_DATE_NONE},		/* t */
+      {"us", 2, DCH_poz.DCH_US, true, FromCharDateMode.FROM_CHAR_DATE_NONE},		/* u */
+      {"ww", 2, DCH_poz.DCH_WW, true, FromCharDateMode.FROM_CHAR_DATE_GREGORIAN},	/* w */
+      {"w", 1, DCH_poz.DCH_W, true, FromCharDateMode.FROM_CHAR_DATE_GREGORIAN},
+      {"y,yyy", 5, DCH_poz.DCH_Y_YYY, true, FromCharDateMode.FROM_CHAR_DATE_GREGORIAN},	/* y */
+      {"yyyy", 4, DCH_poz.DCH_YYYY, true, FromCharDateMode.FROM_CHAR_DATE_GREGORIAN},
+      {"yyy", 3, DCH_poz.DCH_YYY, true, FromCharDateMode.FROM_CHAR_DATE_GREGORIAN},
+      {"yy", 2, DCH_poz.DCH_YY, true, FromCharDateMode.FROM_CHAR_DATE_GREGORIAN},
+      {"y", 1, DCH_poz.DCH_Y, true, FromCharDateMode.FROM_CHAR_DATE_GREGORIAN}
+  };
+
+  static final KeyWord[] DCH_keywords = new KeyWord[DCH_keywordValues.length];
+
+  static Map<Character, Integer> DCH_index = new HashMap<Character, Integer>();
+
+  static {
+    int index = 0;
+    for(Object[] eachKeywordValue: DCH_keywordValues) {
+      KeyWord keyword = new KeyWord();
+      keyword.name = (String)eachKeywordValue[0];
+      keyword.len = ((Integer)eachKeywordValue[1]).intValue();
+      keyword.id = ((DCH_poz)eachKeywordValue[2]).getValue();
+      keyword.idType = ((DCH_poz)eachKeywordValue[2]);
+      keyword.is_digit = ((Boolean)eachKeywordValue[3]).booleanValue();
+      keyword.date_mode = (FromCharDateMode)eachKeywordValue[4];
+
+      Character c = new Character(keyword.name.charAt(0));
+      Integer pos = DCH_index.get(c);
+      if (pos == null) {
+        DCH_index.put(c, index);
+      }
+      DCH_keywords[index++] = keyword;
+    }
+  }
+
+  /**
+   * ----------
+   * Format parser structs
+   * ----------
+   */
+  static class KeySuffix {
+    String name;			/* suffix string		*/
+    int len;			    /* suffix length		*/
+    int id;				    /* used in node->suffix */
+    int type;			    /* prefix / postfix			*/
+
+    public KeySuffix(String name, int len, int id, int type) {
+      this.name = name;
+      this.len = len;
+      this.id = id;
+      this.type = type;
+    }
+  }
+
+  static class KeyWord {
+    String name;
+    int len;
+    int id;
+    DCH_poz idType;
+    boolean is_digit;
+    FromCharDateMode date_mode;
+  }
+
+  static class FormatNode {
+    int type;			  /* node type			*/
+    KeyWord key;		/* if node type is KEYWORD	*/
+    char character;	/* if node type is CHAR		*/
+    int suffix;			/* keyword suffix		*/
+  }
+
+  static class TmFromChar {
+    FromCharDateMode mode = FromCharDateMode.FROM_CHAR_DATE_NONE;
+    int	hh;
+    int pm;
+    int mi;
+    int ss;
+    int ssss;
+    int d;				/* stored as 1-7, Sunday = 1, 0 means missing */
+    int dd;
+    int ddd;
+    int mm;
+    int ms;
+    int year;
+    int bc;
+    int ww;
+    int w;
+    int cc;
+    int j;
+    int us;
+    int yysz;			/* is it YY or YYYY ? */
+    int clock;		/* 12 or 24 hour clock? */
+  }
+  static Map<String, FormatNode[]> formatNodeCache = new HashMap<String, FormatNode[]>();
+
+ /**
+  * ----------
+  * Skip TM / th in FROM_CHAR
+  * ----------
+  */
+  static int SKIP_THth(int suf)	{
+    return (S_THth(suf) != 0 ? 2 : 0);
+  }
+
+  /**
+   * ----------
+   * Suffix tests
+   * ----------
+   */
+  static int S_THth(int s) {
+    return ((((s) & DCH_S_TH) != 0 || ((s) & DCH_S_th) != 0) ? 1 : 0);
+  }
+  static int S_TH(int s) {
+    return (((s) & DCH_S_TH) != 0 ? 1 : 0);
+  }
+  static int S_th(int s) {
+    return (((s) & DCH_S_th) != 0 ? 1 : 0);
+  }
+  static int S_TH_TYPE(int s) {
+    return (((s) & DCH_S_TH) != 0 ? TH_UPPER : TH_LOWER);
+  }
+
+  static final int TH_UPPER	=	1;
+  static final int TH_LOWER = 2;
+
+  /* Oracle toggles FM behavior, we don't; see docs. */
+  static int S_FM(int s) {
+    return (((s) & DCH_S_FM) != 0 ? 1 : 0);
+  }
+  static int S_SP(int s) {
+    return (((s) & DCH_S_SP) != 0 ? 1 : 0);
+  }
+  static int S_TM(int s) {
+    return (((s) & DCH_S_TM) != 0 ? 1 : 0);
+  }
+
+  public static TimeMeta parseDateTime(String dateText, String formatText) {
+    TimeMeta tm = new TimeMeta();
+
+    //TODO consider TimeZone
+    doToTimestamp(dateText, formatText, tm);
+
+    if (tm.dayOfYear > 0 && tm.dayOfMonth > 0) {
+      tm.dayOfYear = 0;
+    }
+
+    return tm;
+  }
+
+  /**
+   * Make Timestamp from date_str which is formatted at argument 'fmt'
+   * ( toTimestamp is reverse to_char() )
+   * @param dateText
+   * @param formatText
+   * @return
+   */
+  public static TimestampDatum toTimestamp(String dateText, String formatText) {
+    TimeMeta tm = parseDateTime(dateText, formatText);
+
+    return new TimestampDatum(DateTimeUtil.toJulianTimestamp(tm));
+  }
+
+  /**
+   * Parse the 'dateText' according to 'formatText', return results as a TimeMeta tm
+   * and fractional seconds.
+   *
+   * We parse 'formatText' into a list of FormatNodes, which is then passed to
+   * DCH_from_char to populate a TmFromChar with the parsed contents of
+   * 'dateText'.
+   *
+   * The TmFromChar is then analysed and converted into the final results in struct 'tm'.
+   *
+   * This function does very little error checking, e.g.
+   * to_timestamp('20096040','YYYYMMDD') works
+   * @param dateText
+   * @param formatText
+   * @param tm
+   */
+  static void doToTimestamp(String dateText, String formatText, TimeMeta tm) {
+    TmFromChar tmfc = new TmFromChar();
+    int formatLength = formatText.length();
+
+    if (formatLength > 0) {
+      FormatNode[] formatNodes;
+      synchronized(formatNodeCache) {
+        formatNodes = formatNodeCache.get(formatText);
+      }
+
+      if (formatNodes == null) {
+        formatNodes = new FormatNode[formatLength + 1];
+        for (int i = 0; i < formatNodes.length; i++) {
+          formatNodes[i] = new FormatNode();
+        }
+        parseFormat(formatNodes, formatText, FORMAT_TYPE.DCH_TYPE);
+        formatNodes[formatLength].type = NODE_TYPE_END;	/* Paranoia? */
+
+        synchronized(formatNodeCache) {
+          formatNodeCache.put(formatText, formatNodes);
+        }
+      }
+      DCH_from_char(formatNodes, dateText, tmfc);
+    }
+
+    /*
+     * Convert values that user define for FROM_CHAR (to_date/to_timestamp) to
+     * standard 'tm'
+     */
+    if (tmfc.ssss != 0) {
+      int x = tmfc.ssss;
+
+      tm.hours = x / DateTimeConstants.SECS_PER_HOUR;
+      x %= DateTimeConstants.SECS_PER_HOUR;
+      tm.minutes = x / DateTimeConstants.SECS_PER_MINUTE;
+      x %= DateTimeConstants.SECS_PER_MINUTE;
+      tm.secs = x;
+    }
+
+    if (tmfc.ss != 0) {
+      tm.secs = tmfc.ss;
+    }
+    if (tmfc.mi != 0) {
+      tm.minutes = tmfc.mi;
+    }
+    if (tmfc.hh != 0) {
+      tm.hours = tmfc.hh;
+    }
+
+    if (tmfc.clock == CLOCK_12_HOUR) {
+      if (tm.hours < 1 || tm.hours > HOURS_PER_DAY / 2) {
+        throw new IllegalArgumentException(
+            "hour \"" + tm.hours + "\" is invalid for the 12-hour clock, " +
+                "Use the 24-hour clock, or give an hour between 1 and 12.");
+      }
+      if (tmfc.pm != 0 && tm.hours < HOURS_PER_DAY / 2) {
+        tm.hours += HOURS_PER_DAY / 2;
+      } else if (tmfc.pm == 0 && tm.hours == HOURS_PER_DAY / 2) {
+        tm.hours = 0;
+      }
+    }
+
+    if (tmfc.year != 0) {
+      /*
+       * If CC and YY (or Y) are provided, use YY as 2 low-order digits for
+       * the year in the given century.  Keep in mind that the 21st century
+       * AD runs from 2001-2100, not 2000-2099; 6th century BC runs from
+       * 600BC to 501BC.
+       */
+      if (tmfc.cc != 0 && tmfc.yysz <= 2) {
+        if (tmfc.bc != 0) {
+          tmfc.cc = -tmfc.cc;
+        }
+        tm.years = tmfc.year % 100;
+        if (tm.years != 0) {
+          if (tmfc.cc >= 0) {
+            tm.years += (tmfc.cc - 1) * 100;
+          } else {
+            tm.years = (tmfc.cc + 1) * 100 - tm.years + 1;
+          }
+        } else {
+				  /* find century year for dates ending in "00" */
+          tm.years = tmfc.cc * 100 + ((tmfc.cc >= 0) ? 0 : 1);
+        }
+      } else {
+			  /* If a 4-digit year is provided, we use that and ignore CC. */
+        tm.years = tmfc.year;
+        if (tmfc.bc != 0 && tm.years > 0) {
+          tm.years = -(tm.years - 1);
+        }
+      }
+    }
+    else if (tmfc.cc != 0) {			/* use first year of century */
+      if (tmfc.bc != 0) {
+        tmfc.cc = -tmfc.cc;
+      }
+      if (tmfc.cc >= 0) {
+			  /* +1 becuase 21st century started in 2001 */
+        tm.years = (tmfc.cc - 1) * 100 + 1;
+      } else {
+			  /* +1 because year == 599 is 600 BC */
+        tm.years = tmfc.cc * 100 + 1;
+      }
+    }
+
+    if (tmfc.j != 0) {
+      DateTimeUtil.j2date(tmfc.j, tm);
+    }
+    if (tmfc.ww != 0) {
+      if (tmfc.mode == FromCharDateMode.FROM_CHAR_DATE_ISOWEEK) {
+        /*
+         * If tmfc.d is not set, then the date is left at the beginning of
+         * the ISO week (Monday).
+         */
+        if (tmfc.d != 0) {
+          DateTimeUtil.isoweekdate2date(tmfc.ww, tmfc.d, tm);
+        } else {
+          DateTimeUtil.isoweek2date(tmfc.ww, tm);
+        }
+      } else {
+        tmfc.ddd = (tmfc.ww - 1) * 7 + 1;
+      }
+    }
+
+    if (tmfc.w != 0) {
+      tmfc.dd = (tmfc.w - 1) * 7 + 1;
+    }
+    if (tmfc.d != 0) {
+      //tm.tm_wday = tmfc.d - 1;		/* convert to native numbering */
+    }
+    if (tmfc.dd != 0) {
+      tm.dayOfMonth = tmfc.dd;
+    }
+    if (tmfc.ddd != 0) {
+      tm.dayOfYear = tmfc.ddd;
+    }
+    if (tmfc.mm != 0) {
+      tm.monthOfYear = tmfc.mm;
+    }
+    if (tmfc.ddd != 0 && (tm.monthOfYear <= 1 || tm.dayOfMonth <= 1)) {
+      /*
+       * The month and day field have not been set, so we use the
+       * day-of-year field to populate them.	Depending on the date mode,
+       * this field may be interpreted as a Gregorian day-of-year, or an ISO
+       * week date day-of-year.
+       */
+      if (tm.years == 0 && tmfc.bc == 0) {
+        throw new IllegalArgumentException("cannot calculate day of year without year information");
+      }
+      if (tmfc.mode == FromCharDateMode.FROM_CHAR_DATE_ISOWEEK) {
+        /* zeroth day of the ISO year, in Julian */
+        int j0 = DateTimeUtil.isoweek2j(tm.years, 1) - 1;
+        DateTimeUtil.j2date(j0 + tmfc.ddd, tm);
+      } else {
+        int	i;
+
+        boolean leap = DateTimeUtil.isLeapYear(tm.years);
+        int[] y = ysum[leap ? 1 : 0];
+
+        for (i = 1; i <= MONTHS_PER_YEAR; i++) {
+          if (tmfc.ddd < y[i])
+            break;
+        }
+        if (tm.monthOfYear <= 1) {
+          tm.monthOfYear = i;
+        }
+
+        if (tm.dayOfMonth <= 1) {
+          tm.dayOfMonth = tmfc.ddd - y[i - 1];
+        }
+        tm.dayOfYear = 0;
+      }
+    }
+
+    if (tmfc.ms != 0) {
+      tm.fsecs += tmfc.ms * 1000;
+    }
+    if (tmfc.us != 0) {
+      tm.fsecs += tmfc.us;
+    }
+  }
+
+  /**
+   * Format parser, search small keywords and keyword's suffixes, and make
+   * format-node tree.
+   *
+   * for DATE-TIME & NUMBER version
+   * @param node
+   * @param str
+   * @param ver
+   */
+  static void parseFormat(FormatNode[] node, String str, FORMAT_TYPE ver) {
+    KeySuffix  s;
+    boolean	node_set = false;
+    int suffix;
+    int last = 0;
+
+    int nodeIndex = 0;
+    int charIdx = 0;
+    char[] chars = str.toCharArray();
+
+    while (charIdx < chars.length) {
+      suffix = 0;
+
+      // Prefix
+      if (ver == FORMAT_TYPE.DCH_TYPE && (s = suff_search(chars, charIdx, SUFFTYPE_PREFIX)) != null) {
+        suffix |= s.id;
+        if (s.len > 0) {
+          charIdx += s.len;
+        }
+      }
+
+      // Keyword
+      if (charIdx < chars.length && (node[nodeIndex].key = index_seq_search(chars, charIdx)) != null) {
+        node[nodeIndex].type = NODE_TYPE_ACTION;
+        node[nodeIndex].suffix = 0;
+        node_set = true;
+        if (node[nodeIndex].key.len > 0) {
+          charIdx += node[nodeIndex].key.len;
+        }
+
+        // NUM version: Prepare global NUMDesc struct
+        if (ver == FORMAT_TYPE.NUM_TYPE) {
+          //NUMDesc_prepare(Num, node);
+        }
+
+         // Postfix
+        if (ver == FORMAT_TYPE.DCH_TYPE && charIdx < chars.length  && (s = suff_search(chars, charIdx, SUFFTYPE_POSTFIX)) != null) {
+          suffix |= s.id;
+          if (s.len > 0) {
+            charIdx += s.len;
+          }
+        }
+      } else if (charIdx < chars.length) {
+        // Special characters '\' and '"'
+        if (chars[charIdx] == '"' && last != '\\') {
+          int			x = 0;
+
+          while (charIdx < chars.length ) {
+            charIdx++;
+            if (chars[charIdx] == '"' && x != '\\') {
+              charIdx++;
+              break;
+            } else if (chars[charIdx] == '\\' && x != '\\') {
+              x = '\\';
+              continue;
+            }
+            node[nodeIndex].type = NODE_TYPE_CHAR;
+            node[nodeIndex].character = chars[charIdx];
+            node[nodeIndex].key = null;
+            node[nodeIndex].suffix = 0;
+            nodeIndex++;
+            x = chars[charIdx];
+          }
+          node_set = false;
+          suffix = 0;
+          last = 0;
+        } else if (charIdx < chars.length - 1 && chars[charIdx] == '\\' && last != '\\' && chars[charIdx + 1] == '"') {
+          last = chars[charIdx];
+          charIdx++;
+        } else if (charIdx < chars.length) {
+          node[nodeIndex].type = NODE_TYPE_CHAR;
+          node[nodeIndex].character = chars[charIdx];
+          node[nodeIndex].key = null;
+          node_set = true;
+          last = 0;
+          charIdx++;
+        }
+      }
+
+		  // end
+      if (node_set) {
+        if (node[nodeIndex].type == NODE_TYPE_ACTION) {
+          node[nodeIndex].suffix = suffix;
+        }
+        nodeIndex++;
+        node[nodeIndex].suffix = 0;
+        node_set = false;
+      }
+    }
+
+    node[nodeIndex].type = NODE_TYPE_END;
+    node[nodeIndex].suffix = 0;
+  }
+
+  /**
+   * Process a string as denoted by a list of FormatNodes.
+   * The TmFromChar struct pointed to by 'out' is populated with the results.
+   *
+   * Note: we currently don't have any to_interval() function, so there
+   * is no need here for INVALID_FOR_INTERVAL checks.
+   * @param nodes
+   * @param dateText
+   * @param out
+   */
+  static void DCH_from_char(FormatNode[] nodes, String dateText, TmFromChar out) {
+    int	len;
+    AtomicInteger value = new AtomicInteger();
+    boolean	fx_mode = false;
+
+    char[] chars = dateText.toCharArray();
+    int charIdx = 0;
+    int nodeIdx = 0;
+    for (; nodeIdx < nodes.length; nodeIdx++) {
+      FormatNode node = nodes[nodeIdx];
+      if (node.type == NODE_TYPE_END || charIdx >= chars.length) {
+        break;
+      }
+      if (node.type != NODE_TYPE_ACTION) {
+        charIdx++;
+			  /* Ignore spaces when not in FX (fixed width) mode */
+        if (Character.isSpaceChar(node.character) && !fx_mode) {
+          while (charIdx < chars.length && Character.isSpaceChar(chars[charIdx])) {
+            charIdx++;
+          }
+        }
+        continue;
+      }
+
+      from_char_set_mode(out, node.key.date_mode);
+
+      switch (node.key.idType) {
+        case DCH_FX:
+          fx_mode = true;
+          break;
+        case DCH_A_M:
+        case DCH_P_M:
+        case DCH_a_m:
+        case DCH_p_m:
+          value.set(out.pm);
+          charIdx += from_char_seq_search(value, dateText, charIdx, ampm_strings_long, ALL_UPPER, node.key.len, node);
+          assertOutValue(out.pm, value.get() % 2, node);
+          out.pm = value.get() % 2;
+          out.clock = CLOCK_12_HOUR;
+          break;
+        case DCH_AM:
+        case DCH_PM:
+        case DCH_am:
+        case DCH_pm:
+          value.set(out.pm);
+          charIdx += from_char_seq_search(value, dateText, charIdx, ampm_strings, ALL_UPPER, node.key.len, node);
+          assertOutValue(out.pm, value.get() % 2, node);
+          out.pm = value.get() % 2;
+          out.clock = CLOCK_12_HOUR;
+          break;
+        case DCH_HH:
+        case DCH_HH12:
+          value.set(out.hh);
+          charIdx += from_char_parse_int_len(value, dateText, charIdx, 2, nodes, nodeIdx);
+          out.hh = value.get();
+          out.clock = CLOCK_12_HOUR;
+          charIdx += SKIP_THth(node.suffix);
+          break;
+        case DCH_HH24:
+          value.set(out.hh);
+          charIdx += from_char_parse_int_len(value, dateText, charIdx, 2, nodes, nodeIdx);
+          out.hh = value.get();
+          charIdx += SKIP_THth(node.suffix);
+          break;
+        case DCH_MI:
+          value.set(out.mi);
+          charIdx += from_char_parse_int(value, dateText, charIdx, nodes, nodeIdx);
+          out.mi = value.get();
+          charIdx += SKIP_THth(node.suffix);
+          break;
+        case DCH_SS:
+          value.set(out.ss);
+          charIdx += from_char_parse_int(value, dateText, charIdx, nodes, nodeIdx);
+          out.ss = value.get();
+          charIdx += SKIP_THth(node.suffix);
+          break;
+        case DCH_MS:		/* millisecond */
+          value.set(out.ms);
+          len = from_char_parse_int_len(value, dateText, charIdx, 3, nodes, nodeIdx);
+          charIdx += len;
+          out.ms = value.get();
+          /*
+           * 25 is 0.25 and 250 is 0.25 too; 025 is 0.025 and not 0.25
+           */
+          out.ms *= len == 1 ? 100 :
+              len == 2 ? 10 : 1;
+
+          charIdx += SKIP_THth(node.suffix);
+          break;
+        case DCH_US:		/* microsecond */
+          value.set(out.us);
+          len = from_char_parse_int_len(value, dateText, charIdx, 6, nodes, nodeIdx);
+          charIdx += len;
+          out.us = value.get();
+          out.us *= len == 1 ? 100000 :
+              len == 2 ? 10000 :
+                  len == 3 ? 1000 :
+                      len == 4 ? 100 :
+                          len == 5 ? 10 : 1;
+
+          charIdx += SKIP_THth(node.suffix);
+          break;
+        case DCH_SSSS:
+          value.set(out.ssss);
+          charIdx += from_char_parse_int(value, dateText, charIdx, nodes, nodeIdx);
+          out.ssss = value.get();
+          charIdx += SKIP_THth(node.suffix);
+          break;
+        case DCH_tz:
+        case DCH_TZ:
+          throw new IllegalArgumentException("\"TZ\"/\"tz\" format patterns are not supported in to_date");
+        case DCH_A_D:
+        case DCH_B_C:
+        case DCH_a_d:
+        case DCH_b_c:
+          value.set(out.bc);
+          charIdx += from_char_seq_search(value, dateText, charIdx, adbc_strings_long, ALL_UPPER, node.key.len, node);
+          assertOutValue(out.bc, value.get() % 2, node);
+          out.bc = value.get() % 2;
+          break;
+        case DCH_AD:
+        case DCH_BC:
+        case DCH_ad:
+        case DCH_bc:
+          value.set(out.bc);
+          charIdx += from_char_seq_search(value, dateText, charIdx, adbc_strings, ALL_UPPER, node.key.len, node);
+          assertOutValue(out.bc, value.get() % 2, node);
+          out.bc = value.get() % 2;
+          break;
+        case DCH_MONTH:
+        case DCH_Month:
+        case DCH_month:
+          value.set(out.mm);
+          charIdx += from_char_seq_search(value, dateText, charIdx, months_full, ONE_UPPER, MAX_MONTH_LEN, node);
+          assertOutValue(out.mm, value.get() + 1, node);
+          out.mm = value.get() + 1;
+          break;
+        case DCH_MON:
+        case DCH_Mon:
+        case DCH_mon:
+          value.set(out.mm);
+          charIdx += from_char_seq_search(value, dateText, charIdx, months_short, ONE_UPPER, MAX_MON_LEN, node);
+          assertOutValue(out.mm, value.get() + 1, node);
+          out.mm = value.get() + 1;
+          break;
+        case DCH_MM:
+          value.set(out.mm);
+          charIdx += from_char_parse_int(value, dateText, charIdx, nodes, nodeIdx);
+          out.mm = value.get();
+          charIdx += SKIP_THth(node.suffix);
+          break;
+        case DCH_DAY:
+        case DCH_Day:
+        case DCH_day:
+          value.set(out.d);
+          charIdx += from_char_seq_search(value, dateText, charIdx, days_full, ONE_UPPER, MAX_DAY_LEN, node);
+          assertOutValue(out.d, value.get(), node);
+          out.d = value.get();
+          out.d++;
+          break;
+        case DCH_DY:
+        case DCH_Dy:
+        case DCH_dy:
+          value.set(out.d);
+          charIdx += from_char_seq_search(value, dateText, charIdx, days_full, ONE_UPPER, MAX_DY_LEN, node);
+          assertOutValue(out.d, value.get(), node);
+          out.d = value.get();
+          out.d++;
+          break;
+        case DCH_DDD:
+          value.set(out.ddd);
+          charIdx += from_char_parse_int(value, dateText, charIdx, nodes, nodeIdx);
+          out.ddd = value.get();
+          charIdx += SKIP_THth(node.suffix);
+          break;
+        case DCH_IDDD:
+          value.set(out.ddd);
+          charIdx += from_char_parse_int_len(value, dateText, charIdx, 3, nodes, nodeIdx);
+          out.ddd = value.get();
+          charIdx += SKIP_THth(node.suffix);
+          break;
+        case DCH_DD:
+          value.set(out.dd);
+          charIdx += from_char_parse_int(value, dateText, charIdx, nodes, nodeIdx);
+          out.dd = value.get();
+          charIdx += SKIP_THth(node.suffix);
+          break;
+        case DCH_D:
+          value.set(out.d);
+          charIdx += from_char_parse_int(value, dateText, charIdx, nodes, nodeIdx);
+          out.d = value.get();
+          charIdx += SKIP_THth(node.suffix);
+          break;
+        case DCH_ID:
+          value.set(out.d);
+          charIdx += from_char_parse_int_len(value, dateText, charIdx, 1, nodes, nodeIdx);
+          out.d = value.get();
+				  /* Shift numbering to match Gregorian where Sunday = 1 */
+          if (++out.d > 7) {
+            out.d = 1;
+          }
+          charIdx += SKIP_THth(node.suffix);
+          break;
+        case DCH_WW:
+        case DCH_IW:
+          value.set(out.ww);
+          charIdx += from_char_parse_int(value, dateText, charIdx, nodes, nodeIdx);
+          out.ww = value.get();
+          charIdx += SKIP_THth(node.suffix);
+          break;
+        case DCH_Q:
+          /*
+           * We ignore 'Q' when converting to date because it is unclear
+           * which date in the quarter to use, and some people specify
+           * both quarter and month, so if it was honored it might
+           * conflict with the supplied month. That is also why we don't
+           * throw an error.
+           *
+           * We still parse the source string for an integer, but it
+           * isn't stored anywhere in 'out'.
+           */
+          charIdx += from_char_parse_int(null, dateText, charIdx, nodes, nodeIdx);
+          charIdx += SKIP_THth(node.suffix);
+          break;
+        case DCH_CC:
+          value.set(out.cc);
+          charIdx += from_char_parse_int(value, dateText, charIdx, nodes, nodeIdx);
+          out.cc = value.get();
+          charIdx += SKIP_THth(node.suffix);
+          break;
+        case DCH_Y_YYY: {
+            int commaIndex = dateText.indexOf(",", charIdx);
+            if (commaIndex <= 0) {
+              throw new IllegalArgumentException("invalid input string for \"Y,YYY\"");
+            }
+            int millenia = Integer.parseInt(dateText.substring(charIdx, commaIndex));
+            int years = Integer.parseInt(dateText.substring(commaIndex + 1, commaIndex + 1 + 3));
+            years += (millenia * 1000);
+            assertOutValue(out.year, years, node);
+            out.year = years;
+            out.yysz = 4;
+            charIdx += strdigits_len(dateText, charIdx) + 4 + SKIP_THth(node.suffix);
+          }
+          break;
+        case DCH_YYYY:
+        case DCH_IYYY:
+          value.set(out.year);
+          charIdx += from_char_parse_int(value, dateText, charIdx, nodes, nodeIdx);
+          out.year = value.get();
+          out.yysz = 4;
+          charIdx += SKIP_THth(node.suffix);
+          break;
+        case DCH_YYY:
+        case DCH_IYY: {
+            int retVal = from_char_parse_int(value, dateText, charIdx, nodes, nodeIdx);
+            charIdx += retVal;
+            out.year = value.get();
+            if (retVal < 4) {
+              out.year = adjust_partial_year_to_2020(out.year);
+            }
+            out.yysz = 3;
+            charIdx += SKIP_THth(node.suffix);
+            }
+          break;
+        case DCH_YY:
+        case DCH_IY: {
+            value.set(out.year);
+            int retVal = from_char_parse_int(value, dateText, charIdx, nodes, nodeIdx);
+            charIdx += retVal;
+            out.year = value.get();
+            if (retVal < 4) {
+              out.year = adjust_partial_year_to_2020(out.year);
+            }
+            out.yysz = 2;
+            charIdx += SKIP_THth(node.suffix);
+          }
+          break;
+        case DCH_Y:
+        case DCH_I:
+          value.set(out.year);
+          int retVal = from_char_parse_int(value, dateText, charIdx, nodes, nodeIdx);
+          charIdx += retVal;
+          out.year = value.get();
+          if (retVal < 4) {
+            out.year = adjust_partial_year_to_2020(out.year);
+          }
+          out.yysz = 1;
+          charIdx += SKIP_THth(node.suffix);
+          break;
+        case DCH_RM:
+          value.set(out.mm);
+          charIdx += from_char_seq_search(value, dateText, charIdx, rm_months_upper, ALL_UPPER, MAX_RM_LEN, node);
+          assertOutValue(out.mm, MONTHS_PER_YEAR - value.get(), node);
+          out.mm = MONTHS_PER_YEAR - value.get();
+          break;
+        case DCH_rm:
+          value.set(out.mm);
+          charIdx += from_char_seq_search(value, dateText, charIdx, rm_months_lower, ALL_LOWER, MAX_RM_LEN, node);
+          assertOutValue(out.mm, MONTHS_PER_YEAR - value.get(), node);
+          out.mm = MONTHS_PER_YEAR - value.get();
+          break;
+        case DCH_W:
+          value.set(out.w);
+          charIdx += from_char_parse_int(value, dateText, charIdx, nodes, nodeIdx);
+          out.w = value.get();
+          charIdx += SKIP_THth(node.suffix);
+          break;
+        case DCH_J:
+          value.set(out.j);
+          charIdx += from_char_parse_int(value, dateText, charIdx, nodes, nodeIdx);
+          out.j = value.get();
+          charIdx += SKIP_THth(node.suffix);
+          break;
+      }
+    }
+  }
+
+  static KeySuffix suff_search(char[] chars, int startIdx, int type) {
+    for (KeySuffix eachSuffix: DCH_suff) {
+      if (eachSuffix.type != type) {
+        continue;
+      }
+
+      if (strncmp(chars, startIdx, eachSuffix.name, eachSuffix.len)) {
+        return eachSuffix;
+      }
+    }
+    return null;
+  }
+
+  /**
+   * Fast sequential search, use index for data selection which
+   * go to seq. cycle (it is very fast for unwanted strings)
+   * (can't be used binary search in format parsing)
+   * @param chars
+   * @param startIdx
+   * @return
+   */
+  static KeyWord index_seq_search(char[] chars, int startIdx) {
+    if (KeyWord_INDEX_FILTER(chars[startIdx]) == 0) {
+      return null;
+    }
+
+    Integer pos = DCH_index.get(chars[startIdx]);
+
+    if (pos != null) {
+      KeyWord keyword = DCH_keywords[pos];
+      do {
+        if (strncmp(chars, startIdx, keyword.name, keyword.len)) {
+          return keyword;
+        }
+        pos++;
+        if (pos >=  DCH_keywords.length) {
+          return null;
+        }
+        keyword = DCH_keywords[pos];
+      } while (chars[startIdx] == keyword.name.charAt(0));
+    }
+    return null;
+  }
+
+  static boolean strncmp(char[] chars, int startIdx, String str, int len) {
+    if (chars.length - startIdx < len) {
+      return false;
+    }
+
+    int index = startIdx;
+    for (int i = 0; i < len; i++, index++) {
+      if (chars[index] != str.charAt(i)) {
+        return false;
+      }
+    }
+
+    return true;
+  }
+
+  static int KeyWord_INDEX_FILTER(char c)	{
+    return ((c) <= ' ' || (c) >= '~' ? 0 : 1);
+  }
+
+  /**
+   * Set the date mode of a from-char conversion.
+   *
+   * Puke if the date mode has already been set, and the caller attempts to set
+   * it to a conflicting mode.
+   * @param tmfc
+   * @param mode
+   */
+  static void from_char_set_mode(TmFromChar tmfc, FromCharDateMode mode) {
+    if (mode != FromCharDateMode.FROM_CHAR_DATE_NONE) {
+      if (tmfc.mode == FromCharDateMode.FROM_CHAR_DATE_NONE) {
+        tmfc.mode = mode;
+      } else if (tmfc.mode != mode) {
+        throw new IllegalArgumentException("invalid combination of date conventions: " +
+                "Do not mix Gregorian and ISO week date " +
+                "conventions in a formatting template.");
+      }
+    }
+  }
+
+  /**
+   * Perform a sequential search in 'array' for text matching the first 'max'
+   * characters of the source string.
+   *
+   * If a match is found, copy the array index of the match into the integer
+   * pointed to by 'dest', advance 'src' to the end of the part of the string
+   * which matched, and return the number of characters consumed.
+   *
+   * If the string doesn't match, throw an error.
+   * @param dest
+   * @param src
+   * @param charIdx
+   * @param array
+   * @param type
+   * @param max
+   * @param node
+   * @return
+   */
+  static int from_char_seq_search(AtomicInteger dest, String src, int charIdx, String[] array, int type, int max,
+                       FormatNode node) {
+    AtomicInteger len = new AtomicInteger(0);
+
+    dest.set(seq_search(src, charIdx, array, type, max, len));
+    if (len.get() <= 0) {
+      String copy;
+      if (charIdx + node.key.len >= src.length()) {
+        copy = src.substring(charIdx, charIdx + node.key.len);
+      } else {
+        copy = src.substring(charIdx);
+      }
+      throw new IllegalArgumentException("Invalid value \"" + copy + "\" for \"" + node.key.name + "\". " +
+          "The given value did not match any of the allowed values for this field.");
+    }
+    return len.get();
+  }
+
+  /**
+   * Sequential search with to upper/lower conversion
+   * @param name
+   * @param charIdx
+   * @param array
+   * @param type
+   * @param max
+   * @param len
+   * @return
+   */
+  static int seq_search(String name, int charIdx, String[] array, int type, int max, AtomicInteger len) {
+    if (name == null || name.length() <= charIdx) {
+      return -1;
+    }
+
+    char[] nameChars = name.toCharArray();
+    char nameChar = nameChars[charIdx];
+
+	  /* set first char */
+    if (type == ONE_UPPER || type == ALL_UPPER) {
+      nameChar = Character.toUpperCase(nameChar);
+    } else if (type == ALL_LOWER) {
+      nameChar = Character.toLowerCase(nameChar);
+    }
+
+    int arrayIndex = 0;
+    for (int last = 0; array[arrayIndex] != null; arrayIndex++) {
+      String arrayStr = array[arrayIndex];
+		  /* comperate first chars */
+      if (nameChar != arrayStr.charAt(0)) {
+        continue;
+      }
+      int arrayStrLen = arrayStr.length();
+      int arrayCharIdx = 1;
+      int nameCharIdx = charIdx + 1;
+
+      for (int idx = 1; ; nameCharIdx++, arrayCharIdx++, idx++) {
+			  // search fragment (max) only
+        if (max != 0 && idx == max) {
+          len.set(idx + 1);   // '\0'
+          return arrayIndex;
+        }
+			  // full size
+        if (arrayCharIdx == arrayStrLen - 1) {
+          len.set(idx + 1);   // '\0'
+          return arrayIndex;
+        }
+        // Not found in array 'a'
+        if (nameCharIdx == nameChars.length - 1) {
+          break;
+        }
+        /*
+         * Convert (but convert new chars only)
+         */
+        nameChar = nameChars[nameCharIdx];
+        if (idx > last) {
+          if (type == ONE_UPPER || type == ALL_LOWER) {
+            nameChar = Character.toLowerCase(nameChar);
+          } else if (type == ALL_UPPER) {
+            nameChar = Character.toUpperCase(nameChar);
+          }
+          last = idx;
+        }
+        if (nameChar != arrayStr.charAt(arrayCharIdx)){
+          break;
+        }
+      }
+    }
+
+    return -1;
+  }
+
+  /**
+   * Read a single integer from the source string, into the int pointed to by
+   * 'dest'. If 'dest' is NULL, the result is discarded.
+   *
+   * In fixed-width mode (the node does not have the FM suffix), consume at most
+   * 'len' characters.  However, any leading whitespace isn't counted in 'len'.
+   *
+   * We use strtol() to recover the integer value from the source string, in
+   * accordance with the given FormatNode.
+   *
+   * If the conversion completes successfully, src will have been advanced to
+   * point at the character immediately following the last character used in the
+   * conversion.
+   *
+   * Return the number of characters consumed.
+   *
+   * Note that from_char_parse_int() provides a more convenient wrapper where
+   * the length of the field is the same as the length of the format keyword (as
+   * with DD and MI).
+   * @param dest
+   * @param src
+   * @param charIdx
+   * @param len
+   * @param nodes
+   * @param nodeIndex
+   * @return
+   */
+  static int from_char_parse_int_len(AtomicInteger dest, String src, int charIdx, int len, FormatNode[] nodes, int nodeIndex) {
+    long result;
+    int	 initCharIdx = charIdx;
+    StringBuilder tempSb = new StringBuilder();
+
+    /*
+     * Skip any whitespace before parsing the integer.
+     */
+    charIdx = strspace_len(src, charIdx);
+
+    int used = src.length() <= charIdx + len ? src.length() - (charIdx + len) : len;
+    if (used <= 0) {
+      used = src.length() - charIdx;
+    }
+    String copy = src.substring(charIdx, charIdx + used);
+
+    if (S_FM(nodes[nodeIndex].suffix) != 0 || is_next_separator(nodes, nodeIndex)) {
+		/*
+		 * This node is in Fill Mode, or the next node is known to be a
+		 * non-digit value, so we just slurp as many characters as we can get.
+		 */
+      result = DateTimeUtil.strtol(src, charIdx, tempSb);
+      charIdx = src.length() - tempSb.length();
+    } else {
+
+      /*
+       * We need to pull exactly the number of characters given in 'len' out
+       * of the string, and convert those.
+       */
+      if (used < len) {
+        throw new IllegalArgumentException("source string too short for \"" + nodes[nodeIndex].key.name + "\" + formatting field");
+      }
+      result = DateTimeUtil.strtol(copy, 0, tempSb);
+      used = copy.length() - tempSb.length();
+
+      if (used > 0 && used < len) {
+        throw new IllegalArgumentException("invalid value \"" + copy + "\" for \"" + nodes[nodeIndex].key.name + "\"." +
+            "Field requires " + len + " characters, but only " + used);
+      }
+      charIdx += used;
+    }
+
+    if (charIdx == initCharIdx) {
+      throw new IllegalArgumentException("invalid value \"" + copy + "\" for \"" + nodes[nodeIndex].key.name + "\"." +
+          "Value must be an integer.");
+    }
+    if (result < Integer.MIN_VALUE || result > Integer.MAX_VALUE) {
+      throw new IllegalArgumentException("value for \"" + nodes[nodeIndex].key.name + "\"" +
+          " in source string is out of range." +
+          "Value must be in the range " + Integer.MIN_VALUE + " to " + Integer.MAX_VALUE + ".");
+    }
+    if (dest != null) {
+      assertOutValue(dest.get(), (int)result, nodes[nodeIndex]);
+      dest.set((int)result);
+    }
+    return charIdx - initCharIdx;
+  }
+
+  /**
+   * Call from_char_parse_int_len(), using the length of the format keyword as
+   * the expected length of the field.
+   *
+   * Don't call this function if the field differs in length from the format
+   * keyword (as with HH24; the keyword length is 4, but the field length is 2).
+   * In such cases, call from_char_parse_int_len() instead to specify the
+   * required length explicitly.
+   * @param dest
+   * @param src
+   * @param charIdx
+   * @param nodes
+   * @param nodeIdx
+   * @return
+   */
+  static int from_char_parse_int(AtomicInteger dest, String src, int charIdx, FormatNode[] nodes, int nodeIdx) {
+    return from_char_parse_int_len(dest, src, charIdx, nodes[nodeIdx].key.len, nodes, nodeIdx);
+  }
+
+  static int strspace_len(String str, int charIdx) {
+    int len = str.length();
+    while (charIdx < len && Character.isSpaceChar(str.charAt(charIdx))) {
+      charIdx++;
+    }
+    return charIdx;
+  }
+
+  static int strdigits_len(String str, int charIdx) {
+    int len = strspace_len(str, charIdx);
+    int index = charIdx + len;
+
+    int strLen = str.length();
+
+    while (index < strLen && Character.isDigit(str.charAt(index)) && len <= DCH_MAX_ITEM_SIZ) {
+      len++;
+      index++;
+    }
+    return len;
+  }
+
+  static void assertOutValue(int dest, int value, FormatNode node) {
+    if (dest != 0 && dest != value) {
+      throw new IllegalArgumentException(
+          "conflicting values for \"" + node.key.name + "\" field in formatting string," +
+          "This value contradicts a previous setting for the same field type(" + dest + "," + value + ")");
+    }
+  }
+
+  /**
+   * Return true if next format picture is not digit value
+   * @param nodes
+   * @param nodeIndex
+   * @return
+   */
+  static boolean is_next_separator(FormatNode[] nodes, int nodeIndex) {
+    int index = nodeIndex;
+    if (nodes[index].type == NODE_TYPE_END) {
+      return false;
+    }
+
+    if (nodes[index].type == NODE_TYPE_ACTION && S_THth(nodes[index].suffix) != 0) {
+      return true;
+    }
+
+    /*
+     * Next node
+     */
+    index++;
+
+	/* end of format string is treated like a non-digit separator */
+    if (nodes[index].type == NODE_TYPE_END) {
+      return true;
+    }
+
+    if (nodes[index].type == NODE_TYPE_ACTION) {
+      return nodes[index].key.is_digit ? false : true;
+    } else if (Character.isDigit(nodes[index].character)) {
+      return false;
+    }
+
+    return true;				/* some non-digit input (separator) */
+  }
+
+  /**
+   * Adjust all dates toward 2020; this is effectively what happens when we
+   * assume '70' is 1970 and '69' is 2069.
+   * @param year
+   * @return
+   */
+  static int adjust_partial_year_to_2020(int year) {
+    if (year < 70) {
+      /* Force 0-69 into the 2000's */
+      return year + 2000;
+    } else if (year < 100) {
+      /* Force 70-99 into the 1900's */
+      return year + 1900;
+    } else if (year < 520) {
+      /* Force 100-519 into the 2000's */
+      return year + 2000;
+    } else if (year < 1000) {
+      /* Force 520-999 into the 1000's */
+      return year + 1000;
+    } else {
+      return year;
+    }
+  }
+
+  /**
+   * Converts TimeMeta to a string using given the format pattern text.
+   * @param tm
+   * @param formatText
+   * @return
+   */
+  public static String to_char(TimeMeta tm, String formatText) {
+    int fmt_len = formatText.length();
+
+    StringBuilder out = new StringBuilder();
+    if (fmt_len > 0) {
+      FormatNode[] formatNodes;
+
+      synchronized(formatNodeCache) {
+        formatNodes = formatNodeCache.get(formatText);
+      }
+
+      if (formatNodes == null) {
+        formatNodes = new FormatNode[fmt_len + 1];
+        for (int i = 0; i < formatNodes.length; i++) {
+          formatNodes[i] = new FormatNode();
+        }
+        parseFormat(formatNodes, formatText, FORMAT_TYPE.DCH_TYPE);
+        formatNodes[fmt_len].type = NODE_TYPE_END;	/* Paranoia? */
+
+        synchronized(formatNodeCache) {
+          formatNodeCache.put(formatText, formatNodes);
+        }
+      }
+      DCH_to_char(formatNodes, false, tm, out);
+      return out.toString();
+    } else {
+      throw new IllegalArgumentException("No format text.");
+    }
+  }
+
+  /**
+   * Process a TmToChar struct as denoted by a list of FormatNodes.
+   * The formatted data is written to the string pointed to by 'out'.
+   * @param nodes
+   * @param isInterval
+   * @param tm
+   * @param out
+   */
+  private static void DCH_to_char(FormatNode[] nodes, boolean isInterval, TimeMeta tm, StringBuilder out) {
+    int i;
+    for (FormatNode node: nodes) {
+      if (node.type == NODE_TYPE_END) {
+        break;
+      }
+      if (node.type != NODE_TYPE_ACTION) {
+        out.append(node.character);
+        continue;
+      }
+
+      switch (node.key.idType) {
+        case DCH_A_M:
+        case DCH_P_M:
+          out.append((tm.hours % HOURS_PER_DAY >= HOURS_PER_DAY / 2) ? P_M_STR : A_M_STR);
+          break;
+        case DCH_AM:
+        case DCH_PM:
+          out.append((tm.hours % HOURS_PER_DAY >= HOURS_PER_DAY / 2) ? PM_STR : AM_STR);
+          break;
+        case DCH_a_m:
+        case DCH_p_m:
+          out.append((tm.hours % HOURS_PER_DAY >= HOURS_PER_DAY / 2) ? p_m_STR : a_m_STR);
+          break;
+        case DCH_am:
+        case DCH_pm:
+          out.append((tm.hours % HOURS_PER_DAY >= HOURS_PER_DAY / 2) ? pm_STR : am_STR);
+          break;
+        case DCH_HH:
+        case DCH_HH12: {
+          /*
+           * display time as shown on a 12-hour clock, even for
+           * intervals
+           */
+          String formatStr = (S_FM(node.suffix) != 0 ? "%d" : "%02d");
+          out.append(String.format(formatStr,
+              tm.hours % (HOURS_PER_DAY / 2) == 0 ? HOURS_PER_DAY / 2 : tm.hours % (HOURS_PER_DAY / 2)));
+          if (S_THth(node.suffix) != 0) {
+            str_numth(out, out, S_TH_TYPE(node.suffix));
+          }
+          break;
+        }
+        case DCH_HH24: {
+          String formatStr = (S_FM(node.suffix) != 0 ? "%d" : "%02d");
+          out.append(String.format(formatStr, tm.hours));
+          if (S_THth(node.suffix) != 0) {
+            str_numth(out, out, S_TH_TYPE(node.suffix));
+          }
+          break;
+        }
+        case DCH_MI: {
+          String formatStr = (S_FM(node.suffix) != 0 ? "%d" : "%02d");
+          out.append(String.format(formatStr, tm.minutes));
+          if (S_THth(node.suffix) != 0) {
+            str_numth(out, out, S_TH_TYPE(node.suffix));
+          }
+          break;
+        }
+        case DCH_SS: {
+          String formatStr = (S_FM(node.suffix) != 0 ? "%d" : "%02d");
+          out.append(String.format(formatStr, tm.secs));
+          if (S_THth(node.suffix) != 0) {
+            str_numth(out, out, S_TH_TYPE(node.suffix));
+          }
+          break;
+        }
+        case DCH_MS:		/* millisecond */
+          out.append(String.format("%03d", (int) (tm.fsecs / 1000.0)));
+          if (S_THth(node.suffix) != 0) {
+            str_numth(out, out, S_TH_TYPE(node.suffix));
+          }
+          break;
+        case DCH_US:		/* microsecond */
+          out.append(String.format("%06d", (int) tm.fsecs));
+          if (S_THth(node.suffix) != 0) {
+            str_numth(out, out, S_TH_TYPE(node.suffix));
+          }
+          break;
+        case DCH_SSSS:
+          out.append(String.format("%d", tm.hours * DateTimeConstants.SECS_PER_HOUR +
+              tm.minutes * DateTimeConstants.SECS_PER_MINUTE + tm.secs));
+          if (S_THth(node.suffix) != 0) {
+            str_numth(out, out, S_TH_TYPE(node.suffix));
+          }
+          break;
+        case DCH_tz:
+          invalidForInterval(isInterval, node);
+          //TODO
+          break;
+        case DCH_A_D:
+        case DCH_B_C:
+          invalidForInterval(isInterval, node);
+          out.append((tm.years <= 0 ? B_C_STR : A_D_STR));
+          break;
+        case DCH_AD:
+        case DCH_BC:
+          invalidForInterval(isInterval, node);
+          out.append((tm.years <= 0 ? BC_STR : AD_STR));
+          break;
+        case DCH_a_d:
+        case DCH_b_c:
+          invalidForInterval(isInterval, node);
+          out.append((tm.years <= 0 ? b_c_STR : a_d_STR));
+          break;
+        case DCH_ad:
+        case DCH_bc:
+          invalidForInterval(isInterval, node);
+          out.append((tm.years <= 0 ? bc_STR : ad_STR));
+          break;
+        case DCH_MONTH:
+          invalidForInterval(isInterval, node);
+          if (tm.monthOfYear == 0) {
+            break;
+          }
+          if (S_TM(node.suffix) != 0) {
+            out.append(months_full[tm.monthOfYear - 1].toUpperCase());
+          } else {
+            String formatStr =(S_FM(node.suffix) != 0 ? "%0d": "%-09d");
+            out.append(String.format(formatStr, months_full[tm.monthOfYear - 1].toUpperCase()));
+          }
+          break;
+        case DCH_Month:
+          invalidForInterval(isInterval, node);
+          if (tm.monthOfYear == 0) {
+            break;
+          }
+          if (S_TM(node.suffix) != 0) {
+            out.append(months_full[tm.monthOfYear - 1]);
+          } else {
+            String formatStr = (S_FM(node.suffix) != 0 ? "%s": "%-9s");
+            out.append(String.format(formatStr, months_full[tm.monthOfYear - 1]));
+          }
+          break;
+        case DCH_month:
+          invalidForInterval(isInterval, node);
+          if (tm.monthOfYear == 0) {
+            break;
+          }
+          if (S_TM(node.suffix) != 0) {
+            out.append(months_full[tm.monthOfYear - 1].toLowerCase());
+          } else {
+            String formatStr = (S_FM(node.suffix) != 0 ? "%s": "%-9s");
+            out.append(String.format(formatStr, months_full[tm.monthOfYear - 1].toLowerCase()));
+          }
+          break;
+        case DCH_MON:
+          invalidForInterval(isInterval, node);
+          if (tm.monthOfYear == 0) {
+            break;
+          }
+          if (S_TM(node.suffix) != 0) {
+            out.append(months_short[tm.monthOfYear - 1].toUpperCase());
+          } else {
+            out.append(months_short[tm.monthOfYear - 1].toUpperCase());
+          }
+          break;
+        case DCH_Mon:
+          invalidForInterval(isInterval, node);
+          if (tm.monthOfYear == 0) {
+            break;
+          }
+          if (S_TM(node.suffix) != 0) {
+            out.append(months_short[tm.monthOfYear - 1]);
+          } else {
+            out.append(months_short[tm.monthOfYear - 1]);
+          }
+          break;
+        case DCH_mon:
+          invalidForInterval(isInterval, node);
+          if (tm.monthOfYear == 0)
+            break;
+          if (S_TM(node.suffix) != 0) {
+            out.append(months_short[tm.monthOfYear - 1].toLowerCase());
+          } else {
+            out.append(months_short[tm.monthOfYear - 1].toLowerCase());
+          }
+          break;
+        case DCH_MM: {
+          String formatStr = (S_FM(node.suffix) != 0 ? "%d" : "%02d");
+          out.append(String.format(formatStr, tm.monthOfYear));
+          if (S_THth(node.suffix) != 0) {
+            str_numth(out, out, S_TH_TYPE(node.suffix));
+          }
+          break;
+        }
+        case DCH_DAY: {
+          invalidForInterval(isInterval, node);
+          if (S_TM(node.suffix) != 0) {
+            out.append(days_full[tm.getDayOfWeek()].toUpperCase());
+          } else {
+            String formatStr = (S_FM(node.suffix) != 0 ? "%s" : "%-9s");
+            out.append(String.format(formatStr, days_full[tm.getDayOfWeek()].toUpperCase()));
+          }
+          break;
+        }
+        case DCH_Day:
+          invalidForInterval(isInterval, node);
+          if (S_TM(node.suffix) != 0) {
+            out.append(days_full[tm.getDayOfWeek()]);
+          } else {
+            String formatStr = (S_FM(node.suffix) != 0 ? "%s" : "%-9s");
+            out.append(String.format(formatStr, days_full[tm.getDayOfWeek()]));
+          }
+          break;
+        case DCH_day:
+          invalidForInterval(isInterval, node);
+          if (S_TM(node.suffix) != 0) {
+            out.append(days_full[tm.getDayOfWeek()].toLowerCase());
+          } else {
+            String formatStr = (S_FM(node.suffix) != 0 ? "%s" : "%-9s");
+            out.append(String.format(formatStr, days_full[tm.getDayOfWeek()].toLowerCase()));
+          }
+          break;
+        case DCH_DY:
+          invalidForInterval(isInterval, node);
+          if (S_TM(node.suffix) != 0) {
+            out.append(days_short[tm.getDayOfWeek()]);
+          } else {
+            out.append(days_short[tm.getDayOfWeek()]);
+          }
+          break;
+        case DCH_Dy:
+          invalidForInterval(isInterval, node);
+          if (S_TM(node.suffix) != 0) {
+            out.append(days_short[tm.getDayOfWeek()]);
+          } else {
+            out.append(days_short[tm.getDayOfWeek()]);
+          }
+          break;
+        case DCH_dy:
+          invalidForInterval(isInterval, node);
+          if (S_TM(node.suffix) != 0) {
+            out.append(days_short[tm.getDayOfWeek()]);
+          } else {
+            out.append(days_short[tm.getDayOfWeek()]);
+          }
+          break;
+        case DCH_DDD:
+        case DCH_IDDD: {
+          String formatStr = (S_FM(node.suffix) != 0 ? "%0d" : "%03d");
+          out.append(String.format(formatStr,
+              (node.key.idType == DCH_poz.DCH_DDD) ?
+                  tm.getDayOfYear() : DateTimeUtil.date2isoyearday(tm.years, tm.monthOfYear, tm.dayOfMonth)
+          ));
+          if (S_THth(node.suffix) != 0) {
+            str_numth(out, out, S_TH_TYPE(node.suffix));
+          }
+          break;
+        }
+        case DCH_DD: {
+          String formatStr = (S_FM(node.suffix) != 0 ? "%d" : "%02d");
+          out.append(String.format(formatStr, tm.dayOfMonth));
+          if (S_THth(node.suffix) != 0) {
+            str_numth(out, out, S_TH_TYPE(node.suffix));
+          }
+          break;
+        }
+        case DCH_D:
+          invalidForInterval(isInterval, node);
+          out.append(String.format("%d", tm.getDayOfWeek() + 1));
+          if (S_THth(node.suffix) != 0) {
+            str_numth(out, out, S_TH_TYPE(node.suffix));
+          }
+          break;
+        case DCH_ID:
+          invalidForInterval(isInterval, node);
+          out.append(String.format("%d", (tm.getDayOfWeek() == 0) ? 7 : tm.getDayOfWeek()));
+          if (S_THth(node.suffix) != 0) {
+            str_numth(out, out, S_TH_TYPE(node.suffix));
+          }
+          break;
+        case DCH_WW: {
+          String formatStr = (S_FM(node.suffix) != 0 ? "%0d" : "%02d");
+          out.append(String.format(formatStr, (tm.getDayOfYear() - 1) / 7 + 1));
+          if (S_THth(node.suffix) != 0) {
+            str_numth(out, out, S_TH_TYPE(node.suffix));
+          }
+          break;
+        }
+        case DCH_IW: {
+          String formatStr = (S_FM(node.suffix) != 0 ? "%0d" : "%02d");
+          out.append(String.format(formatStr,
+              DateTimeUtil.date2isoweek(tm.years, tm.monthOfYear, tm.dayOfMonth)));
+          if (S_THth(node.suffix) != 0) {
+            str_numth(out, out, S_TH_TYPE(node.suffix));
+          }
+          break;
+        }
+        case DCH_Q:
+          if (tm.monthOfYear == 0) {
+            break;
+          }
+          out.append(String.format("%d", (tm.monthOfYear - 1) / 3 + 1));
+          if (S_THth(node.suffix) != 0) {
+            str_numth(out, out, S_TH_TYPE(node.suffix));
+          }
+          break;
+        case DCH_CC: {
+          if (isInterval) { /* straight calculation */
+            i = tm.years / 100;
+          } else {
+            if (tm.years > 0) {
+						/* Century 20 == 1901 - 2000 */
+              i = (tm.years - 1) / 100 + 1;
+            } else {
+						/* Century 6BC == 600BC - 501BC */
+              i = tm.years / 100 - 1;
+            }
+          }
+          if (i <= 99 && i >= -99) {
+            String formatStr = (S_FM(node.suffix) != 0 ? "%0d" : "%02d");
+            out.append(String.format(formatStr, i));
+          } else {
+            out.append(String.format("%d", i));
+          }
+          if (S_THth(node.suffix) != 0) {
+            str_numth(out, out, S_TH_TYPE(node.suffix));
+          }
+          break;
+        }
+        case DCH_Y_YYY:
+          i = ADJUST_YEAR(tm.years, isInterval) / 1000;
+          out.append(String.format("%d,%03d", i, ADJUST_YEAR(tm.years, isInterval) - (i * 1000)));
+          if (S_THth(node.suffix) != 0) {
+            str_numth(out, out, S_TH_TYPE(node.suffix));
+          }
+          break;
+        case DCH_YYYY:
+        case DCH_IYYY: {
+          String formatStr = (S_FM(node.suffix) != 0 ? "%d" : "%04d");
+          out.append(String.format(formatStr,
+              (node.key.idType == DCH_poz.DCH_YYYY ? ADJUST_YEAR(tm.years, isInterval) :
+                  ADJUST_YEAR(DateTimeUtil.date2isoyear(tm.years, tm.monthOfYear, tm.dayOfMonth), isInterval))
+          ));
+          if (S_THth(node.suffix) != 0) {
+            str_numth(out, out, S_TH_TYPE(node.suffix));
+          }
+          break;
+        }
+        case DCH_YYY:
+        case DCH_IYY: {
+          String formatStr = (S_FM(node.suffix) != 0 ? "%0d" : "%03d");
+          out.append(String.format(formatStr,
+              (node.key.idType == DCH_poz.DCH_YYY ? ADJUST_YEAR(tm.years, isInterval) :
+                  ADJUST_YEAR(DateTimeUtil.date2isoyear(tm.years, tm.monthOfYear, tm.dayOfMonth), isInterval)) % 1000
+          ));
+          if (S_THth(node.suffix) != 0) {
+            str_numth(out, out, S_TH_TYPE(node.suffix));
+          }
+          break;
+        }
+        case DCH_YY:
+        case DCH_IY: {
+          String formatStr = (S_FM(node.suffix) != 0 ? "%0d" : "%02d");
+          out.append(String.format(formatStr,
+              (node.key.idType == DCH_poz.DCH_YY ? ADJUST_YEAR(tm.years, isInterval) :
+                  ADJUST_YEAR(DateTimeUtil.date2isoyear(tm.years, tm.monthOfYear, tm.dayOfMonth), isInterval)) % 100
+          ));
+          if (S_THth(node.suffix) != 0)
+            str_numth(out, out, S_TH_TYPE(node.suffix));
+          break;
+        }
+        case DCH_Y:
+        case DCH_I:
+          out.append(String.format("%1d",
+              (node.key.idType == DCH_poz.DCH_Y ?
+                  ADJUST_YEAR(tm.years, isInterval) :
+                  ADJUST_YEAR(DateTimeUtil.date2isoyear(tm.years, tm.monthOfYear, tm.dayOfMonth),
+                      isInterval)) % 10));
+          if (S_THth(node.suffix) != 0) {
+            str_numth(out, out, S_TH_TYPE(node.suffix));
+          }
+          break;
+        case DCH_RM: {
+          if (tm.monthOfYear == 0) {
+            break;
+          }
+          String formatStr = (S_FM(node.suffix) != 0 ? "%s" : "%-4s");
+          out.append(String.format(formatStr,
+              rm_months_upper[MONTHS_PER_YEAR - tm.monthOfYear]));
+          break;
+        }
+        case DCH_rm: {
+          if (tm.monthOfYear == 0) {
+            break;
+          }
+          String formatStr = (S_FM(node.suffix) != 0 ? "%s" : "%-4s");
+          out.append(String.format(formatStr, rm_months_lower[MONTHS_PER_YEAR - tm.monthOfYear]));
+          break;
+        }
+        case DCH_W:
+          out.append(String.format("%d", (tm.dayOfMonth - 1) / 7 + 1));
+          if (S_THth(node.suffix) != 0) {
+            str_numth(out, out, S_TH_TYPE(node.suffix));
+          }
+          break;
+        case DCH_J:
+          out.append(String.format("%d", DateTimeUtil.date2j(tm.years, tm.monthOfYear, tm.dayOfMonth)));
+          if (S_THth(node.suffix) != 0) {
+            str_numth(out, out, S_TH_TYPE(node.suffix));
+          }
+          break;
+      }
+    }
+  }
+
+  /**
+   * Return ST/ND/RD/TH for simple (1..9) numbers
+   * type --> 0 upper, 1 lower
+   * @param num
+   * @param type
+   * @return
+   */
+  static String get_th(StringBuilder num, int type) {
+    int	len = num.length();
+    char last = num.charAt(len - 1);
+
+    if (!Character.isDigit(last)) {
+      throw new IllegalArgumentException("\"" + num.toString() + "\" is not a number");
+    }
+
+    /*
+     * All "teens" (<x>1[0-9]) get 'TH/th', while <x>[02-9][123] still get
+     * 'ST/st', 'ND/nd', 'RD/rd', respectively
+     */
+    char seclast;
+    if ((len > 1) && ((seclast = num.charAt(len - 2)) == '1')) {
+      last = 0;
+    }
+
+    switch (last) {
+      case '1':
+        if (type == TH_UPPER)
+          return numTH[0];
+        return numth[0];
+      case '2':
+        if (type == TH_UPPER)
+          return numTH[1];
+        return numth[1];
+      case '3':
+        if (type == TH_UPPER)
+          return numTH[2];
+        return numth[2];
+      default:
+        if (type == TH_UPPER)
+          return numTH[3];
+        return numth[3];
+    }
+  }
+
+  /**
+   * Convert string-number to ordinal string-number
+   * type --> 0 upper, 1 lower
+   * @param dest
+   * @param num
+   * @param type
+   */
+  static void str_numth(StringBuilder dest, StringBuilder num, int type) {
+    if (!dest.equals(num)) {
+      dest.append(num);
+    }
+    dest.append(get_th(num, type));
+  }
+
+  private static void invalidForInterval(boolean isInterval, FormatNode node) {
+    if (isInterval) {
+      throw new IllegalArgumentException("\"" + node.key.name + "\" not support for interval");
+    }
+  }
+}