/* Copyright (C) 1998, 1999 Cygnus Solutions This file is part of libgcj. This software is copyrighted work licensed under the terms of the Libgcj License. Please consult the file "LIBGCJ_LICENSE" for details. */ package java.text; import java.util.*; /** * @author Per Bothner * @date October 25, 1998. */ /* Written using "Java Class Libraries", 2nd edition, plus online * API docs for JDK 1.2 beta from http://www.javasoft.com. * Status: parse is not implemented. */ public class SimpleDateFormat extends DateFormat { private Date defaultCenturyStart; private DateFormatSymbols formatData; private String pattern; public SimpleDateFormat () { this("dd/MM/yy HH:mm", Locale.getDefault()); } public SimpleDateFormat (String pattern) { this(pattern, Locale.getDefault()); } public SimpleDateFormat (String pattern, Locale locale) { this.pattern = pattern; this.calendar = Calendar.getInstance(locale); this.numberFormat = NumberFormat.getInstance(locale); numberFormat.setGroupingUsed(false); this.formatData = new DateFormatSymbols (locale); } public SimpleDateFormat (String pattern, DateFormatSymbols formatData) { this.pattern = pattern; this.formatData = formatData; this.calendar = Calendar.getInstance(); this.numberFormat = NumberFormat.getInstance(); numberFormat.setGroupingUsed(false); } public Date get2DigitYearStart() { return defaultCenturyStart; } public void set2DigitYearStart(Date startDate) { defaultCenturyStart = startDate; } public DateFormatSymbols getDateFormatSymbols () { return formatData; } public void setDateFormatSymbols (DateFormatSymbols value) { formatData = value; } public String toPattern () { return pattern; } public void applyPattern (String pattern) { this.pattern = pattern; } private String applyLocalizedPattern (String pattern, String oldChars, String newChars) { int len = pattern.length(); StringBuffer buf = new StringBuffer(len); boolean quoted = false; for (int i = 0; i < len; i++) { char ch = pattern.charAt(i); if (ch == '\'') quoted = ! quoted; if (! quoted) { int j = oldChars.indexOf(ch); if (j >= 0) ch = newChars.charAt(j); } buf.append(ch); } return buf.toString(); } public void applyLocalizedPattern (String pattern) { String localChars = formatData.getLocalPatternChars(); String standardChars = DateFormatSymbols.localPatternCharsDefault; pattern = applyLocalizedPattern (pattern, localChars, standardChars); applyPattern(pattern); } public String toLocalizedPattern () { String localChars = formatData.getLocalPatternChars(); String standardChars = DateFormatSymbols.localPatternCharsDefault; return applyLocalizedPattern (pattern, standardChars, localChars); } private final void append (StringBuffer buf, int value, int numDigits) { numberFormat.setMinimumIntegerDigits(numDigits); numberFormat.format(value, buf, null); } public StringBuffer format (Date date, StringBuffer buf, FieldPosition pos) { Calendar calendar = (Calendar) this.calendar.clone(); calendar.setTime(date); int len = pattern.length(); int quoteStart = -1; for (int i = 0; i < len; i++) { char ch = pattern.charAt(i); if (ch == '\'') { // We must do a little lookahead to see if we have two // single quotes embedded in quoted text. if (i < len - 1 && pattern.charAt(i + 1) == '\'') { ++i; buf.append(ch); } else quoteStart = quoteStart < 0 ? i : -1; } // From JCL: any characters in the pattern that are not in // the ranges of [a..z] and [A..Z] are treated as quoted // text. else if (quoteStart != -1 || ((ch < 'a' || ch > 'z') && (ch < 'A' || ch > 'Z'))) buf.append(ch); else { int first = i; int value; while (++i < len && pattern.charAt(i) == ch) ; int count = i - first; // Number of repetions of ch in pattern. int beginIndex = buf.length(); int field; i--; // Skip all but last instance of ch in pattern. switch (ch) { case 'd': append(buf, calendar.get(Calendar.DATE), count); field = DateFormat.DATE_FIELD; break; case 'D': append(buf, calendar.get(Calendar.DAY_OF_YEAR), count); field = DateFormat.DAY_OF_YEAR_FIELD; break; case 'F': append(buf, calendar.get(Calendar.DAY_OF_WEEK_IN_MONTH),count); field = DateFormat.DAY_OF_WEEK_IN_MONTH_FIELD; break; case 'E': value = calendar.get(calendar.DAY_OF_WEEK); buf.append(count <= 3 ? formatData.getShortWeekdays()[value] : formatData.getWeekdays()[value]); field = DateFormat.DAY_OF_WEEK_FIELD; break; case 'w': append(buf, calendar.get(Calendar.WEEK_OF_YEAR), count); field = DateFormat.WEEK_OF_YEAR_FIELD; break; case 'W': append(buf, calendar.get(Calendar.WEEK_OF_MONTH), count); field = DateFormat.WEEK_OF_MONTH_FIELD; break; case 'M': value = calendar.get(Calendar.MONTH); if (count <= 2) append(buf, value + 1, count); else buf.append(count <= 3 ? formatData.getShortMonths()[value] : formatData.getMonths()[value]); field = DateFormat.MONTH_FIELD; break; case 'y': value = calendar.get(Calendar.YEAR); append(buf, count <= 2 ? value % 100 : value, count); field = DateFormat.YEAR_FIELD; break; case 'K': append(buf, calendar.get(Calendar.HOUR), count); field = DateFormat.HOUR0_FIELD; break; case 'h': value = ((calendar.get(Calendar.HOUR) + 11) % 12) + 1; append(buf, value, count); field = DateFormat.HOUR1_FIELD; break; case 'H': append(buf, calendar.get(Calendar.HOUR_OF_DAY), count); field = DateFormat.HOUR_OF_DAY0_FIELD; break; case 'k': value = ((calendar.get(Calendar.HOUR_OF_DAY) + 23) % 24) + 1; append(buf, value, count); field = DateFormat.HOUR_OF_DAY1_FIELD; break; case 'm': append(buf, calendar.get(Calendar.MINUTE), count); field = DateFormat.MINUTE_FIELD; break; case 's': append(buf, calendar.get(Calendar.SECOND), count); field = DateFormat.SECOND_FIELD; break; case 'S': append(buf, calendar.get(Calendar.MILLISECOND), count); field = DateFormat.MILLISECOND_FIELD; break; case 'a': value = calendar.get(calendar.AM_PM); buf.append(formatData.getAmPmStrings()[value]); field = DateFormat.AM_PM_FIELD; break; case 'z': String zoneID = calendar.getTimeZone().getID(); String[][] zoneStrings = formatData.getZoneStrings(); int zoneCount = zoneStrings.length; for (int j = 0; j < zoneCount; j++) { String[] strings = zoneStrings[j]; if (zoneID.equals(strings[0])) { j = count > 3 ? 2 : 1; if (calendar.get(Calendar.DST_OFFSET) != 0) j+=2; zoneID = strings[j]; break; } } buf.append(zoneID); field = DateFormat.TIMEZONE_FIELD; break; default: // Note that the JCL is actually somewhat // contradictory here. It defines the pattern letters // to be a particular list, but also says that a // pattern containing an invalid pattern letter must // throw an exception. It doesn't describe what an // invalid pattern letter might be, so we just assume // it is any letter in [a-zA-Z] not explicitly covered // above. throw new RuntimeException("bad format string"); } if (pos != null && field == pos.getField()) { pos.setBeginIndex(beginIndex); pos.setEndIndex(buf.length()); } } } return buf; } private final boolean expect (String source, ParsePosition pos, char ch) { int x = pos.getIndex(); boolean r = x < source.length() && source.charAt(x) == ch; if (r) pos.setIndex(x + 1); else pos.setErrorIndex(x); return r; } public Date parse (String source, ParsePosition pos) { int fmt_index = 0; int fmt_max = pattern.length(); calendar.clear(); int quote_start = -1; for (; fmt_index < fmt_max; ++fmt_index) { char ch = pattern.charAt(fmt_index); if (ch == '\'') { int index = pos.getIndex(); if (fmt_index < fmt_max - 1 && pattern.charAt(fmt_index + 1) == '\'') { if (! expect (source, pos, ch)) return null; ++fmt_index; } else quote_start = quote_start < 0 ? fmt_index : -1; continue; } if (quote_start != -1 || ((ch < 'a' || ch > 'z') && (ch < 'A' || ch > 'Z'))) { if (! expect (source, pos, ch)) return null; continue; } // We've arrived at a potential pattern character in the // pattern. int first = fmt_index; while (++fmt_index < fmt_max && pattern.charAt(fmt_index) == ch) ; int count = fmt_index - first; --fmt_index; // We can handle most fields automatically: most either are // numeric or are looked up in a string vector. In some cases // we need an offset. When numeric, `offset' is added to the // resulting value. When doing a string lookup, offset is the // initial index into the string array. int calendar_field; boolean is_numeric = true; String[] match = null; int offset = 0; int zone_number = 0; switch (ch) { case 'd': calendar_field = Calendar.DATE; break; case 'D': calendar_field = Calendar.DAY_OF_YEAR; break; case 'F': calendar_field = Calendar.DAY_OF_WEEK_IN_MONTH; break; case 'E': is_numeric = false; offset = 1; calendar_field = Calendar.DAY_OF_WEEK; match = (count <= 3 ? formatData.getShortWeekdays() : formatData.getWeekdays()); break; case 'w': calendar_field = Calendar.WEEK_OF_YEAR; break; case 'W': calendar_field = Calendar.WEEK_OF_MONTH; break; case 'M': calendar_field = Calendar.MONTH; if (count <= 2) ; else { is_numeric = false; match = (count <= 3 ? formatData.getShortMonths() : formatData.getMonths()); } break; case 'y': calendar_field = Calendar.YEAR; if (count <= 2) offset = 1900; break; case 'K': calendar_field = Calendar.HOUR; break; case 'h': calendar_field = Calendar.HOUR; offset = -1; break; case 'H': calendar_field = Calendar.HOUR_OF_DAY; break; case 'k': calendar_field = Calendar.HOUR_OF_DAY; offset = -1; break; case 'm': calendar_field = Calendar.MINUTE; break; case 's': calendar_field = Calendar.SECOND; break; case 'S': calendar_field = Calendar.MILLISECOND; break; case 'a': is_numeric = false; calendar_field = Calendar.AM_PM; match = formatData.getAmPmStrings(); break; case 'z': // We need a special case for the timezone, because it // uses a different data structure than the other cases. is_numeric = false; calendar_field = Calendar.DST_OFFSET; String[][] zoneStrings = formatData.getZoneStrings(); int zoneCount = zoneStrings.length; int index = pos.getIndex(); boolean found_zone = false; for (int j = 0; j < zoneCount; j++) { String[] strings = zoneStrings[j]; int k; for (k = 1; k < strings.length; ++k) { if (source.startsWith(strings[k], index)) break; } if (k != strings.length) { if (k > 2) ; // FIXME: dst. zone_number = 0; // FIXME: dst. // FIXME: raw offset to SimpleTimeZone const. calendar.setTimeZone(new SimpleTimeZone (1, strings[0])); pos.setIndex(index + strings[k].length()); break; } } if (! found_zone) { pos.setErrorIndex(pos.getIndex()); return null; } break; default: pos.setErrorIndex(pos.getIndex()); return null; } // Compute the value we should assign to the field. int value; if (is_numeric) { numberFormat.setMinimumIntegerDigits(count); Number n = numberFormat.parse(source, pos); if (pos == null || ! (n instanceof Long)) return null; value = n.intValue() + offset; } else if (match != null) { int index = pos.getIndex(); int i; for (i = offset; i < match.length; ++i) { if (source.startsWith(match[i], index)) break; } if (i == match.length) { pos.setErrorIndex(index); return null; } pos.setIndex(index + match[i].length()); value = i; } else value = zone_number; // Assign the value and move on. try { calendar.set(calendar_field, value); } // FIXME: what exception is thrown on an invalid // non-lenient set? catch (IllegalArgumentException x) { pos.setErrorIndex(pos.getIndex()); return null; } } return calendar.getTime(); } public boolean equals (Object obj) { if (! (obj instanceof SimpleDateFormat) || ! super.equals(obj) ) return false; SimpleDateFormat other = (SimpleDateFormat) obj; return (DateFormatSymbols.equals(pattern, other.pattern) && DateFormatSymbols.equals(formatData, other.formatData) && DateFormatSymbols.equals(defaultCenturyStart, other.defaultCenturyStart)); } public int hashCode () { int hash = super.hashCode(); if (pattern != null) hash ^= pattern.hashCode(); return hash; } }