You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@myfaces.apache.org by sk...@apache.org on 2007/12/20 12:31:14 UTC

svn commit: r605891 - in /myfaces/tomahawk/trunk/core/src: main/java/org/apache/myfaces/dateformat/ test/java/org/apache/myfaces/dateformat/

Author: skitching
Date: Thu Dec 20 03:31:08 2007
New Revision: 605891

URL: http://svn.apache.org/viewvc?rev=605891&view=rev
Log:
Add SimpleDateFormatter class, backported from date.js to java. 
Enhance it with better week-handling.
Add unit tests, etc.

Added:
    myfaces/tomahawk/trunk/core/src/main/java/org/apache/myfaces/dateformat/
    myfaces/tomahawk/trunk/core/src/main/java/org/apache/myfaces/dateformat/DateFormatSymbols.java
    myfaces/tomahawk/trunk/core/src/main/java/org/apache/myfaces/dateformat/ParserContext.java
    myfaces/tomahawk/trunk/core/src/main/java/org/apache/myfaces/dateformat/SimpleDateFormatter.java
    myfaces/tomahawk/trunk/core/src/test/java/org/apache/myfaces/dateformat/
    myfaces/tomahawk/trunk/core/src/test/java/org/apache/myfaces/dateformat/TestSimpleDateFormatter.java

Added: myfaces/tomahawk/trunk/core/src/main/java/org/apache/myfaces/dateformat/DateFormatSymbols.java
URL: http://svn.apache.org/viewvc/myfaces/tomahawk/trunk/core/src/main/java/org/apache/myfaces/dateformat/DateFormatSymbols.java?rev=605891&view=auto
==============================================================================
--- myfaces/tomahawk/trunk/core/src/main/java/org/apache/myfaces/dateformat/DateFormatSymbols.java (added)
+++ myfaces/tomahawk/trunk/core/src/main/java/org/apache/myfaces/dateformat/DateFormatSymbols.java Thu Dec 20 03:31:08 2007
@@ -0,0 +1,54 @@
+package org.apache.myfaces.dateformat;
+
+import java.util.Date;
+
+/**
+ * A simple class that contains locale-specific constants used for date
+ * parsing and formatting.
+ * <p>
+ * An instance of this can be created, and the symbols modified before
+ * passing it to a SimpleDateFormatter. This allows date formatting and
+ * parsing to be localised.
+ */
+public class DateFormatSymbols
+{
+	String[] eras = {"BC", "AD"};
+
+	String[] months = {
+			"January", "February", "March", "April",
+            "May", "June", "July", "August", "September", "October",
+            "November", "December", "Undecimber"
+    };
+
+	String[] shortMonths = {
+			"Jan", "Feb", "Mar", "Apr",
+            "May", "Jun", "Jul", "Aug", "Sep", "Oct",
+            "Nov", "Dec", "Und"
+    };
+
+	String[] weekdays = {
+			"Sunday", "Monday", "Tuesday",
+            "Wednesday", "Thursday", "Friday", "Saturday"
+    };
+
+	String[] shortWeekdays = {
+			"Sun", "Mon", "Tue",
+            "Wed", "Thu", "Fri", "Sat"
+    };
+
+	String[] ampms = { "AM", "PM" };
+
+	String[] zoneStrings = {
+			null, "long-name", "short-name"
+	};
+
+	Date threshold;
+	Date twoDigitYearStart;
+
+	public DateFormatSymbols()
+	{
+        threshold = new Date();
+        threshold.setYear(threshold.getYear()-80);
+        this.twoDigitYearStart = threshold;
+	}
+}

