Return-Path: X-Original-To: apmail-tajo-commits-archive@minotaur.apache.org Delivered-To: apmail-tajo-commits-archive@minotaur.apache.org Received: from mail.apache.org (hermes.apache.org [140.211.11.3]) by minotaur.apache.org (Postfix) with SMTP id 50BC1111D7 for ; Thu, 22 May 2014 01:33:38 +0000 (UTC) Received: (qmail 3297 invoked by uid 500); 22 May 2014 01:33:38 -0000 Delivered-To: apmail-tajo-commits-archive@tajo.apache.org Received: (qmail 3225 invoked by uid 500); 22 May 2014 01:33:38 -0000 Mailing-List: contact commits-help@tajo.apache.org; run by ezmlm Precedence: bulk List-Help: List-Unsubscribe: List-Post: List-Id: Reply-To: dev@tajo.apache.org Delivered-To: mailing list commits@tajo.apache.org Received: (qmail 3104 invoked by uid 99); 22 May 2014 01:33:38 -0000 Received: from tyr.zones.apache.org (HELO tyr.zones.apache.org) (140.211.11.114) by apache.org (qpsmtpd/0.29) with ESMTP; Thu, 22 May 2014 01:33:38 +0000 Received: by tyr.zones.apache.org (Postfix, from userid 65534) id E006499B818; Thu, 22 May 2014 01:33:37 +0000 (UTC) Content-Type: text/plain; charset="us-ascii" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit From: jihoonson@apache.org To: commits@tajo.apache.org Date: Thu, 22 May 2014 01:33:40 -0000 Message-Id: <5e3c2d1c49184adf86decd957819258e@git.apache.org> In-Reply-To: References: X-Mailer: ASF-Git Admin Mailer Subject: [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 DCH_index = new HashMap(); + + 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 formatNodeCache = new HashMap(); + + /** + * ---------- + * 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" (1[0-9]) get 'TH/th', while [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"); + } + } +}