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

org.apache.myfaces.dateformat.SimpleDateFormatter Maven / Gradle / Ivy

/*
 * 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.myfaces.dateformat;

import java.util.Date;
import java.util.LinkedList;
import java.util.List;

/**
 * A reimplementation of the java.text.SimpleDateFormat class.
 * 

* This class has been created for use with the tomahawk InputCalendar * component. It exists for the following reasons: *

    *
  • The java.text.SimpleDateFormat class is simply broken with respect * to "week of year" functionality. *
  • The inputCalendar needs a javascript equivalent of SimpleDateFormat * in order to process data in the popup calendar. But it is hard to * unit-test javascript code. By maintaining a version in Java that is * unit-tested, then making the javascript version a direct "port" of that * code the javascript gets improved reliability. *
  • Documentation is necessary for this code, but it is not desirable to * add lots of docs to a javascript file that is downloaded. The javascript * version can simply reference the documentation here. *
* Note that the JODA project also provides a SimpleDateFormat implementation, * but that does not support firstDayOfWeek functionality. In any case, * it is not desirable to add a dependency from tomahawk on JODA just for * the InputCalendar. *

* This implementation does extend the SimpleDateFormat class by adding the * JODA "xxxx" yearOfWeekYear format option, as this is missing in the * standard SimpleDateFormat class. *

* The parse methods also return null on error rather than throw an exception. *

* The code here was originally written in javascript (date.js), and has been * ported to java. *

* At the current time, the following format options are NOT supported: * DFkKSzZ. *

*

Week Based Calendars

*

* ISO standard ISO-8601 defines a calendaring system based not upon * year/month/day_in_month but instead year/week/day_in_week. This is * particularly popular in embedded systems as date arithmetic is * much simpler; there are no irregular month lengths to handle. *

* The only tricky part is mapping to and from year/month/day formats. * Unfortunately, while java.text.SimpleDateFormat does support a "ww" * week format character, it has a number of flaws. *

* Weeks are always complete and discrete, ie week yyyy-ww always has * 7 days in it, and never "shares" days with yyyy-(ww+1). However to * achieve this, the last week of a year might include a few days of * the next year, or the last few days of a year might be counted as * part of the first week of the following year. The decision is made * depending on which year the "majority" of days in that week belong to. *

* With ISO-8601, a week always starts on a monday. However many countries * use a different convention, starting weeks on saturday, sunday or monday. * This class supports setting the firstDayOfWeek. * * @since 1.1.7 * @author Simon Kitching (latest modification by $Author: grantsmith $) * @version $Revision: 472638 $ $Date: 2006-11-08 15:54:13 -0500 (Wed, 08 Nov 2006) $ */ public class SimpleDateFormatter { private static final long MSECS_PER_SEC = 1000; private static final long MSECS_PER_MIN = 60 * MSECS_PER_SEC; private static final long MSECS_PER_HOUR = 60 * MSECS_PER_MIN; private static final long MSECS_PER_DAY = 24 * MSECS_PER_HOUR; private static final long MSECS_PER_WEEK = 7 * MSECS_PER_DAY; // ====================================================================== // Static Week-handling Methods // ====================================================================== /** * Cumulative sum of the number of days in the year up to the first * day of each month. */ private static final int[] MONTH_LEN = { 0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334 }; /** * Return the ISO week# represented by the specified date (1..53). * * This implements the ISO-8601 standard for week numbering, as documented in * Klaus Tondering's Calendar document, version 2.8: * http://www.tondering.dk/claus/calendar.html * * For dates in January and February, calculate: * * a = year-1 * b = a/4 - a/100 + a/400 * c = (a-1)/4 - (a-1)/100 + (a-1)/400 * s = b-c * e = 0 * f = day - 1 + 31*(month-1) * * For dates in March through December, calculate: * * a = year * b = a/4 - a/100 + a/400 * c = (a-1)/4 - (a-1)/100 + (a-1)/400 * s = b-c * e = s+1 * f = day + (153*(month-3)+2)/5 + 58 + s * * Then, for any month continue thus: * * g = (a + b) mod 7 * d = (f + g - e) mod 7 * n = f + 3 - d * * We now have three situations: * * If n<0, the day lies in week 53-(g-s)/5 of the previous year. * If n>364+s, the day lies in week 1 of the coming year. * Otherwise, the day lies in week n/7 + 1 of the current year. * * This algorithm gives you a couple of additional useful values: * * d indicates the day of the week (0=Monday, 1=Tuesday, etc.) * f+1 is the ordinal number of the date within the current year. * * Note that ISO-8601 specifies that week1 of a year is the first week in * which the majority of days lie in that year. An equivalent description * is that it is the first week including the 4th of january. This means * that the 1st, 2nd and 3rd of January might lie in the last week of the * previous year, and that the last week of a year may include the first * few days of the following year. * * ISO-8601 also specifies that the first day of the week is always Monday. * * This function returns the week number regardless of which year it lies in. * That means that asking for the week# of 01/01/yyyy might return 52 or 53, * and asking for the week# of 31/12/yyyy might return 1. */ public static WeekDate getIsoWeekDate(Date date) { int year = fullYearFromDate(date.getYear()); int month = date.getMonth() + 1; int day = date.getDate(); int a,b,c,d,e,f,g,s,n; if (month <= 2) { a = year - 1; b = (int) Math.floor(a/4) - (int) Math.floor(a/100) + (int) Math.floor(a/400); c = (int) Math.floor((a-1)/4) - (int) Math.floor((a-1)/100) + (int) Math.floor((a-1)/400); s = b - c; e = 0; f = day - 1 + 31*(month-1); } else { a = year; b = (int) Math.floor(a/4) - (int) Math.floor(a/100) + (int) Math.floor(a/400); c = (int) Math.floor((a-1)/4) - (int) Math.floor((a-1)/100) + (int) Math.floor((a-1)/400); s = b - c; e = s + 1; f = day + (int) Math.floor((153*(month-3) + 2)/5) + 58 + s; } g = (a + b) % 7; d = (f + g - e) % 7; n = f + 3 - d; if (n<0) { // previous year int resultWeek = 53 - (int) Math.floor((g-s)/5); return new WeekDate(year-1, resultWeek); } else if (n > (364+s)) { // next year int resultWeek = 1; return new WeekDate(year+1, resultWeek); } else { // current year int resultWeek = (int) Math.floor(n/7) + 1; return new WeekDate(year, resultWeek); } } /** Return true if the specified year is a leapyear (has 29 days in feb). */ private static boolean isLeapYear(int year) { return ((year%4 == 0) && (year%100 != 0)) || (year%400 == 0); } /** * Compute which day of the week (sun,mon, etc) a particular date * falls on. *

* Returns 0 for sunday, 1 for monday, 6 for saturday (the java.util.Date * and the javascript Date convention): *

* Note that java.util.Calendar uses 1=sun, 7=sat. *

* This algorithm is documented as part of the RFC3339 specification. * * @param year is full year value (eg 2007). * @param month is 1..12 * @param day is 1..31 */ private static int dayOfWeek(int year, int month, int day) { /* adjust months so February is the last one */ month -= 2; if (month < 1) { month += 12; --year; } /* split by century */ int cent = year / 100; year %= 100; // dow (0=sunday) int base = (26 * month - 2) / 10 + day + year + (year / 4) + (cent / 4) + (5 * cent); int dow = base % 7; return dow; } /** * Return the (year, week) representation of the given date. *

* This is exactly like getIsoWeekNumber, except that a firstDayOfWeek * can be specified; ISO-8601 hard-wires "monday" as first day of week. *

* TODO: support minimumDaysInWeek property. Currently, assumes * this is set to 4 (the ISO standard). *

* @param firstDayOfWeek is: 0=sunday, 1=monday, 6=sat. This is the * convention used by java.util.Date. NOTE: java.util.Calendar uses * 1=sunday, 2=monday, 7=saturday. */ public static WeekDate getWeekDate(Date date, int firstDayOfWeek) { int year = fullYearFromDate(date.getYear()); int month = date.getMonth() + 1; int day = date.getDate(); boolean thisIsLeapYear = isLeapYear(year); int dayOfYear = day + MONTH_LEN[month-1]; if (thisIsLeapYear && (month>2)) { ++dayOfYear; } int jan1Weekday = dayOfWeek(year, 1, 1); // The first week of a year always starts on firstDayOfWeek. However that // week starts up to 3 days before the 1st of the year, or 3 days after. // // Here, we find where the first week actually starts, measured as an // offset from the first day of the year (-3..+3). // // Examples: // * if firstDayOfWeek=mon, and 1st jan is wed, then pivotOffset=-2, // ie 30 dec of previous year is where the first week starts. // * if firstDayOfWeek=sun and 1st jan is fri, then pivotOffset=2, // ie 3 jan is where the first week starts. int pivotOffset = firstDayOfWeek - jan1Weekday; if (pivotOffset > 3) { pivotOffset -= 7; } else if (pivotOffset < -3) { pivotOffset += 7; } // Compute the offset of date relative to the start of this year. // This will be in range 0..364 (or 365 for leap year) int dayOffset = dayOfYear-1; if (dayOffset < pivotOffset) { // This date falls in either week52 or week53 of the previous year // // Because (365%7)=1, the pivotOffset moves forweards by one if the previous // year is a normal one, or two if the previous year is a leapyear (wrapping // around from +3 to -3). And a year has 53 weeks only when its pivotOffset // is -3 (or -2 for leapyear). // // so: // when prev is not leapyear, has53 when pivotOffset is 3 for this year. // when prev is leapyear, has53 when pivotOffset is 2 or 3 for this year. boolean prevIsLeapYear = isLeapYear(year-1); if ((pivotOffset==3) || ((pivotOffset==2) && prevIsLeapYear)) { return new WeekDate(year-1, 53); } return new WeekDate(year-1, 52); } // Compute the number of days relative to the start of the first // week in this year, then divide by seven to get the week count. int daysFromFirstWeekStart = (dayOfYear - 1 - pivotOffset); int weekNumber = daysFromFirstWeekStart/7 + 1; // In a normal year, there are 52 weeks with 1 day (365%7) left over. // // So, when weeks start on the first day of a year, there is one day left // at the end, which will fall into the first week of the next year. When // weeks start on the 2nd, then week 52 ends on 31 dec. When weeks start on // the max pivotOffset of +3, then week52 includes 3jan of next year. It is // still week52 because only 3 days are from the next year adn 4 are in the // current year. // // But when pivotOffset is -3, then there are 4 days left over at the end of // the year - making week 53. And in a leap year, pivotOffset=-2 is sufficient // to create a week53. if ((weekNumber < 53) || (pivotOffset==-3) || (pivotOffset==-2 && thisIsLeapYear)) { return new WeekDate(year, weekNumber); } else { // weekNumber=53, but this year only has 52 weeks so this must be week // one of the next year. return new WeekDate(year+1, 1); } } /** * Return the point in time at which the first week of the specified year starts. */ private static long getStartOfWeekYear(int year, int firstDayOfWeek) { // Create a new date on the 1st. Date d1 = new Date(shortYearFromDate(year), 0, 1, 0, 0, 0); // adjust forward or backwards to the nearest firstDayOfWeek int firstDayOfYear = d1.getDay(); // 0 = sunday int dayDiff = firstDayOfWeek - firstDayOfYear; int dayShift; if (dayDiff >= 4) { dayShift = 7-dayDiff; } else if (dayDiff >= 0) { dayShift = dayDiff; } else if (dayDiff >= -3) { dayShift = dayDiff; } else { dayShift = 7 + dayDiff; } // now compute the number of weeks between start of weekYear and input date. long weekYearStartMsecs = d1.getTime() + (dayShift* MSECS_PER_DAY); return weekYearStartMsecs; } /** * This is the inverse of method getJavaWeekNumber. */ private static Date getDateForWeekDate ( int year, int week, int day, int hour, int min, int sec, int firstDayOfWeek) { long msecsBase = getStartOfWeekYear(year, firstDayOfWeek); long msecsOffset = (week - 1) * MSECS_PER_WEEK; msecsOffset += (day-1) * MSECS_PER_DAY; msecsOffset += hour * MSECS_PER_HOUR; msecsOffset += min * MSECS_PER_MIN; msecsOffset += sec * MSECS_PER_SEC; Date finalDate = new Date(); finalDate.setTime(msecsBase + msecsOffset); return finalDate; } // ====================================================================== // Static Generic Date Manipulation Methods // ====================================================================== private static int fullYearFromDate(int year) { if (year < 1900) { return year + 1900; } else { return year; } } private static int shortYearFromDate(int year) { if (year > 1900) { return year - 1900; } else { return year; } } private static Date createDateFromContext(ParserContext context) { Date date; if (context.weekOfWeekYear != 0) { date = getDateForWeekDate( context.weekYear, context.weekOfWeekYear, context.day, context.hour, context.min, context.sec, context.firstDayOfWeek); } else { // Class java.util.Date expects year to be relative to 1900. Note that // this is different for javascript Date class - that takes a year // relative to 0AD. date = new Date( context.year - 1900, context.month, context.day, context.hour, context.min, context.sec); } return date; } /** * Return a substring starting from a specific location, and extending * len characters. *

* It is an error if s is null. * It is an error if s.length <= start. *

* It is NOT an error if s.length < start+len; in this case a string * starting at "start" but less than len characters will be returned. */ private static String substr(String s, int start, int len) { String s2 = s.substring(start); if (s2.length() <= len) return s2; else return s2.substring(0, len); } // ====================================================================== // Static Parsing Methods // ====================================================================== /** * Parse a string according to the provided sequence of parsing ops. *

* Returns a ParserContext object that has its year/month/day etc fields * set according to data extracted from the string. *

* If an error has occured during parsing, context.invalid will be true. */ private static ParserContext parseOps( DateFormatSymbols symbols, boolean yearIsWeekYear, int firstDayOfWeek, String[] ops, String dateStr) { ParserContext context = new ParserContext(firstDayOfWeek); int dateIndex = 0; int dateStrLen = dateStr.length(); for(int i=0; (i= 4) { String fragment = dateStr.substring(dateIndex); int index = parsePrefixOf(context, symbols.months, fragment); if (index != -1) { context.month = index; } } else { context.month = parseNum(context, dateStr, 2, dateIndex) - 1; } } else if (c == 'd') { context.day = parseNum(context, dateStr, 2, dateIndex); } else if (c == 'E') { if (patlen <= 3) { String fragment = dateStr.substring(dateIndex, dateIndex+3); int index = parseIndexOf(context, symbols.shortWeekdays, fragment); if (index != -1) { context.dayOfWeek = index; } } else { String fragment = dateStr.substring(dateIndex); int index = parsePrefixOf(context, symbols.weekdays, fragment); if (index != -1) { context.dayOfWeek = index; } } } else if (c == 'H') { // H is in range 0..23 context.hour = parseNum(context, dateStr, 2, dateIndex); } else if (c == 'h') { // h is in range 1am..12pm or 1pm-12am. // Note that this field is later post-adjusted context.hourAmpm = parseNum(context, dateStr, 2, dateIndex); } else if (c == 'm') { context.min = parseNum(context, dateStr, 2, dateIndex); } else if (c == 's') { context.sec = parseNum(context, dateStr, 2, dateIndex); } else if (c == 'a') { context.ampm = parseString(context, dateStr, dateIndex, symbols.ampms); } else if (c == 'w') { context.weekOfWeekYear = parseNum(context, dateStr, 2, dateIndex); } else { context.invalid = true; } } /** * Convert a string of digits (in base 10) to an integer. *

* Only positive values are accepted. Returns -1 on failure. */ private static int parseInt(String value) { int sum = 0; for(int i=0; i< value.length(); ++i) { char c = value.charAt(i); if ((c<'0') || (c>'9')) { return -1; } sum = sum*10 + (c-'0'); } return sum; } /** * Convert at most the next nChars characters to numeric, starting from offset dateIndex * within dateStr. *

* Updates context.newIndex to contain the offset of the next unparsed char. */ private static int parseNum(ParserContext context, String dateStr, int nChars, int dateIndex) { // Try to convert the most possible characters (nChars). If that fails, // then try again without the last character. Repeat until successful // numeric conversion occurs. int nToParse = Math.min(nChars, dateStr.length() - dateIndex); for(int i=nToParse;i>0;i--) { String numStr = dateStr.substring(dateIndex,dateIndex+i); int value = parseInt(numStr); if(value == -1) continue; context.newIndex = dateIndex+i; return value; } context.newIndex = -1; context.invalid = true; return -1; } /** * Return the index of the array element which matches the provided string. *

* This is used when the next thing in value (string being parsed) is expected * to be one of the values in the provided array, AND all the array entries * are of the same length. The appropriate sequence of chars can then be * extracted from the string to parse, and passed here as the exact value * to be matched. */ private static int parseIndexOf(ParserContext context, String[] array, String value) { for(int i=0; i * This is used when the next thing in value (string being parsed) is expected * to be one of the values in the provided array. *

* This is like indexOf, except that an exact match is not expected. */ private static int parsePrefixOf(ParserContext context, String[] array, String value) { for(int i=0; i * Returns an index into the strings array, or -1 if none match. *

* Also updates context.newIndex to be the location after the matched string (if any). * On failure, the context.invalid flag is set before returning -1. */ private static int parseString(ParserContext context, String dateStr, int dateIndex, String[] strings) { String fragment = dateStr.substring(dateIndex); return parsePrefixOf(context, strings, fragment); } /** * Handle fields that need to be processed after all information is available. */ private static void parsePostProcess(DateFormatSymbols symbols, ParserContext context) { if (context.ambiguousYear) { // TODO: maybe this adjustment could be made while parsing? context.year += 1900; Date date = createDateFromContext(context); Date threshold = symbols.twoDigitYearStart; if (date.getTime() < threshold.getTime()) { context.year += 100; } } if (context.hourAmpm > 0) { // yes, the user has set the hour using 12-hour clock // 01am->01, 11am->11, 12pm->12, 1pm->13, 11pm->23, 12pm->00 if (context.ampm == 1) { context.hour = context.hourAmpm + 12; if (context.hour == 24) context.hour = 0; } else { context.hour = context.hourAmpm; } } } // ====================================================================== // Static Formatting Methods // ====================================================================== private static String formatOps( DateFormatSymbols symbols, boolean yearIsWeekYear, int firstDayOfWeek, String[] ops, Date date) { ParserContext context = new ParserContext(firstDayOfWeek); context.year = fullYearFromDate(date.getYear()); context.month = date.getMonth(); context.day = date.getDate(); context.dayOfWeek = date.getDay(); context.hour = date.getHours(); context.min = date.getMinutes(); context.sec = date.getSeconds(); // 00 --> 12am, 01->1am, 12 --> 12pm, 13 -> 1pm, 23->11pm context.ampm = (context.hour < 12) ? 0 : 1; WeekDate weekDate = getWeekDate(date, firstDayOfWeek); context.weekYear = weekDate.getYear(); context.weekOfWeekYear = weekDate.getWeek(); StringBuffer str = new StringBuffer(); for(int i=0; i= 4) { out.append(symbols.months[context.month]); } else { formatNum(context.month+1, patlen, false, out); } } else if (c == 'd') { formatNum(context.day, patlen, false, out); } else if (c == 'E') { if (patlen <= 3) { out.append(symbols.shortWeekdays[context.dayOfWeek]); } else { out.append(symbols.weekdays[context.dayOfWeek]); } } else if (c == 'H') { // output hour in range 0..23 formatNum(context.hour, patlen, false, out); } else if (c == 'h') { // output hour in range 1..12: // 00 --> 12am, 01->1am, 12 --> 12pm, 13 -> 1pm, 23->11pm int hour = context.hour; if (hour == 0) { hour = 12; // 12am } else if (hour > 12) { hour = hour - 12; } formatNum(hour, patlen, false, out); } else if (c == 'm') { formatNum(context.min, patlen, false, out); } else if (c == 's') { formatNum(context.sec, patlen, false, out); } else if (c == 'a') { out.append(symbols.ampms[context.ampm]); } else if (c == 'w') { formatNum(context.weekOfWeekYear, patlen, false, out); } else { context.invalid = true; } } /** * Write out an integer padded with leading zeros to a specified width. *

* If ensureLength is set, and the number is longer than length, then display only the * rightmost length digits. */ private static void formatNum(int num, int length, boolean ensureLength, StringBuffer out) { String str = String.valueOf(num); while (str.length() < length) { str = "0" + str; } // XXX do we have to distinguish left and right 'cutting' //ensureLength - enable cutting only for parameters like the year, the other if (ensureLength && str.length() > length) { str = str.substring(str.length() - length); } out.append(str); } // ====================================================================== // Pattern Processing Methods // ====================================================================== /** * Given a date parsing or formatting pattern, split it up into an * array of separate pieces to be processed. *

* Each piece is either: *

    *
  • a "format" section *
  • a "quote" section, *
  • a "literal" section, or *
*

* A format section is a sequence of 1 or more identical alphabetical * characters, eg "yyyy", "MMM" or "dd". When parsing, this indicates what * data is expected next; if it is not a recognised sequence then it is * just ignored. When formatting, this indicates which part of the provided * date object should be output, and how to format it; if it is not a * recognised sequence then it is simply written literally to the output. *

* A quote section is something in the pattern that was enclosed in quote * marks. When parsing, quote sections are expected to be present in exactly * the same form in the input string; an error is reported if the data is * not present. When formatting, quote sections are output literally as * they occurred in the pattern. *

* A literal section is a sequence of 1 or more non-quoted non-alphabetical * characters, eg "-" or "+++". When parsing, literal sections just cause * the same number of characters in the input stream to be skipped. When * formatting, they are just output literally. *

* The elements of the string array returned are of form "f:xxxx" (format * section), "q:text" (quote section), or "l:-" (literal section). *

* TODO: when formatting, should literal chars really just cause skipping? */ private static String[] analysePattern(String pattern) { int patternIndex = 0; int patternLen = pattern.length(); char lastChar = 0; StringBuffer patternSub = null; boolean quoteMode = false; List ops = new LinkedList(); while (patternIndex < patternLen) { char currentChar = pattern.charAt(patternIndex); char nextChar; if (patternIndex < patternLen - 1) { nextChar = pattern.charAt(patternIndex + 1); } else { nextChar = 0; } if (currentChar == '\'' && lastChar != '\\') { if (patternSub != null) { ops.add(patternSub.toString()); patternSub = null; } quoteMode = !quoteMode; } else if (quoteMode) { if (patternSub == null) { patternSub = new StringBuffer("q:"); } patternSub.append(currentChar); } else { if (currentChar == '\\' && lastChar != '\\') { // do nothing } else { if (patternSub == null) { if (Character.isLetter(currentChar)) { patternSub = new StringBuffer("f:"); } else { patternSub = new StringBuffer("l:"); } } patternSub.append(currentChar); if (currentChar != nextChar) { ops.add(patternSub.toString()); patternSub = null; } } } patternIndex++; lastChar = currentChar; } if (patternSub != null) { ops.add(patternSub.toString()); } String[] data = new String[ops.size()]; return (String[]) ops.toArray(data); } /** * Determine whether to make the "yyyy" pattern behave in a non-standard manner. *

* The java.text.SimpleDateFormat class has no option to output the "weekyear" * property, ie the year in which the "ww" value occurs. This makes the "ww" * formatter basically useless. *

* This class therefore implements the JODA "xxxx" formatter that does exactly * that. However many people will use "ww/yyyy" patterns without realising that * this generates garbage (eg 01/2000 when it should output 01/2001 because the * week has rolled over from one year to the next). This therefore checks whether * ww is present in the pattern string, and if so makes yy work like xx. Of * course this does not allow patterns like "xxxx-ww yyyy-MM-dd", so we then * disable this hack if "xx" is also present. */ private static boolean hasWeekPattern(String[] ops) { boolean wwPresent = false; boolean xxPresent = false; for(int i=0; i





© 2015 - 2024 Weber Informatics LLC | Privacy Policy