com.codename1.l10n.SimpleDateFormat Maven / Gradle / Ivy
/*
* Copyright (c) 2012, Eric Coolman, Codename One and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation. Codename One designates this
* particular file as subject to the "Classpath" exception as provided
* by Oracle in the LICENSE file that accompanied this code.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Codename One through http://www.codenameone.com/ if you
* need additional information or have any questions.
*/
package com.codename1.l10n;
import com.codename1.util.DateUtil;
import java.util.Arrays;
import java.util.Calendar;
import java.util.Date;
import java.util.List;
import java.util.TimeZone;
import java.util.Vector;
/**
* A class for parsing and formatting dates with a given pattern, compatible
* with the Java 6 API, as in the examples here: https://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html
*
* To localize the formatted dates, see the discussion
* Format a localized date
* in Codename One.
*
* @author Eric Coolman
*/
public class SimpleDateFormat extends DateFormat {
/**
* Pattern character for ERA (ie. BC, AD).
*/
private static final char ERA_LETTER = 'G';
/**
* Pattern character for year.
*/
private static final char YEAR_LETTER = 'y';
/**
* Pattern character for month.
*/
private static final char MONTH_LETTER = 'M';
/**
* Pattern character for week in year.
*/
private static final char WEEK_IN_YEAR_LETTER = 'w';
/**
* Pattern character for week in month.
*/
private static final char WEEK_IN_MONTH_LETTER = 'W';
/**
* Pattern character for day in year.
*/
private static final char DAY_IN_YEAR_LETTER = 'D';
/**
* Pattern character for day.
*/
private static final char DAY_LETTER = 'd';
/**
* Pattern character for day-of-week in month.
*/
private static final char DOW_IN_MONTH_LETTER = 'F';
/**
* Pattern character for day of week.
*/
private static final char DAY_OF_WEEK_LETTER = 'E';
/**
* Pattern character for am/pm.
*/
private static final char AMPM_LETTER = 'a';
/**
* Pattern character for hour (0-23).
*/
private static final char HOUR_LETTER = 'H';
/**
* Pattern character for 1-based hour (1-24).
*/
private static final char HOUR_1_LETTER = 'k';
/**
* Pattern character for 12-hour (0-11).
*/
private static final char HOUR12_LETTER = 'K';
/**
* Pattern character for 1-based 12-hour (1-12).
*/
private static final char HOUR12_1_LETTER = 'h';
/**
* Pattern character for minute.
*/
private static final char MINUTE_LETTER = 'm';
/**
* Pattern character for second.
*/
private static final char SECOND_LETTER = 's';
/**
* Pattern character for millisecond.
*/
private static final char MILLISECOND_LETTER = 'S';
/**
* Pattern character for general timezone.
*/
private static final char TIMEZONE_LETTER = 'z';
/**
* Pattern character for RFC 822-style timezone.
*/
private static final char TIMEZONE822_LETTER = 'Z';
/**
* Internally used character for literal text.
*/
private static final char LITERAL_LETTER = '*';
/**
* Pattern character for starting/ending literal text explicitly in pattern.
*/
private static final char EXPLICIT_LITERAL = '\'';
/**
* positive sign
*/
private static final char SIGN_POSITIVE = '+';
/**
* negative sign
*/
private static final char SIGN_NEGATIVE = '-';
/**
* The number of milliseconds in a minute.
*/
private static final int MILLIS_TO_MINUTES = 60000;
/**
* Pattern characters recognized by this implementation (same as JDK 1.6).
*/
private static final String PATTERN_LETTERS = "adDEFGHhKkMmsSwWyzZ";
/**
* TimeZone ID for Greenwich Mean Time
*/
private static final String GMT = "GMT";
/**
* This is missing from the Codename One Calendar object, but required by
* TimeZone.getOffset()
*/
private static final int ERA = 0;
/**
* More missing from the calendar object - being lower field values, it is
* likely they do exist on the devices Calendar object, if they don't,
* certain lesser-used letters will not work in a pattern.
*/
private static final int WEEK_OF_MONTH = 4;
private static final int WEEK_OF_YEAR = 3;
private static final int DAY_OF_WEEK_IN_MONTH = 8;
private static final int DAY_OF_YEAR = 6;
/**
* Localisation sensitive symbols used for handling text components.
*/
private DateFormatSymbols dateFormatSymbols;
/**
* The user-supplied pattern
*/
private String pattern;
/**
* The parsed pattern
*/
private List patternTokens;
/**
* Construct a SimpleDateFormat with no pattern.
*/
public SimpleDateFormat() {
super();
}
/**
* Construct a SimpleDateFormat with a given pattern.
*
* @param pattern
*/
public SimpleDateFormat(String pattern) {
super();
this.pattern = pattern;
}
/**
* @return the pattern
*/
public String toPattern() {
return pattern;
}
/**
* Get the date format symbols for parsing/formatting textual components of
* dates in a localization sensitive way.
*
* @return current symbols.
*/
public DateFormatSymbols getDateFormatSymbols() {
if (dateFormatSymbols == null) {
dateFormatSymbols = new DateFormatSymbols();
}
return dateFormatSymbols;
}
/**
* Apply new date format symbols for parsing/formatting textual components
* of dates in a localisation sensitive way.
*
* @param newSymbols new format symbols.
*/
public void setDateFormatSymbols(DateFormatSymbols newSymbols) {
dateFormatSymbols = newSymbols;
}
/**
* Apply a new pattern.
*
* @param pattern the pattern to set
*/
public void applyPattern(String pattern) {
this.pattern = pattern;
if (patternTokens != null) {
patternTokens.clear();
patternTokens = null;
}
}
/**
* G
*
* @return
*/
List getPatternTokens() {
if (this.patternTokens == null) {
patternTokens = parseDatePattern(pattern);
}
return patternTokens;
}
/*
* (non-Javadoc)
*
* @see java.lang.Object#clone()
*/
@Override
public Object clone() {
SimpleDateFormat sdf = new SimpleDateFormat(pattern);
sdf.setDateFormatSymbols(dateFormatSymbols);
return sdf;
}
/*
* (non-Javadoc)
*
* @see java.text.DateFormat#format(java.util.Date)
*/
@Override
public String format(Date source) {
return format(source, new StringBuilder());
}
/*
* (non-Javadoc)
*
* @see java.text.DateFormat#format(java.util.Date, java.lang.StringBuffer)
*/
@Override
String format(Date source, StringBuffer toAppendTo) {
StringBuilder sb = new StringBuilder();
String out = format(source, sb);
toAppendTo.append(sb.toString());
return toAppendTo.toString();
}
@Override
String format(Date source, StringBuilder toAppendTo) {
if (pattern == null) {
return super.format(source, toAppendTo);
}
// format based on local timezone
Calendar calendar = Calendar.getInstance(TimeZone.getDefault());
calendar.setTime(source);
List pattern = getPatternTokens();
for (int i = 0; i < pattern.size(); i++) {
String token = (String) pattern.get(i);
char patternChar = token.charAt(0);
token = token.substring(1);
int len = token.length();
int v = -1;
switch (patternChar) {
case LITERAL_LETTER:
toAppendTo.append(token);
break;
case AMPM_LETTER:
boolean am = calendar.get(Calendar.AM_PM) == Calendar.AM;
String ampm[] = getDateFormatSymbols().getAmPmStrings();
toAppendTo.append(am ? ampm[0] : ampm[1]);
break;
case ERA_LETTER:
toAppendTo.append(getDateFormatSymbols().getEras()[calendar.get(ERA)]);
break;
case DAY_OF_WEEK_LETTER:
v = calendar.get(Calendar.DAY_OF_WEEK) - 1;
if (len > 3) {
toAppendTo.append(getDateFormatSymbols().getWeekdays()[v]);
} else {
toAppendTo.append(getDateFormatSymbols().getShortWeekdays()[v]);
}
break;
case TIMEZONE_LETTER:
String names[] = getTimeZoneDisplayNames(calendar.getTimeZone().getID());
if (names == null) {
toAppendTo.append(calendar.getTimeZone().getID());
} else {
DateUtil du = new DateUtil(TimeZone.getTimeZone(names[DateFormatSymbols.ZONE_ID]));
toAppendTo.append(names[du.inDaylightTime(source) ? DateFormatSymbols.ZONE_SHORTNAME_DST : DateFormatSymbols.ZONE_SHORTNAME]);
}
break;
case TIMEZONE822_LETTER:
v = getOffsetInMinutes(calendar, calendar.getTimeZone());
if (v < 0) {
toAppendTo.append(SIGN_NEGATIVE);
v = -v;
} else {
toAppendTo.append(SIGN_POSITIVE);
}
toAppendTo.append(leftPad(v / 60, 2));
toAppendTo.append(leftPad(v % 60, 2));
break;
case YEAR_LETTER:
v = calendar.get(Calendar.YEAR);
if (len == 2) {
v %= 100;
}
toAppendTo.append(leftPad(v, len));
break;
case MONTH_LETTER:
v = calendar.get(Calendar.MONTH) - Calendar.JANUARY;
if (len > 3) {
toAppendTo.append(L10NManager.getInstance().getLongMonthName(source));
} else if (len == 3) {
toAppendTo.append(L10NManager.getInstance().getShortMonthName(source));
} else {
toAppendTo.append(leftPad(v + 1, len));
}
break;
case DAY_LETTER:
v = calendar.get(Calendar.DAY_OF_MONTH);
toAppendTo.append(leftPad(v, len));
break;
case HOUR_LETTER:
case HOUR_1_LETTER:
case HOUR12_LETTER:
case HOUR12_1_LETTER:
v = calendar.get(Calendar.HOUR_OF_DAY);
if (patternChar == HOUR_1_LETTER && v == 0) {
v = 24;
}
if (patternChar == HOUR12_1_LETTER) {
v %= 12;
if (v == 0) {
v = 12;
}
} else {
if (patternChar == HOUR12_LETTER) {
v %= 12;
}
}
toAppendTo.append(leftPad(v, len));
break;
case MINUTE_LETTER:
v = calendar.get(Calendar.MINUTE);
toAppendTo.append(leftPad(v, len));
break;
case SECOND_LETTER:
v = calendar.get(Calendar.SECOND);
toAppendTo.append(leftPad(v, len));
break;
case MILLISECOND_LETTER:
v = calendar.get(Calendar.MILLISECOND);
toAppendTo.append(leftPad(v, len));
break;
case WEEK_IN_YEAR_LETTER:
v = calendar.get(WEEK_OF_YEAR);
toAppendTo.append(leftPad(v, len));
break;
case WEEK_IN_MONTH_LETTER:
v = calendar.get(WEEK_OF_MONTH);
toAppendTo.append(leftPad(v, len));
break;
case DAY_IN_YEAR_LETTER:
v = calendar.get(DAY_OF_YEAR);
toAppendTo.append(leftPad(v, len));
break;
case DOW_IN_MONTH_LETTER:
v = calendar.get(DAY_OF_WEEK_IN_MONTH);
toAppendTo.append(leftPad(v, len));
break;
}
}
return toAppendTo.toString();
}
private String[] getTimeZoneDisplayNames(String id) {
for (String zoneStrings[] : getDateFormatSymbols().getZoneStrings()) {
if (zoneStrings[DateFormatSymbols.ZONE_ID].equalsIgnoreCase(id)) {
return zoneStrings;
}
}
return null;
}
String leftPad(int v, int size) {
String s = String.valueOf(v);
for (int i = s.length(); i < size; i++) {
s = '0' + s;
}
return s;
}
/**
* Parses text from a string to produce a Date.
*/
@Override
public Date parse(String source) throws ParseException {
if (pattern == null) {
throw new ParseException("You must provide a template before calling the SimpleDateFormat.parse(...) method", 0);
}
int startIndex = 0;
// parse based on GMT timezone for handling offsets
Calendar calendar = Calendar.getInstance(TimeZone.getTimeZone(GMT));
TimeZone parsedTimeZone = null;
calendar.set(Calendar.MINUTE, 0);
calendar.set(Calendar.SECOND, 0);
calendar.set(Calendar.MILLISECOND, 0);
int tzMinutes = -1;
int pmMinutes = 0;
List pattern = getPatternTokens();
for (int i = 0; i < pattern.size(); i++) {
String token = (String) pattern.get(i);
boolean adjacent = false;
if (i < (pattern.size() - 1)) {
adjacent = ((String) pattern.get(i + 1)).charAt(0) != LITERAL_LETTER;
}
String s = null;
int v = -1;
char patternChar = token.charAt(0);
token = token.substring(1);
switch (patternChar) {
case LITERAL_LETTER:
s = readLiteral(source, startIndex, token);
if (!s.equalsIgnoreCase(token)) {
throw new ParseException("Unparseable string "+source, startIndex);
}
break;
case AMPM_LETTER:
s = readAmPmMarker(source, startIndex);
if (s == null || ((v = parseAmPmMarker(source, startIndex)) == -1)) {
throwInvalid("am/pm marker", startIndex);
}
if (v == Calendar.PM) {
pmMinutes = 12 * 60;
}
break;
case DAY_OF_WEEK_LETTER:
s = readDayOfWeek(source, startIndex);
if (s == null) {
throwInvalid("weekday", startIndex);
}
break;
case TIMEZONE_LETTER:
case TIMEZONE822_LETTER:
s = readTimeZone(source, startIndex);
TimeZoneResult res = new TimeZoneResult();
if (s == null || (v = parseTimeZone(s, startIndex, res)) == -1) {
throwInvalid("timezone", startIndex);
}
if (res != null) {
parsedTimeZone = res.timeZone;
}
tzMinutes = ((tzMinutes == -1) ? 0 : tzMinutes) + v;
break;
case YEAR_LETTER:
s = readNumber(source, startIndex, token, adjacent);
calendar.set(Calendar.YEAR, parseYear(s, token, startIndex));
break;
case MONTH_LETTER:
s = readMonth(source, startIndex, token, adjacent);
calendar.set(Calendar.MONTH, parseMonth(s, startIndex));
break;
case DAY_LETTER:
s = readNumber(source, startIndex, token, adjacent);
calendar.set(Calendar.DAY_OF_MONTH, parseNumber(s, startIndex, "day of month", 1, 31));
break;
case HOUR_LETTER:
case HOUR_1_LETTER:
case HOUR12_LETTER:
case HOUR12_1_LETTER:
s = readNumber(source, startIndex, token, adjacent);
calendar.set(Calendar.HOUR_OF_DAY, parseHour(s, patternChar, startIndex));
break;
case MINUTE_LETTER:
s = readNumber(source, startIndex, token, adjacent);
calendar.set(Calendar.MINUTE, parseNumber(s, startIndex, "minute", 0, 59));
break;
case SECOND_LETTER:
s = readNumber(source, startIndex, token, adjacent);
calendar.set(Calendar.SECOND, parseNumber(s, startIndex, "second", 0, 59));
break;
case MILLISECOND_LETTER:
s = readNumber(source, startIndex, token, adjacent);
calendar.set(Calendar.MILLISECOND, parseNumber(s, startIndex, "millisecond", 0, 999));
break;
case WEEK_IN_YEAR_LETTER:
s = readNumber(source, startIndex, token, adjacent);
calendar.set(WEEK_OF_YEAR, parseNumber(s, startIndex, "week of year", 1, 52));
break;
case WEEK_IN_MONTH_LETTER:
s = readNumber(source, startIndex, token, adjacent);
calendar.set(WEEK_OF_MONTH, parseNumber(s, startIndex, "week of month", 0, 5));
break;
case DAY_IN_YEAR_LETTER:
s = readNumber(source, startIndex, token, adjacent);
calendar.set(DAY_OF_YEAR, parseNumber(s, startIndex, "day of year", 1, 365));
break;
case DOW_IN_MONTH_LETTER:
s = readNumber(source, startIndex, token, adjacent);
calendar.set(DAY_OF_WEEK_IN_MONTH,
parseNumber(s, startIndex, "day of week in month", -5, 5));
break;
}
if (s != null) {
startIndex += s.length();
}
}
TimeZone localTimezone = Calendar.getInstance().getTimeZone();
calendar.getTime(); // this seems to be necessary to calculate the time before changing the timzezone
calendar.setTimeZone(localTimezone);
if (pmMinutes != 0) {
calendar.set(Calendar.MINUTE, calendar.get(Calendar.MINUTE) + pmMinutes);
}
long rawOffset = localTimezone.getRawOffset();
int rawOffsetMinutes = (int)(rawOffset / MILLIS_TO_MINUTES);
int localDSTOffset = getLocalDSTOffset(calendar);
if (tzMinutes != -1) {
tzMinutes = -rawOffsetMinutes - tzMinutes;
int tzDstOffset = 0;
if (parsedTimeZone != null) {
Calendar tzCalendar = Calendar.getInstance(parsedTimeZone);
tzCalendar.setTime(calendar.getTime());
tzDstOffset = getDSTOffset(tzCalendar);
}
tzMinutes = tzMinutes - (localDSTOffset - tzDstOffset);
calendar.set(Calendar.MINUTE, calendar.get(Calendar.MINUTE) - tzMinutes);
}
calendar.set(Calendar.MINUTE, calendar.get(Calendar.MINUTE) - rawOffsetMinutes - localDSTOffset);
return calendar.getTime();
}
/**
* Parse a hour value. Depending on patternChar parameter, the hour can be
* 0-23, 1-24, 0-11, or 1-12. The returned value will always be 0 based.
*
* @param source as a string.
* @param offset the offset of original timestamp where marker started, for
* error reporting.
* @return hour.
* @throws ParseException if the source could not be parsed. See http
* ://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html
*/
int parseHour(String source, char patternChar, int offset) throws ParseException {
int min = 0;
boolean oneBased = (patternChar == HOUR_1_LETTER || patternChar == HOUR12_1_LETTER);
int max = ((patternChar == HOUR_LETTER || patternChar == HOUR_1_LETTER) ? 23 : 11) + (oneBased ? 1 : 0);
return parseNumber(source, offset, "hour", min, max);
}
/**
* Utility method to validate a number is within given range.
*/
void validateNumber(int i, int ofs, String name, int min, int max) throws ParseException {
if (i < min || i > max) {
throwInvalid(name, ofs);
}
}
/**
* Utility method to keep parsing errors consistent.
*
* @param name name of the element being parsed when error occurred.
* @param offset offset within the original timestamp where named element
* beings.
*/
int throwInvalid(String name, int offset) throws ParseException {
throw new ParseException("Invalid " + name + " value", offset);
}
/**
* Parse a numeric value, validating against given min/max constraints.
*
* @param source as a string.
* @param ofs the offset of original timestamp where number starts, for
* error reporting.
* @return numeric value as an int
* @throws ParseException if the source could not be parsed.
*/
int parseNumber(String source, int ofs, String name, int min, int max) throws ParseException {
if (source == null) {
throwInvalid(name, ofs);
}
int v = -1;
try {
v = Integer.parseInt(source);
} catch (NumberFormatException nfe) {
throwInvalid(name, ofs);
}
if (min != max) {
validateNumber(v, ofs, name, min, max);
}
return v;
}
/**
* Determine the number of minutes to adjust the date for local DST. This
* should provide a historically correct value, also accounting for changes
* in GMT offset. See TimeZone javadoc for more details.
*
* @param source
* @return
*/
int getLocalDSTOffset(Calendar source) {
TimeZone localTimezone = Calendar.getInstance().getTimeZone();
int rawOffset = localTimezone.getRawOffset() / MILLIS_TO_MINUTES;
return getOffsetInMinutes(source, localTimezone) - rawOffset;
}
int getDSTOffset(Calendar source) {
TimeZone timeZone = source.getTimeZone();
int rawOffset = timeZone.getRawOffset() / MILLIS_TO_MINUTES;
return getOffsetInMinutes(source, timeZone) - rawOffset;
}
/**
* Get the offset from GMT for a given timezone.
*
* @param source
* @param timezone
* @return
*/
int getOffsetInMinutes(Calendar source, TimeZone timezone) {
return timezone.getOffset(source.get(ERA), source.get(Calendar.YEAR), source.get(Calendar.MONTH),
source.get(Calendar.DAY_OF_MONTH), source.get(Calendar.DAY_OF_WEEK), source.get(Calendar.MILLISECOND))
/ MILLIS_TO_MINUTES;
}
/**
* Read a substring from source.
*
* @param ofs start index of substring
* @param end end index of substring
* @throws ParseException if substring is out of bounds.
*/
String readSubstring(String source, int ofs, int end) {
if (source == null || ofs > source.length() || end > source.length()) {
return null;
}
return source.substring(ofs, end);
}
/**
* Read a substring from source.
*
* @param ofs start index of substring
* @throws ParseException if substring is out of bounds.
*/
String readSubstring(String source, int ofs) {
return readSubstring(source, ofs, source.length());
}
/**
* Read an unparsable text string.
*
* @param source full timestamp
* @param ofs offset within timestamp where text starts
* @return the text
*/
String readLiteral(String source, int ofs, String token) {
return readSubstring(source, ofs, ofs + token.length());
}
/**
* Read the number. Does not attempt to parse.
*
* @param source full timestamp
* @param ofs offset within timestamp where number starts
* @param token the token currently being parsed
* @param adjacent true if the number is adjacent to next field with no
* literal separator.
* @return the number as a string, or null if could not read.
* @see #parseNumber(String, int, String, int, int)
*/
String readNumber(String source, int ofs, String token, boolean adjacent) {
if (adjacent) {
return readSubstring(source, ofs, ofs + token.length());
}
int len = source.length();
for (int i = ofs; i < len; i++) {
char ch = source.charAt(i);
if (isNumeric(ch) == false) {
// empty string would be invalid number
if (i == 0) {
return null;
}
return readSubstring(source, ofs, i);
}
}
return readSubstring(source, ofs);
}
/**
* Parse a year value. If the year is a two digit value, if the value is
* within 20 years ahead of the current year, current century will be used
* (ie. if current year is 2013, a value of "33" will return 2033),
* otherwise previous century is used (ie. with current year of 2012, a
* value of 97 will return "1997"). See Java 6 documentation for more
* details of this algorithm.
*
* @param source year as a string.
* @param ofs the offset of original timestamp where marker started, for
* error reporting.
* @return full year.
* @throws ParseException if the source could not be parsed. See http
* ://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html
*/
int parseYear(String source, String token, int ofs) throws ParseException {
int year = parseNumber(source, ofs, "year", -1, -1);
int len = source.length();
int tokenLen = token.length();
int thisYear = Calendar.getInstance().get(Calendar.YEAR);
if ((len == 2) && (tokenLen < 3)) {
int c = (thisYear / 100) * 100;
year += c;
if (year > (thisYear + 20)) {
year -= 100;
}
validateNumber(year, ofs, "year", 1000, thisYear + 1000);
}
return year;
}
/**
* Read the day of week string. Does not attempt to parse.
*
* @param source full timestamp
* @param ofs offset within timestamp where day of week starts
* @return the day of week as a string, or null if could not read.
*/
String readDayOfWeek(String source, int ofs) {
int i = findEndText(source, ofs);
if (i == -1) {
i = source.length();
}
String fragment = readSubstring(source, ofs, i);
if (fragment == null) {
return null;
}
DateFormatSymbols ds = getDateFormatSymbols();
for (String weekday : ds.getWeekdays()) {
if (fragment.equalsIgnoreCase(weekday)) {
return readSubstring(source, ofs, ofs + weekday.length());
}
}
for (String weekday : ds.getShortWeekdays()) {
if (fragment.equalsIgnoreCase(weekday)) {
return readSubstring(source, ofs, ofs + weekday.length());
}
}
if(ds.isLocalized()) {
ds.setLocalized(false);
String s = readDayOfWeek(source, ofs);
ds.setLocalized(true);
return s;
}
return null;
}
/**
* Read the am/pm marker string. Does not attempt to parse.
*
* @param source full timestamp
* @param ofs offset within timestamp where marker starts
* @return the marker as a string, or null if could not read.
* @see #parseAmPmMarker(String, int)
*/
String readAmPmMarker(String source, int ofs) {
int i = findEndText(source, ofs);
if (i == -1) {
i = source.length();
}
String fragment = readSubstring(source, ofs, i).toLowerCase();
if (fragment == null) {
return null;
}
DateFormatSymbols ds = getDateFormatSymbols();
String markers[] = ds.getAmPmStrings();
for (String marker : markers) {
if (fragment.toLowerCase().startsWith(marker.toLowerCase())) {
return readSubstring(source, ofs, ofs + marker.length());
}
}
for (String marker : markers) {
if (fragment.toLowerCase().charAt(0) == marker.toLowerCase().charAt(0)) {
return readSubstring(source, ofs, ofs + 1);
}
}
if(ds.isLocalized()) {
ds.setLocalized(false);
String s = readAmPmMarker(source, ofs);
ds.setLocalized(true);
return s;
}
return null;
}
/**
* Parse an AM/PM marker. The source marker can be the marker name as
* defined in DateFormatSymbols, or the first character of the marker name.
*
* @param source month as a string.
* @param ofs the offset of original timestamp where marker started, for
* error reporting.
* @return Calendar.AM or Calendar.PM
* @see DateFormatSymbols
* @throws ParseException if the source could not be parsed.
*/
int parseAmPmMarker(String source, int ofs) throws ParseException {
DateFormatSymbols ds = getDateFormatSymbols();
String markers[] = getDateFormatSymbols().getAmPmStrings();
int mlen = markers.length;
for (int i = 0; i < mlen; i++) {
if (markers[i].equalsIgnoreCase(source)) {
return i;
}
}
char ch = source.charAt(ofs);
if (ch == markers[0].charAt(0)) {
return Calendar.AM;
}
if (ch == markers[1].charAt(0)) {
return Calendar.PM;
}
if(ds.isLocalized()) {
ds.setLocalized(false);
int i = parseAmPmMarker(source, ofs);
ds.setLocalized(true);
return i;
}
return throwInvalid("am/pm marker", ofs);
}
/**
* Read the month string. Does not attempt to parse.
*
* @param source full timestamp
* @param ofs offset within timestamp where month starts
* @return the month as a string, or null if could not read.
* @see #parseMonth(String, int)
*/
String readMonth(String source, int ofs, String token, boolean adjacent) {
if (token.length() < 3) {
if (adjacent) {
return readSubstring(source, ofs, ofs + token.length());
}
if (isNumeric(source.charAt(0))) {
return readNumber(source, ofs, token, adjacent);
}
}
int i = findEndText(source, ofs);
if (i == -1) {
i = source.length();
}
String fragment = readSubstring(source, ofs, i);
if (fragment == null) {
return null;
}
DateFormatSymbols ds = getDateFormatSymbols();
for (String month : ds.getMonths()) {
if (fragment.equalsIgnoreCase(month)) {
return readSubstring(source, ofs, ofs + month.length());
}
}
for (String month : ds.getShortMonths()) {
if (fragment.equalsIgnoreCase(month)) {
return readSubstring(source, ofs, ofs + month.length());
}
}
if(ds.isLocalized()) {
ds.setLocalized(false);
String s = readMonth(source, ofs, token, adjacent);
ds.setLocalized(true);
return s;
}
return null;
}
/**
* Parse a month value to an offset from Calendar.JANUARY. The source month
* value can be numeric (1-12), a shortform or longform month name as
* defined in DateFormatSymbols.
*
* @param month as a string.
* @param offset the offset of original timestamp where month started, for
* error reporting.
* @return month as an offset from Calendar.JANUARY.
* @see DateFormatSymbols
* @throws ParseException if the source could not be parsed.
*/
int parseMonth(String month, int offset) throws ParseException {
if (month == null) {
throwInvalid("month", offset);
}
if (month.length() < 3) {
return (parseNumber(month, offset, "month", 1, 12) - 1) + Calendar.JANUARY;
}
DateFormatSymbols ds = getDateFormatSymbols();
String months[] = ds.getMonths();
int mlen = months.length;
for (int i = 0; i < mlen; i++) {
if (month.equalsIgnoreCase(months[i])) {
return i + Calendar.JANUARY;
}
}
months = ds.getShortMonths();
mlen = months.length;
for (int i = 0; i < mlen; i++) {
if (month.equalsIgnoreCase(months[i])) {
return i + Calendar.JANUARY;
}
}
if(ds.isLocalized()) {
ds.setLocalized(false);
int i = 0;
try {
i = parseMonth(month, offset);
} finally {
ds.setLocalized(true);
}
return i;
}
return throwInvalid("month", offset);
}
/**
* Read the timezone string. Does not attempt to parse.
*
* @param source full timestamp
* @param ofs offset within timestamp where timezone starts
* @return the timezone as a string or null if error reading.
* @see #parseTimeZone(String, int)
*/
String readTimeZone(String source, int ofs) {
int sp = source.indexOf(' ', ofs);
String fragment;
if (sp != -1) {
fragment = readSubstring(source, ofs, sp);
} else {
fragment = readSubstring(source, ofs);
}
if (fragment == null) {
return null;
}
int len = fragment.length();
if (len == 0) {
return null;
}
// handle zulu
if (len == 1) {
if (fragment.toLowerCase().equals("z")) {
return readSubstring(source, ofs, ofs + 1);
}
return null;
}
// 8 is length of "GMT-H:MM"
if (len >= 8 && fragment.toUpperCase().startsWith(GMT)) {
return readSubstring(source, ofs);
}
int ch = fragment.charAt(0);
if (len >= 5 && (ch == SIGN_NEGATIVE || ch == SIGN_POSITIVE)) {
return readSubstring(source, ofs, ofs + 5);
}
DateFormatSymbols ds = getDateFormatSymbols();
for (String timezone[] : ds.getZoneStrings()) {
for (String z : timezone) {
if (z.equalsIgnoreCase(fragment)) {
return readSubstring(source, ofs, ofs + z.length());
}
}
}
if(ds.isLocalized()) {
ds.setLocalized(false);
String s = readTimeZone(source, ofs);
ds.setLocalized(true);
return s;
}
return null;
}
private static class TimeZoneResult {
TimeZone timeZone;
}
/**
* Parse the timezone to an offset from GMT in minutes. The source can be
* RFC-822 (ie. -0400), ISO8601 (ie. GMT+08:50), or TimeZone ID (ie. PDT,
* America/New_York, etc). This method does not adjust for DST.
*
* @param source source timezone.
* @param ofs the offset of original timestamp where month started, for
* error reporting.
* @return offset from GMT in minutes.
* @throws ParseException if the source could not be parsed.
*/
int parseTimeZone(String source, int ofs, TimeZoneResult res) throws ParseException {
if (source == null) {
throwInvalid("timezone", ofs);
}
char tzSign = source.charAt(0);
if(tzSign == 'z' || tzSign == 'Z') {
return 0;
}
// handle RFC822 style GMT offset (-0500)
if (tzSign == SIGN_NEGATIVE || tzSign == SIGN_POSITIVE) {
source = readSubstring(source, 1);
if (source == null) {
throwInvalid("timezone", ofs);
}
// set the index to point to divider between hours
// and minutes. Hour can be one or two digits, minutes
// is always 2 digits.
int index = 2;
if (source.length() == 3) {
index--;
}
int tzHours = parseNumber(readSubstring(source, 0, index), ofs, "timezone", 0, 23);
int tzMinutes = parseNumber(readSubstring(source, index), ofs, "timezone", 0, 59);
tzMinutes += tzHours * 60;
if (tzSign != SIGN_NEGATIVE) {
tzMinutes = -tzMinutes;
}
return tzMinutes;
}
// handle explicit GMT offset (GMT+H:MM)
if (source.toUpperCase().startsWith(GMT)) {
int index = source.indexOf(':');
if (index != -1) {
String part1 = readSubstring(source, 3, index);
String part2 = readSubstring(source, index + 1);
if (part1 == null || part2 == null) {
throwInvalid("timezone", ofs);
}
source = part1 + part2;
} else {
source = readSubstring(source, 3);
}
if (source.length() == 0) {
return 0;
}
return parseTimeZone(source, ofs, res);
}
DateFormatSymbols ds = getDateFormatSymbols();
// Handle timezone based on ID or full name
for (String timezone[] : ds.getZoneStrings()) {
for (String z : timezone) {
if (z.equalsIgnoreCase(source)) {
TimeZone tz = TimeZone.getTimeZone(timezone[DateFormatSymbols.ZONE_ID]);
res.timeZone = tz;
return -(tz.getRawOffset() / MILLIS_TO_MINUTES);
}
}
}
if(ds.isLocalized()) {
ds.setLocalized(false);
int i = 0;
try {
i = parseTimeZone(source, ofs, res);
} finally {
ds.setLocalized(true);
}
return i;
}
return throwInvalid("timezone", ofs);
}
/**
* Attempt to find the end of a field if the length is not known.
*
* @param source the full source timestamp
* @param ofs index of where current field starts.
* @return the index of the end of field, or -1 if couldn't determine.
*/
int findEndText(String source, int ofs) {
int slen = source.length();
for (int i = ofs; i < slen; i++) {
if (!isAlpha(source.charAt(i)) && !isNumeric(source.charAt(i))) {
return i;
}
}
return -1;
}
/**
* Test if a character is alpha (A-Z,a-z).
*/
boolean isAlpha(char ch) {
return ((ch >= 'A' && ch <= 'Z') || (ch >= 'a' && ch <= 'z'));
}
/**
* Test if a character is number (0-9).
*/
boolean isNumeric(char ch) {
return (ch >= '0' && ch <= '9');
}
/**
* Parse the date pattern.
*
* The list will contain each token of the pattern. The first character of
* the token contains the pattern component type, or wildcard (*) for
* literal patterns.
*
* @param pattern
* @return parsed pattern.
*/
List parseDatePattern(String pattern) {
List tokens = new Vector();
String tmp = null;
int plen = pattern.length();
for (int i = 0; i < plen; i++) {
char ch = pattern.charAt(i);
// Handle literal text enclosed in quotes
if (ch == EXPLICIT_LITERAL) {
int n = pattern.indexOf(EXPLICIT_LITERAL, i + 1);
if (n != -1) {
if (tmp != null) {
tokens.add(tmp.charAt(0) + tmp);
tmp = null;
}
tokens.add(LITERAL_LETTER + pattern.substring(i + 1, n));
}
i = n;
continue;
}
// Any invalid non-alpha characters are treated as literal text.
// invalid alpha characters are illegal.
boolean isValid = PATTERN_LETTERS.indexOf(ch) != -1;
if (isValid == false) {
if (tmp != null) {
tokens.add(tmp.charAt(0) + tmp);
tmp = null;
}
int n;
for (n = i; n < plen; n++) {
ch = pattern.charAt(n);
if (PATTERN_LETTERS.indexOf(ch) != -1) {
break;
}
if (isAlpha(ch)) {
throw new IllegalArgumentException("Illegal pattern character: " + ch);
}
}
tokens.add(LITERAL_LETTER + pattern.substring(i, n));
i = n - 1;
continue;
}
if (tmp == null) {
tmp = String.valueOf(ch);
continue;
} else if (ch == tmp.charAt(0)) {
tmp += ch;
} else {
tokens.add(tmp.charAt(0) + tmp);
tmp = String.valueOf(ch);
}
}
if (tmp != null) {
tokens.add(tmp.charAt(0) + tmp);
}
return tokens;
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy