Looks like I'm ending up spending as much time on date parsing as Martin...
Parsing of MMM and MMMM starts to work now, too. Additionally I looked into parsing and formatting of yy vs. yyyy.
Regards, Arvid
/* * Copyright 2004 The Apache Software Foundation. * * Licensed 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. */ var DateFormatSymbols = Class.create(); DateFormatSymbols.prototype = { initialize: function() { this.eras = new Array('BC', 'AD'); this.months = new Array('January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December', 'Undecimber'); this.shortMonths = new Array('Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec', 'Und'); this.weekdays = new Array('Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'); this.shortWeekdays = new Array('Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'); this.ampms = new Array('AM', 'PM'); this.zoneStrings = new Array(new Array(0, 'long-name', 'short-name')); }, } var SimpleDateFormatParserContext = Class.create(); SimpleDateFormatParserContext.prototype = { initialize: function() { this.newIndex=0; this.retValue=0; this.year=0; this.month=0; this.day=0; this.dayOfWeek=0; this.hour=0; this.min=0; this.sec=0; this.ampm=0; this.dateStr=""; }, } var SimpleDateFormat = Class.create(); SimpleDateFormat.prototype = { initialize: function(pattern, dateFormatSymbols) { this.pattern = pattern; this.dateFormatSymbols = dateFormatSymbols ? dateFormatSymbols : new DateFormatSymbols(); }, _handle: function(dateStr, date, parse) { var patternIndex = 0; var dateIndex = 0; var commentMode = false; var lastChar = 0; var currentChar=0; var nextChar=0; var patternSub = null; var context = new SimpleDateFormatParserContext(); if(date != null) { var yearStr = date.getYear()+""; if (yearStr.length < 4) { yearStr=(parseInt(yearStr, 10)+1900)+""; } context.year = parseInt(yearStr, 10); context.month = date.getMonth(); context.day = date.getDate(); context.dayOfWeek = date.getDay(); context.hour = date.getHours(); context.min = date.getMinutes(); context.sec = date.getSeconds(); } while (patternIndex < this.pattern.length) { currentChar = this.pattern.charAt(patternIndex); if(patternIndex<(this.pattern.length-1)) { nextChar = this.pattern.charAt(patternIndex+1); } else { nextChar = 0; } if (currentChar == '\'' && lastChar!='\\') { commentMode = !commentMode; patternIndex++; } else { if(!commentMode) { if (currentChar == '\\' && lastChar!='\\') { patternIndex++; } else { if(patternSub == null) patternSub = ""; patternSub+=currentChar; if(currentChar != nextChar) { this._handlePatternSub(context, patternSub, dateStr, dateIndex, parse); patternSub = null; if(context.newIndex<0) break; dateIndex = context.newIndex; } patternIndex++; } } else { if(parse) { if(this.pattern.charAt(patternIndex)!=dateStr.charAt(dateIndex)) { //invalid character in dateString return null; } } else { context.dateStr+=this.pattern.charAt(patternIndex); } patternIndex++; dateIndex++; } } lastChar = currentChar; } this._handlePatternSub(context, patternSub, dateStr, dateIndex, parse); return context; }, parse: function(dateStr) { var context = this._handle(dateStr, null, true); return new Date(context.year, context.month, context.day,context.hour,context.min,context.sec); }, format: function(date) { var context = this._handle(null, date, false); return context.dateStr; }, parseString: function(context, dateStr, dateIndex, strings) { for(var i=0; i<strings.length;i++) { var currentStrings = strings[0]; for (var j = 0; j < currentStrings.length; j++) { var currentString = currentStrings[j]; if(dateStr.substring(dateIndex,dateIndex+currentString.length).equals(currentString)) { context.newIndex=dateIndex+currentString.length; context.retValue=j; return context; } } } context.newIndex=-1; return context; }, _parseNum: function(context, dateStr, posCount, dateIndex) { for(var i=posCount;i>0;i--) { var numStr = dateStr.substring(dateIndex,dateIndex+posCount); context.retValue = parseInt(numStr, 10); if(context.retValue == Number.NaN) continue; context.newIndex = dateIndex+posCount; return context; } context.newIndex = -1; return context; }, _handlePatternSub: function(context, patternSub, dateStr, dateIndex, parse) { if(patternSub==null || patternSub.length==0) return; if(patternSub.charAt(0)=='y') { if(parse) { if (patternSub.length <= 3) { this._parseNum(context, dateStr,2,dateIndex); context.year = (context.retValue < 26) ? 2000 + context.retValue : 1900 + context.retValue; } else { this._parseNum(context, dateStr,4,dateIndex); context.year = context.retValue; } } else { this._formatNum(context,context.year,patternSub.length <= 3 ? 2 : 4); } } else if(patternSub.charAt(0)=='M') { if(parse) { if (patternSub.length == 3) { var fragment = dateStr.substr(dateIndex, 3); var index = this._indexOf(this.dateFormatSymbols.shortMonths, fragment); if (index != -1) { context.month = index; context.newIndex = dateIndex + 3; } } else if (patternSub.length >= 4) { var fragment = dateStr.substr(dateIndex); var index = this._prefixOf(this.dateFormatSymbols.months, fragment); if (index != -1) { context.month = index; context.newIndex = dateIndex + this.dateFormatSymbols.months[index].length; } } else { this._parseNum(context, dateStr,2,dateIndex); context.month = context.retValue-1; } } else { if (patternSub.length == 3) { context.dateStr += this.dateFormatSymbols.shortMonths[context.month]; } else if (patternSub.length >= 4) { context.dateStr += this.dateFormatSymbols.months[context.month]; } else { this._formatNum(context,context.month+1,patternSub.length); } } } else if(patternSub.charAt(0)=='d') { if(parse) { this._parseNum(context, dateStr,2,dateIndex); context.day = context.retValue; } else { this._formatNum(context,context.day,patternSub.length); } } else if(patternSub.charAt(0)=='E') { if(parse) { // XXX dayOfWeek is not used to generate date at the moment if (patternSub.length <= 3) { var fragment = dateStr.substr(dateIndex, 3); var index = this._indexOf(this.dateFormatSymbols.shortWeekdays, fragment); if (index != -1) { context.dayOfWeek = index; context.newIndex = dateIndex + 3; } } else { var fragment = dateStr.substr(dateIndex); var index = this._prefixOf(this.dateFormatSymbols.weekdays, fragment); if (index != -1) { context.dayOfWeek = index; context.newIndex = dateIndex + this.dateFormatSymbols.weekdays[index].length; } } } else { if (patternSub.length <= 3) { context.dateStr += this.dateFormatSymbols.shortWeekdays[context.dayOfWeek]; } else { context.dateStr += this.dateFormatSymbols.weekdays[context.dayOfWeek]; } } } else if(patternSub.charAt(0)=='H' || patternSub.charAt(0)=='h') { if(parse) { this._parseNum(context, dateStr,2,dateIndex); context.hour = context.retValue; } else { this._formatNum(context,context.hour,patternSub.length); } } else if(patternSub.charAt(0)=='m') { if(parse) { this._parseNum(context, dateStr,2,dateIndex); context.min = context.retValue; } else { this._formatNum(context,context.min,patternSub.length); } } else if(patternSub.charAt(0)=='s') { if(parse) { this._parseNum(context, dateStr,2,dateIndex); context.sec = context.retValue; } else { this._formatNum(context,context.sec,patternSub.length); } } else if(patternSub.charAt(0)=='a') { if(parse) { parseString(context, dateStr,dateIndex,new Array(ampms)); context.ampm = context.retValue; } else { this._formatNum(context,context.ampm,patternSub.length); } } else { if(parse) { context.newIndex=dateIndex+patternSub.length; } else { context.dateStr+=patternSub; } } }, _formatNum: function (context, num, length) { var str = num+""; while(str.length<length) str="0"+str; // XXX dow we have to distinguish left and right 'cutting' if (str.length > length) { str = str.substr(str.length - length); } context.dateStr+=str; }, // perhaps add to Array.prototype _indexOf: function (array, value) { for (var i = 0; i < array.length; ++i) { if (array[i] == value) { return i; } } return -1; }, _prefixOf: function (array, value) { for (var i = 0; i < array.length; ++i) { if (value.indexOf(array[i]) == 0) { return i; } } return -1; } }
import org.mozilla.javascript.JavaScriptException; import java.text.SimpleDateFormat; import java.text.DecimalFormat; import java.text.ParseException; import java.util.Date; import java.util.Locale; import java.util.Calendar; import java.io.IOException; public class DateTest extends JavaScriptTestCase { private static final int[] YEAR_MONTH_DAY = {Calendar.YEAR, Calendar.MONTH, Calendar.DAY_OF_MONTH}; protected void setUp() throws Exception { super.setUp(); // loadScriptFile("debug.js"); loadScriptFile("prototype-basics.js"); loadScriptFile("date.js"); } public void testNumberOnlyDateFormats() throws IOException, JavaScriptException { Date date = new Date(); checkFormat("yyyyMMdd", date, YEAR_MONTH_DAY); checkFormat("ddMMyyyy", date, YEAR_MONTH_DAY); checkFormat("dd/MM/yyyy", date, YEAR_MONTH_DAY); checkFormat("dd/MM/yyy", date, YEAR_MONTH_DAY); checkFormat("dd/MM/yy", date, YEAR_MONTH_DAY); checkFormat("EddMMyyyy", date, YEAR_MONTH_DAY); } public void testEnglishMonths() throws IOException { for (int month = 0; month < 12; ++month) { Calendar calendar = Calendar.getInstance(Locale.ENGLISH); calendar.set(Calendar.MONTH, month); Date date = calendar.getTime(); StringBuffer format = new StringBuffer("M"); for (int i = 0; i < 4; ++i) { format.append('M'); checkFormat(format.toString(), date, new int[] {Calendar.MONTH}); } } } public void testTwoDigitYears() throws IOException, ParseException { DecimalFormat decimalFormat = new DecimalFormat("00"); SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yy", Locale.ENGLISH); Calendar calendar = Calendar.getInstance(Locale.ENGLISH); for (int year = 0; year < 100; ++year) { String yearString = decimalFormat.format(year); calendar.setTime(simpleDateFormat.parse(yearString)); assertEquals(calendar.get(Calendar.YEAR), evalParseDate(yearString, "yy").get(Calendar.YEAR)); } } public void testEnglishWeekDays() throws IOException { for (int day = 1; day <= 7; ++day) { Calendar calendar = Calendar.getInstance(); calendar.set(Calendar.DAY_OF_WEEK, day); Date date = calendar.getTime(); StringBuffer format = new StringBuffer(); for (int i = 0; i < 4; ++i) { format.append('E'); checkFormat(format.toString(), date, new int[0]); // XXX new int[] {Calendar.DAY_OF_WEEK} } } } private Object evalFormatDate(Date date, String format) { return eval("new SimpleDateFormat(\"" + format + "\")" + ".format(new Date(" + date.getTime() + "))"); } private Calendar evalParseDate(String input, String format) { long time = evalLong("new SimpleDateFormat(\"" + format + "\")" + ".parse(\"" + input + "\").getTime()"); Calendar calendar = Calendar.getInstance(Locale.ENGLISH); calendar.setTime(new Date(time)); return calendar; } private void checkFormat(String format, Date date, int[] fields) { SimpleDateFormat simpleDateFormat = new SimpleDateFormat(format, Locale.ENGLISH); assertEquals(simpleDateFormat.format(date), evalFormatDate(date, format)); Calendar calendar1 = Calendar.getInstance(Locale.ENGLISH); calendar1.setTime(date); Calendar calendar2 = evalParseDate(simpleDateFormat.format(date), format.toString()); for (int i = 0; i < fields.length; i++) { int field = fields[i]; checkField(calendar1, calendar2, field); } } private void checkField(Calendar calendar1, Calendar calendar2, int field) { assertEquals(calendar1.get(field), calendar2.get(field)); } }
import junit.framework.TestCase; import org.mozilla.javascript.Context; import org.mozilla.javascript.Scriptable; import org.mozilla.javascript.JavaScriptException; import java.io.IOException; import java.io.FileReader; import java.util.Date; public class JavaScriptTestCase extends TestCase { protected Context cx; protected Scriptable scope; protected void setUp() throws Exception { super.setUp(); cx = Context.enter(); scope = cx.initStandardObjects(null); } protected void tearDown() throws Exception { Context.exit(); } protected Object eval(String script) throws JavaScriptException { return cx.evaluateString(scope, script, "test", 1, null); } protected int evalInt(String script) throws JavaScriptException { Object o = eval(script); if (o instanceof Number) { return ((Number) o).intValue(); } throw new JavaScriptException(null, "invalid return type " + o.getClass().getName() + " with value " + o, 0); } protected long evalLong(String script) throws JavaScriptException { Object o = eval(script); if (o instanceof Number) { return ((Number) o).longValue(); } throw new JavaScriptException(null, "invalid return type " + o.getClass().getName() + " with value " + o, 0); } protected boolean evalBoolean(String script) throws JavaScriptException { Object o = eval(script); if (o instanceof Boolean) { return ((Boolean) o).booleanValue(); } throw new JavaScriptException(null, "invalid return type " + o.getClass().getName() + " with value " + o, 0); } // XXX directory handling protected void loadScriptFile(String jsFile) throws IOException, JavaScriptException { cx.evaluateReader(scope, new FileReader(jsFile), jsFile, 0, null); } }