Added: myfaces/tomahawk/trunk/core/src/main/java/org/apache/myfaces/dateformat/ParserContext.java
URL: http://svn.apache.org/viewvc/myfaces/tomahawk/trunk/core/src/main/java/org/apache/myfaces/dateformat/ParserContext.java?rev=605891&view=auto
==============================================================================
--- myfaces/tomahawk/trunk/core/src/main/java/org/apache/myfaces/dateformat/ParserContext.java (added)
+++ myfaces/tomahawk/trunk/core/src/main/java/org/apache/myfaces/dateformat/ParserContext.java Thu Dec 20 03:31:08 2007
@@ -0,0 +1,102 @@
+package org.apache.myfaces.dateformat;
+
+/**
+ * A simple object that contains the current parsing state when parsing
+ * a string into a date.
+ * <p>
+ * This encapsulates all the properties of a SimpleDateFormatter which
+ * are modified during the parsing of a specific input string.
+ */
+public class ParserContext
+{
+	/**
+	 * Set during various string-parsing operations to indicate the
+	 * offset of the next unparsed character.  
+	 */
+	int newIndex;
+
+	/**
+	 * Set during string-parsing operations if a parsing error occurred.
+	 * Normally, an error status is also returned from the method.
+	 */ 
+	boolean invalid = false;
+
+	/**
+	 * Controls how "weekYear" and "weekOfWeekYear" map to ymd dates.
+	 * <p>
+	 * Values are 0=sunday, 1=monday, 6=saturday (the java.util.Date
+	 * return values for getDay). Note that java.util.Calendar uses
+	 * 1=sunday, 2=monday, 7=saturday.
+	 * <p>
+	 * Defaults to monday (the ISO standard).
+	 */
+	int firstDayOfWeek = 1;
+
+	/**
+	 * Set to true if the input string had a year specifier of less
+	 * than 4 digits, meaning we have to guess the century.
+	 */
+	boolean ambiguousYear;
+
+	/**
+	 * Set to true if the input string had a weekYear specifier of less
+	 * than 4 digits, meaning we have to guess the century.
+	 */
+	boolean ambiguousWeekYear;
+
+	// --------------------------------------------------
+	// standard properties parsed out of the input string
+	// --------------------------------------------------
+	
+	/**
+	 * Year is relative to 0AD, unless ambiguousYear is set.
+	 */
+	int year;
+
+	/** Month is in range 0..11 */
+	int month;
+
+	/** Day is in range 1..31 */
+	int day = 1;
+
+	/** mon=1, sun=7 */
+	int dayOfWeek;
+
+	/** The hour value used for formatter "H", in range 00-23. */
+	int hour;
+	
+	/**
+	 * The hour value used for "h" formatter; in range 1..12.
+	 * <ul>
+	 * <li>00:00 is 12:00 am (this field is 12)
+	 * <li>01:00 is 01:00 am (this field is 1)
+	 * <li>11:59 is 11:59 am (this field is 11)
+	 * <li>12:00 is 12:00 pm (this field is 12)
+	 * <li>13:00 is 01:00 pm (this field is 1)
+	 * <li>23:59 is 11:59 pm (this field is 11)
+	 * </ul>
+	 */
+	int hourAmpm;
+
+	/** minutes in range 0-59 */
+	int min;
+	
+	/** seconds in range 0-59 */
+	int sec;
+	
+	/** 0 = am, 1 = pm */
+	int ampm;
+
+	/**
+	 * The year in which the weekOfWeekYear value lies.
+	 * Note that date yyyy-01-01 may be week 5n of the previous year.
+	 */
+	int weekYear;
+	
+	/** The week number (1..53). */
+	int weekOfWeekYear;
+
+	public ParserContext(int dow) {
+		firstDayOfWeek = dow;
+	}
+}

Added: myfaces/tomahawk/trunk/core/src/main/java/org/apache/myfaces/dateformat/SimpleDateFormatter.java
URL: http://svn.apache.org/viewvc/myfaces/tomahawk/trunk/core/src/main/java/org/apache/myfaces/dateformat/SimpleDateFormatter.java?rev=605891&view=auto
==============================================================================
--- myfaces/tomahawk/trunk/core/src/main/java/org/apache/myfaces/dateformat/SimpleDateFormatter.java (added)
+++ myfaces/tomahawk/trunk/core/src/main/java/org/apache/myfaces/dateformat/SimpleDateFormatter.java Thu Dec 20 03:31:08 2007
@@ -0,0 +1,1158 @@
+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.
+ * <p>
+ * This class has been created for use with the tomahawk InputCalendar
+ * component. It exists for the following reasons:
+ * <ul>
+ * <li>The java.text.SimpleDateFormat class is simply broken with respect
+ * to "week of year" functionality.
+ * <li>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.
+ * <li>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.
+ * </ul>
+ * 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.
+ * <p>
+ * This implementation does extend the SimpleDateFormat class by adding the
+ * JODA "xxxx" yearOfWeekYear format option, as this is missing in the
+ * standard SimpleDateFormat class.
+ * <p>
+ * The parse methods also return null on error rather than throw an exception.
+ * <p>
+ * The code here was originally written in javascript (date.js), and has been
+ * ported to java.
+ * <p>
+ * At the current time, the following format options are NOT supported:
+ * <code>DFkKSzZ</code>.
+ */
+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
+    // ======================================================================
+
+    public static class WeekDate
+    {
+        int year, week;
+        public WeekDate(int y, int w)
+        {
+            year = y;
+            week = w;
+        }
+    }
+
+    /**
+     * 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 getIsoWeekNumber(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.
+     * <p>
+     * Returns 0 for sunday, 1 for monday, 6 for saturday (the java.util.Date
+     * and the javascript Date convention):
+     * <p>
+     * Note that java.util.Calendar uses 1=sun, 7=sat.
+     * <p>
+     * This algorithm is documented as part of the RFC3339 specification.
+     */
+    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.
+     * <p>
+     * This is exactly like getIsoWeekNumber, except that a firstDayOfWeek
+     * can be specified; ISO-8601 hard-wires "monday" as first day of week.
+     * <p>
+     * TODO: support minimumDaysInWeek property. Currently, assumes
+     * this is set to 4 (the ISO standard).
+     * <p>
+     * @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 getJavaWeekNumber(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. Use 4am, not midnight, in order to
+        // avoid any problems with leap seconds, rounding errors, etc.
+        Date d1 = new Date(shortYearFromDate(year), 0, 1, 4, 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.
+     */
+    public static Date newJavaDateByWeekYear(
+            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 * 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 = newJavaDateByWeekYear(
+                    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.
+     * <p>
+     * It is an error if s is null.
+     * It is an error if s.length <= start.
+     * <p>
+     * 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.
+     * <p>
+     * Returns a ParserContext object that has its year/month/day etc fields
+     * set according to data extracted from the string.
+     * <p>
+     * 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<ops.length) && (dateIndex < dateStrLen); ++i)
+        {
+            String op = ops[i];
+            String optype = op.substring(0, 2);
+            String opval = op.substring(2);
+
+            if (optype.equals("f:"))
+            {
+                parsePattern(symbols, yearIsWeekYear, context, opval, dateStr, dateIndex);
+
+                if ((context.newIndex < 0) || context.invalid)
+                    break;
+
+                dateIndex = context.newIndex;
+            }
+            else if (optype.equals("q:") || optype.equals("l:"))
+            {
+                // verify that opval matches the next chars in dateStr
+                int oplen = opval.length();
+                String s = substr(dateStr, dateIndex, oplen);
+                if (!opval.equals(s))
+                {
+                    context.invalid = true;
+                    break;
+                }
+                dateIndex += oplen;
+            }
+        }
+
+        return context;
+    }
+
+    /**
+     * Handle parsing of a single property, eg "yyyy" or "EEE".
+     */
+    private static void parsePattern(DateFormatSymbols symbols, boolean yearIsWeekYear, ParserContext context, String patternSub,
+            String dateStr, int dateIndex)
+    {
+
+        char c = patternSub.charAt(0);
+        int patlen = patternSub.length();
+
+        if (c == 'y')
+        {
+            int year = parseNum(context, dateStr, 4, dateIndex);
+            if ((context.newIndex-dateIndex) < 4)
+            {
+                // see method adjustTwoDigitYear
+                context.year = year;
+                context.ambiguousYear = true;
+            }
+            else
+            {
+                context.year = year;
+            }
+
+            if (yearIsWeekYear)
+            {
+                // There is a "ww" pattern present, so set weekYear as well as year.
+                context.weekYear = context.year;
+                context.ambiguousWeekYear = context.ambiguousYear;
+            }
+        }
+        else if (c == 'x')
+        {
+            // extension to standard java.text.SimpleDateFormat class, to support the
+            // JODA "weekYear" formatter.
+            int year = parseNum(context, dateStr, 4, dateIndex);
+
+            if ((context.newIndex-dateIndex) < 4)
+            {
+                context.weekYear = year;
+                context.ambiguousWeekYear = true;
+            }
+            else
+            {
+                context.weekYear = year;
+            }
+        }
+        else if (c == 'M')
+        {
+            if (patlen == 3)
+            {
+                String fragment = substr(dateStr, dateIndex, 3);
+                int index = parseIndexOf(context, symbols.shortMonths, fragment);
+                if (index != -1)
+                {
+                    context.month = index;
+                }
+            }
+            else if (patlen >= 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.
+     * <p>
+     * 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.
+     * <p>
+     * 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.
+     * <p>
+     * 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<array.length; ++i)
+        {
+            String s = array[i];
+            if (value.equals(s))
+            {
+                context.newIndex += s.length();
+                return i;
+            }
+        }
+        context.invalid = true;
+        context.newIndex = -1;
+        return -1;
+    }
+
+    /**
+     * Return the index of the array element which is a prefix of the value string.
+     * <p>
+     * This is used when the next thing in value (string being parsed) is expected
+     * to be one of the values in the provided array.
+     * <p>
+     * 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<array.length; ++i)
+        {
+            String s = array[i];
+            if (value.startsWith(s))
+            {
+                context.newIndex += s.length();
+                return i;
+            }
+        }
+        context.invalid = true;
+        context.newIndex = -1;
+        return -1;
+    }
+
+    /**
+     * This is used when parsing is currently at location dateIndex within the date string,
+     * and one of the values in the strings array is now expected.
+     * <p>
+     * Returns an index into the strings array, or -1 if none match.
+     * <p>
+     * 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?
+            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 = getJavaWeekNumber(date, firstDayOfWeek);
+        context.weekYear = weekDate.year;
+        context.weekOfWeekYear = weekDate.week;
+
+        StringBuffer str = new StringBuffer();
+        for(int i=0; i<ops.length; ++i)
+        {
+            String op = ops[i];
+            String optype = op.substring(0, 2);
+            String opval = op.substring(2);
+
+            if (optype.equals("f:"))
+            {
+                formatPattern(symbols, context, opval, yearIsWeekYear, str);
+                if (context.invalid)
+                    break;
+            }
+            else if (optype.equals("l:"))
+            {
+                // Just copy the literal sequence
+                str.append(opval);
+            }
+            else if (optype.equals("q:"))
+            {
+                // Just copy the literal sequence
+                str.append(opval);
+            }
+        }
+
+        if (context.invalid)
+            return null;
+        else
+            return str.toString();
+    }
+
+    private static void formatPattern(DateFormatSymbols symbols, ParserContext context, String patternSub, boolean yearIsWeekYear, StringBuffer out)
+    {
+        if ((patternSub == null) || (patternSub.isEmpty()))
+        {
+            return;
+        }
+
+        char c = patternSub.charAt(0);
+        int patlen = patternSub.length();
+
+        if (c == 'y')
+        {
+            if (!yearIsWeekYear)
+                formatNum(context.year, patlen <= 3 ? 2 : 4, true, out);
+            else
+                formatNum(context.weekYear, patlen <= 3 ? 2 : 4, true, out);
+        }
+        else if (c == 'x')
+        {
+            formatNum(context.weekYear, patlen <= 3 ? 2 : 4, true, out);
+        }
+        else if (c == 'M')
+        {
+            if (patlen == 3)
+            {
+                out.append(symbols.shortMonths[context.month]);
+            }
+            else if (patlen >= 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.
+     * <p>
+     * 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.
+     * <p>
+     * Each piece is either:
+     * <ul>
+     * <li> a "format" section
+     * <li> a "quote" section,
+     * <li> a "literal" section, or
+     * </ul>
+     * <p>
+     * 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.
+     * <p>
+     * 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.
+     * <p>
+     * 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.
+     * <p>
+     * The elements of the string array returned are of form "f:xxxx" (format
+     * section), "q:text" (quote section), or "l:-" (literal section).
+     * <p>
+     * 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.
+     * <p>
+     * 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.
+     * <p>
+     * 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<ops.length; ++i)
+        {
+            String s = ops[i];
+            wwPresent = wwPresent || s.startsWith("f:ww");
+            xxPresent = xxPresent || s.startsWith("f:xx");
+        }
+
+        return wwPresent && !xxPresent;
+    }
+
+    // ======================================================================
+    // Instance methods
+    // ======================================================================
+
+    private DateFormatSymbols symbols;
+
+    private String[] ops;
+    boolean yearIsWeekYear;
+    int firstDayOfWeek;
+
+    public SimpleDateFormatter(String pattern, DateFormatSymbols symbols)
+    {
+        if (symbols == null)
+        {
+            this.symbols = new DateFormatSymbols();
+        }
+        else
+        {
+            this.symbols = symbols;
+        }
+
+        this.ops = analysePattern(pattern);
+        this.yearIsWeekYear = hasWeekPattern(ops);
+        this.firstDayOfWeek = 1;
+    }
+
+    public void setFirstDayOfWeek(int dow)
+    {
+        this.firstDayOfWeek = dow;
+    }
+
+    public Date parse(String dateStr)
+    {
+        if ((dateStr==null) || dateStr.isEmpty())
+            return null;
+
+        ParserContext context = parseOps(symbols, yearIsWeekYear, firstDayOfWeek, ops, dateStr);
+
+        if (context.invalid)
+        {
+            return null;
+        }
+
+        parsePostProcess(symbols, context);
+        return createDateFromContext(context);
+    }
+
+    public String format(Date date)
+    {
+        return formatOps(symbols, yearIsWeekYear, firstDayOfWeek, ops, date);
+    }
+}

Added: myfaces/tomahawk/trunk/core/src/test/java/org/apache/myfaces/dateformat/TestSimpleDateFormatter.java
URL: http://svn.apache.org/viewvc/myfaces/tomahawk/trunk/core/src/test/java/org/apache/myfaces/dateformat/TestSimpleDateFormatter.java?rev=605891&view=auto
==============================================================================
--- myfaces/tomahawk/trunk/core/src/test/java/org/apache/myfaces/dateformat/TestSimpleDateFormatter.java (added)
+++ myfaces/tomahawk/trunk/core/src/test/java/org/apache/myfaces/dateformat/TestSimpleDateFormatter.java Thu Dec 20 03:31:08 2007
@@ -0,0 +1,443 @@
+package org.apache.myfaces.dateformat;
+
+import java.text.SimpleDateFormat;
+import java.util.Calendar;
+import java.util.Date;
+import java.util.GregorianCalendar;
+import java.util.Locale;
+import java.util.SimpleTimeZone;
+import java.util.TimeZone;
+
+import org.joda.time.DateTime;
+
+import junit.framework.TestCase;
+
+public class TestSimpleDateFormatter extends TestCase
+{
+    /**
+     * Define a "callback" interface for a test that can be invoked with various dates.
+     */
+    public interface DateTest
+    {
+        public void test(int year, int month, int day) throws Exception;
+    }
+
+    // test just the very basics of date formatting
+    public void testFormatSimple()
+    {
+        SimpleDateFormatter sdf = new SimpleDateFormatter("yyyy-MM-dd'T'hh:mm:ss", null);
+        Date d = new Date();
+        d.setYear(1987 - 1900);
+        d.setMonth(2);
+        d.setDate(12);
+        d.setHours(13);
+        d.setMinutes(23);
+        d.setSeconds(59);
+        
+        String s = sdf.format(d);
+        assertEquals("1987-03-12T01:23:59", s);
+    }
+
+    // test just the very basics of date parsing
+    public void testParseSimple()
+    {
+        SimpleDateFormatter sdf = new SimpleDateFormatter("yyyy-MM-dd'T'HH:mm:ss", null);
+        Date d = sdf.parse("1987-03-12T04:23:59");
+
+        assertNotNull(d);
+        assertEquals(1987, d.getYear() + 1900);
+        assertEquals(2, d.getMonth());
+        assertEquals(12, d.getDate());
+        assertEquals(4, d.getHours());
+        assertEquals(23, d.getMinutes());
+        assertEquals(59, d.getSeconds());
+    }
+    
+    // test every possible formatter in date formatting
+    public void testFormatAll()
+    {
+        Date d = new Date();
+        d.setYear(1987 - 1900);
+        d.setMonth(2);
+        d.setDate(12);
+        d.setHours(4);
+        d.setMinutes(23);
+        d.setSeconds(59);
+        
+        String[] data =
+        {
+            "yyyy", "1987",
+            "yyy", "87",
+            "yy", "87",
+            "y", "87",
+            "MMMM", "March",
+            "MMM", "Mar",
+            "MM", "03",
+            "M", "3",
+            "dd", "12",
+            "EEEE", "Thursday",
+            "EE", "Thu",
+            "HH", "04",
+            "H", "4",
+            "hh", "04",
+            "h", "4",
+            "mm", "23",
+            "m", "23",
+            "ss", "59",
+            "s", "59",
+            "a", "AM"
+        };
+
+        Locale locale = Locale.ENGLISH;
+        for(int i=0; i<data.length; i+=2)
+        {
+            String pattern = data[i];
+            String expected = data[i+1];
+
+            SimpleDateFormatter sdf = new SimpleDateFormatter(pattern, null);
+            String s = sdf.format(d);
+            assertEquals("custom:" + pattern, expected, s);
+            
+            SimpleDateFormat sf = new SimpleDateFormat(pattern, locale);
+            String s2 = sf.format(d);
+            assertEquals("std:" + pattern, expected, s2);
+        }
+    }
+    
+    // test every possible formatter in date parsing
+    public void testParseAll()
+    {
+        Date d = new Date();
+        d.setYear(1987 - 1900);
+        d.setMonth(2);
+        d.setDate(12);
+        d.setHours(4);
+        d.setMinutes(23);
+        d.setSeconds(59);
+        
+        String[] data =
+        {
+            "yyyy", "1987",
+            "yyy", "87",
+            "yy", "87",
+            "y", "87",
+            "MMMM", "March",
+            "MMM", "Mar",
+            "MM", "03",
+            "M", "3",
+            "dd", "12",
+
+            // These are difficult to test in this way; disable them
+            //"EEEE", "Monday",
+            //"EE", "Mon",
+
+            "HH", "04",
+            "H", "4",
+
+            "hh", "04",
+            "h", "4",
+            "mm", "23",
+            "m", "23",
+            "ss", "59",
+            "s", "59",
+            "a", "AM"
+        };
+
+        Locale locale = Locale.ENGLISH;
+        for(int i=0; i<data.length; i+=2)
+        {
+            String pattern = data[i];
+            String expected = data[i+1];
+
+            // parse it with our code, then format it with the std code and
+            // see if we get the same value back.
+            SimpleDateFormatter sdf = new SimpleDateFormatter(pattern, null);
+        
+            Date d2 = sdf.parse(expected);
+            int year = d2.getYear();
+            SimpleDateFormat sf = new SimpleDateFormat(pattern, locale);
+            String s2 = sf.format(d2);
+            assertEquals(pattern, expected, s2);
+        }
+    }
+
+    // try to parse various combinations, and see what we get
+    public void testParseAssorted() throws Exception {
+        Object[] data =
+        {
+            // test standard, with literals
+            "yyyy-MM-dd", "1987-01-08", new Date(1987-1900, 0, 8),
+            
+            // test standard, with multichar literal sequences: any non-alpha
+            // char must exactly match the input.
+            "yyyy--MM-:()dd", "1987--01-:()08", new Date(1987-1900, 0, 8),
+            
+            // test standard, with quoted chars.
+            "yyyy'T'MM'T'dd", "1987T01T08", new Date(1987-1900, 0, 8),
+            
+            // test standard, with non-pattern chars.
+            // An alpha non-pattern char --> error
+            "yyyyRMMRdd", "1987-01-08", null,
+            
+            // test quoted text
+            "yyyy'year'MM'month'dd", "2003year04month06", new Date(2003-1900, 03, 06),
+            
+            // test mismatched quoted text
+            "yyyy'year'MM'month'dd", "2003yexr04month06", null,
+        };
+
+        Locale locale = Locale.ENGLISH;
+        for(int i=0; i<data.length; i+=3)
+        {
+            String pattern = (String) data[i];
+            String input = (String) data[i+1];
+            Date expected = (Date) data[i+2];
+
+            // parse it with our code, and see if we get the expected result
+            SimpleDateFormatter sdf = new SimpleDateFormatter(pattern, null);
+            Date d = sdf.parse(input);
+            assertEquals("custom:" + pattern, expected, d);
+            
+            // try with the standard parser too
+            try
+            {
+                SimpleDateFormat sf = new SimpleDateFormat(pattern, locale);
+                Date d2 = sf.parse(input);
+                assertEquals("std:" + pattern, expected, d2);
+            }
+            catch(java.text.ParseException e)
+            {
+                // thrown when the input does not match the pattern
+                assertEquals("std:" + pattern, null, expected);
+            }
+            catch(IllegalArgumentException e)
+            {
+                // thrown when the pattern is not value
+                assertEquals("std:" + pattern, null, expected);
+            }
+        }
+    }
+
+    // try to format with various combinations, and see what we get
+    public void testFormatAssorted() throws Exception
+    {
+        Date d = new Date();
+        d.setYear(1987 - 1900);
+        d.setMonth(2);
+        d.setDate(12);
+        d.setHours(4);
+        d.setMinutes(23);
+        d.setSeconds(59);
+        
+        String[] data =
+        {
+            // test standard, with literals
+            "yyyy-MM-dd", "1987-03-12",
+            
+            // test standard, with multichar literal sequences: any non-alpha
+            "yyyy--MM-:()dd", "1987--03-:()12",
+            
+            // test standard, with non-pattern chars.--> error
+            "yyyyTMMTdd", null,
+            
+            // test standard, with non-pattern chars.
+            "yyyy'T'MM'T'dd", "1987T03T12",
+            
+            // test quoted text
+            "yyyy'year'MM'month'dd", "1987year03month12",
+        };
+
+        Locale locale = Locale.ENGLISH;
+        for(int i=0; i<data.length; i+=2)
+        {
+            String pattern = data[i];
+            String expected = data[i+1];
+
+            // format it with our code, and check against expected.
+            SimpleDateFormatter sdf = new SimpleDateFormatter(pattern, null);
+            String s = sdf.format(d);
+            assertEquals("custom:" + pattern, expected, s);
+
+            // try with the standard parser too
+            try
+            {
+                SimpleDateFormat sf = new SimpleDateFormat(pattern, locale);
+                String s2 = sf.format(d);
+                assertEquals("std:" + pattern, expected, s2);
+            }
+            catch(IllegalArgumentException e)
+            {
+                // thrown when the pattern is not value
+                assertEquals("std:" + pattern, null, expected);
+            }
+        }
+    }
+
+    // for a wide range of dates, compare the output of
+    //   SimpleDateFormatter.getIsoWeekNumber
+    //   SimpleDateFormatter.getJavaWeekNumber (with firstOfWeek=monday)
+    //   Joda DateTime class
+    //
+    // Of course this comparison can ONLY be done for firstDayOfWeek=monday.
+    public void testWeekFormatAgainstJoda() throws Exception
+    {
+        applyTests(new DateTest()
+        {
+            public void test(int year, int month, int day)
+            {
+                checkWeekFormatAgainstJoda(year, month, day);
+            }
+        });
+    }
+
+    private void checkWeekFormatAgainstJoda(int year, int month, int day)
+    {
+        Date date = new Date(year-1900, month, day);
+        DateTime jdt = new DateTime(date.getTime());
+        int jodaWeekOfWeekyear = jdt.getWeekOfWeekyear();
+        int jodaWeekyear = jdt.getWeekyear();
+
+        SimpleDateFormatter.WeekDate iwd = SimpleDateFormatter.getIsoWeekNumber(date);
+
+        // the java.util.Date convention is that 1 = monday
+        int firstDayOfWeek = 1;
+        SimpleDateFormatter.WeekDate jwd = SimpleDateFormatter.getJavaWeekNumber(date, firstDayOfWeek);
+
+        String ds = new SimpleDateFormat("yyyy-MM-dd").format(date);
+        System.out.println(
+            ds + ":"
+            + "(" + jodaWeekyear + "-" + jodaWeekOfWeekyear + ")"
+            + ",(" + iwd.year + "-" + iwd.week + ")"
+            + ",(" + jwd.year + "-" + jwd.week + ")"
+            );
+        assertEquals(jodaWeekyear, iwd.year);
+        assertEquals(jodaWeekOfWeekyear, iwd.week);
+        assertEquals(jodaWeekyear, jwd.year);
+        assertEquals(jodaWeekOfWeekyear, jwd.week);
+    }
+
+    /**
+     * Execute the specified test against a range of dates selected to
+     * find problems. The dates around the beginning and end of each year
+     * are verified, plus a few in the middle.
+     */
+    private void applyTests(DateTest dt) throws Exception
+    {
+        // for every year from 2000-2010, test:
+        //   1-8 jan jan
+        //   29,30 may,
+        //   1-8 june
+        //   23-31 dec
+         for(int year = 2000; year < 2020; ++year)
+         {
+             dt.test(year, 0, 1);
+             dt.test(year, 0, 2);
+             dt.test(year, 0, 3);
+             dt.test(year, 0, 4);
+             dt.test(year, 0, 5);
+             dt.test(year, 0, 6);
+             dt.test(year, 0, 7);
+             dt.test(year, 0, 8);
+
+             dt.test(year, 4, 29);
+             dt.test(year, 4, 30);
+             dt.test(year, 5, 1);
+             dt.test(year, 5, 2);
+             dt.test(year, 5, 3);
+             dt.test(year, 5, 4);
+             dt.test(year, 5, 5);
+             dt.test(year, 5, 6);
+             dt.test(year, 5, 7);
+             dt.test(year, 5, 8);
+
+             dt.test(year, 11, 23);
+             dt.test(year, 11, 24);
+             dt.test(year, 11, 25);
+             dt.test(year, 11, 26);
+             dt.test(year, 11, 27);
+             dt.test(year, 11, 28);
+             dt.test(year, 11, 29);
+             dt.test(year, 11, 30);
+             dt.test(year, 11, 31);
+         }
+    }
+
+    public void xtestWeekFormatSimple()
+    {
+        // get the supported ids for UTC
+         String[] ids = TimeZone.getAvailableIDs(0);
+         // if no ids were returned, something is wrong. get out.
+         if (ids.length == 0)
+             System.exit(0);
+
+         // create a Pacific Standard Time time zone
+         SimpleTimeZone pdt = new SimpleTimeZone(0, ids[0]);
+
+         GregorianCalendar calendar = new GregorianCalendar(pdt);
+         calendar.setFirstDayOfWeek(2); // 1=sunday, 2=monday, 7=saturday
+         calendar.setMinimalDaysInFirstWeek(4);
+        
+         for(int year = 2000; year < 2020; ++year)
+         {
+             tryDate(calendar, year, 0, 1);
+             tryDate(calendar, year, 0, 2);
+             tryDate(calendar, year, 0, 3);
+             tryDate(calendar, year, 0, 4);
+             tryDate(calendar, year, 0, 5);
+             tryDate(calendar, year, 0, 6);
+             tryDate(calendar, year, 0, 7);
+             tryDate(calendar, year, 0, 8);
+
+             tryDate(calendar, year, 5, 1);
+
+             tryDate(calendar, year, 11, 23);
+             tryDate(calendar, year, 11, 24);
+             tryDate(calendar, year, 11, 25);
+             tryDate(calendar, year, 11, 26);
+             tryDate(calendar, year, 11, 27);
+             tryDate(calendar, year, 11, 28);
+             tryDate(calendar, year, 11, 29);
+             tryDate(calendar, year, 11, 30);
+             tryDate(calendar, year, 11, 31);
+         }
+    }
+    
+    private void tryDate(GregorianCalendar cal, int year, int month, int day)
+    {
+        cal.set(year, month, day);
+        Date date = cal.getTime();
+
+        int javaWeekOfYear = cal.get(Calendar.WEEK_OF_YEAR);
+
+        // aargh, JODA does not support firstDayOfWeek functionality; it only
+        // supports the ISO standard which is first-day=monday
+        DateTime jdt = new DateTime(date.getTime());
+        int jodaWeekOfYear = jdt.getWeekOfWeekyear();
+
+        // aargh, Date.getDay uses 0=sun,1=mon,6=sat
+        // but Calendar.getFirstDayOfWeek uses 1=sun, 2=mon, 7=sat
+        int firstDayOfWeek = cal.getFirstDayOfWeek() - 1;
+        SimpleDateFormatter.WeekDate wd = SimpleDateFormatter.getJavaWeekNumber(date, firstDayOfWeek);
+
+        String ds = new SimpleDateFormat("yyyy-MM-dd").format(date);
+        System.out.println(ds + ":" + javaWeekOfYear + ":" + jodaWeekOfYear + ":" + wd.week);
+        // assertEquals(javaWeekOfYear, myWeekOfYear);
+    }
+
+    // test sanity of java.text.Calendar: convert a date to "ww/yyyy" format, then
+    // parse it back again and ensure the year is correct.
+    private void tryDate3(GregorianCalendar cal, int year, int month, int day) throws Exception
+    {
+        SimpleDateFormat df = new SimpleDateFormat("yyyy-ww-E");
+
+        Date d = new Date(year - 1900, month, day);
+        String s1 = df.format(d);
+        Date d2 = df.parse(s1);
+
+        SimpleDateFormat df2 = new SimpleDateFormat("yyyy/MM/dd");
+        String dstr = df2.format(d);
+        String d2str =df2.format(d2);
+        
+        System.out.println(dstr + ":" + s1 + ":" + d2str + ":");
+    }
+}