package client.net.sf.saxon.ce.functions; import client.net.sf.saxon.ce.Configuration; import client.net.sf.saxon.ce.expr.ExpressionVisitor; import client.net.sf.saxon.ce.expr.XPathContext; import client.net.sf.saxon.ce.expr.number.Numberer_en; import client.net.sf.saxon.ce.lib.Numberer; import client.net.sf.saxon.ce.om.Item; import client.net.sf.saxon.ce.trans.Err; import client.net.sf.saxon.ce.trans.XPathException; import client.net.sf.saxon.ce.tree.util.FastStringBuffer; import client.net.sf.saxon.ce.value.*; import client.net.sf.saxon.ce.value.StringValue; import com.google.gwt.regexp.shared.MatchResult; import com.google.gwt.regexp.shared.RegExp; import java.math.BigDecimal; /** * Implement the format-date(), format-time(), and format-dateTime() functions * in XSLT 2.0 and XQuery 1.1. */ public class FormatDate extends SystemFunction { public FormatDate(int operation) { this.operation = operation; } public FormatDate newInstance() { return new FormatDate(operation); } public void checkArguments(ExpressionVisitor visitor) throws XPathException { int numArgs = argument.length; if (numArgs != 2 && numArgs != 5) { throw new XPathException("Function " + getDisplayName() + " must have either two or five arguments", getSourceLocator()); } super.checkArguments(visitor); } /** * Evaluate in a general context */ public Item evaluateItem(XPathContext context) throws XPathException { CalendarValue value = (CalendarValue)argument[0].evaluateItem(context); if (value==null) { return null; } String format = argument[1].evaluateItem(context).getStringValue(); StringValue calendarVal = null; StringValue countryVal = null; StringValue languageVal = null; if (argument.length > 2) { languageVal = (StringValue)argument[2].evaluateItem(context); calendarVal = (StringValue)argument[3].evaluateItem(context); countryVal = (StringValue)argument[4].evaluateItem(context); } String language = (languageVal == null ? null : languageVal.getStringValue()); String country = (countryVal == null ? null : countryVal.getStringValue()); CharSequence result = formatDate(value, format, language, country, context); if (calendarVal != null) { String cal = calendarVal.getStringValue(); if (!cal.equals("AD") && !cal.equals("ISO")) { result = "[Calendar: AD]" + result.toString(); } } return new StringValue(result); } /** * This method analyzes the formatting picture and delegates the work of formatting * individual parts of the date. * @param value the value to be formatted * @param format the supplied format picture * @param language the chosen language * @param country the chosen country * @param context the XPath dynamic evaluation context * @return the formatted date/time */ private static CharSequence formatDate(CalendarValue value, String format, String language, String country, XPathContext context) throws XPathException { Configuration config = context.getConfiguration(); boolean languageDefaulted = (language == null); if (language == null) { language = "en"; } if (country == null) { country = "US"; } Numberer numberer = config.makeNumberer(language, country); FastStringBuffer sb = new FastStringBuffer(FastStringBuffer.SMALL); if (numberer.getClass() == Numberer_en.class && !"en".equals(language) && !languageDefaulted) { sb.append("[Language: en]"); } int i = 0; while (true) { while (i < format.length() && format.charAt(i) != '[') { sb.append(format.charAt(i)); if (format.charAt(i) == ']') { i++; if (i == format.length() || format.charAt(i) != ']') { XPathException e = new XPathException("Closing ']' in date picture must be written as ']]'"); e.setErrorCode("XTDE1340"); e.setXPathContext(context); throw e; } } i++; } if (i == format.length()) { break; } // look for '[[' i++; if (i < format.length() && format.charAt(i) == '[') { sb.append('['); i++; } else { int close = (i < format.length() ? format.indexOf("]", i) : -1); if (close == -1) { XPathException e = new XPathException("Date format contains a '[' with no matching ']'"); e.setErrorCode("XTDE1340"); e.setXPathContext(context); throw e; } String componentFormat = format.substring(i, close); sb.append(formatComponent(value, Whitespace.removeAllWhitespace(componentFormat), numberer, country, context)); i = close+1; } } return sb; } private static RegExp componentPattern = RegExp.compile("([YMDdWwFHhmsfZzPCE])\\s*(.*)"); private static CharSequence formatComponent(CalendarValue value, CharSequence specifier, Numberer numberer, String country, XPathContext context) throws XPathException { boolean ignoreDate = (value instanceof TimeValue); boolean ignoreTime = (value instanceof DateValue); DateTimeValue dtvalue = value.toDateTime(); MatchResult matcher = componentPattern.exec(specifier.toString()); if (matcher == null) { XPathException error = new XPathException("Unrecognized date/time component [" + specifier + ']'); error.setErrorCode("XTDE1340"); error.setXPathContext(context); throw error; } String component = matcher.getGroup(1); if (component == null) { component = ""; } String format = matcher.getGroup(2); if (format==null) { format = ""; } boolean defaultFormat = false; if ("".equals(format) || format.startsWith(",")) { defaultFormat = true; switch (component.charAt(0) ) { case 'F': format = "Nn" + format; break; case 'P': format = 'n' + format; break; case 'C': case 'E': format = 'N' + format; break; case 'm': case 's': format = "01" + format; break; default: format = '1' + format; } } switch (component.charAt(0)) { case'Y': // year if (ignoreDate) { XPathException error = new XPathException("In formatTime(): an xs:time value does not contain a year component"); error.setErrorCode("XTDE1350"); error.setXPathContext(context); throw error; } else { int year = dtvalue.getYear(); if (year < 0) { year = 1 - year; } return formatNumber(component, year, format, defaultFormat, numberer, context); } case'M': // month if (ignoreDate) { XPathException error = new XPathException("In formatTime(): an xs:time value does not contain a month component"); error.setErrorCode("XTDE1350"); error.setXPathContext(context); throw error; } else { int month = dtvalue.getMonth(); return formatNumber(component, month, format, defaultFormat, numberer, context); } case'D': // day in month if (ignoreDate) { XPathException error = new XPathException("In formatTime(): an xs:time value does not contain a day component"); error.setErrorCode("XTDE1350"); error.setXPathContext(context); throw error; } else { int day = dtvalue.getDay(); return formatNumber(component, day, format, defaultFormat, numberer, context); } case'd': // day in year if (ignoreDate) { XPathException error = new XPathException("In formatTime(): an xs:time value does not contain a day component"); error.setErrorCode("XTDE1350"); error.setXPathContext(context); throw error; } else { int day = DateValue.getDayWithinYear(dtvalue.getYear(), dtvalue.getMonth(), dtvalue.getDay()); return formatNumber(component, day, format, defaultFormat, numberer, context); } case'W': // week of year if (ignoreDate) { XPathException error = new XPathException("In formatTime(): cannot obtain the week number from an xs:time value"); error.setErrorCode("XTDE1350"); error.setXPathContext(context); throw error; } else { int week = DateValue.getWeekNumber(dtvalue.getYear(), dtvalue.getMonth(), dtvalue.getDay()); return formatNumber(component, week, format, defaultFormat, numberer, context); } case'w': // week in month if (ignoreDate) { XPathException error = new XPathException("In formatTime(): cannot obtain the week number from an xs:time value"); error.setErrorCode("XTDE1350"); error.setXPathContext(context); throw error; } else { int week = DateValue.getWeekNumberWithinMonth(dtvalue.getYear(), dtvalue.getMonth(), dtvalue.getDay()); return formatNumber(component, week, format, defaultFormat, numberer, context); } case'H': // hour in day if (ignoreTime) { XPathException error = new XPathException("In formatDate(): an xs:date value does not contain an hour component"); error.setErrorCode("XTDE1350"); error.setXPathContext(context); throw error; } else { IntegerValue hour = (IntegerValue)value.getComponent(Component.HOURS); return formatNumber(component, (int)hour.intValue(), format, defaultFormat, numberer, context); } case'h': // hour in half-day (12 hour clock) if (ignoreTime) { XPathException error = new XPathException("In formatDate(): an xs:date value does not contain an hour component"); error.setErrorCode("XTDE1350"); error.setXPathContext(context); throw error; } else { IntegerValue hour = (IntegerValue)value.getComponent(Component.HOURS); int hr = (int)hour.intValue(); if (hr > 12) { hr = hr - 12; } if (hr == 0) { hr = 12; } return formatNumber(component, hr, format, defaultFormat, numberer, context); } case'm': // minutes if (ignoreTime) { XPathException error = new XPathException("In formatDate(): an xs:date value does not contain a minutes component"); error.setErrorCode("XTDE1350"); error.setXPathContext(context); throw error; } else { IntegerValue min = (IntegerValue)value.getComponent(Component.MINUTES); return formatNumber(component, (int)min.intValue(), format, defaultFormat, numberer, context); } case's': // seconds if (ignoreTime) { XPathException error = new XPathException("In formatDate(): an xs:date value does not contain a seconds component"); error.setErrorCode("XTDE1350"); error.setXPathContext(context); throw error; } else { IntegerValue sec = (IntegerValue)value.getComponent(Component.WHOLE_SECONDS); return formatNumber(component, (int)sec.intValue(), format, defaultFormat, numberer, context); } case'f': // fractional seconds // ignore the format if (ignoreTime) { XPathException error = new XPathException("In formatDate(): an xs:date value does not contain a fractional seconds component"); error.setErrorCode("XTDE1350"); error.setXPathContext(context); throw error; } else { int micros = (int)((IntegerValue)value.getComponent(Component.MICROSECONDS)).intValue(); return formatNumber(component, micros, format, defaultFormat, numberer, context); } case'Z': // timezone in +hh:mm format, unless format=N in which case use timezone name if (value.hasTimezone()) { return getNamedTimeZone(value.toDateTime(), country, format); } else { return ""; } case'z': // timezone if (value.hasTimezone()) { int tz = value.getTimezoneInMinutes(); FastStringBuffer fsb = new FastStringBuffer(FastStringBuffer.TINY); fsb.append("GMT"); if (tz != 0) { CalendarValue.appendTimezone(tz, fsb); } int comma = format.indexOf(','); int min = 0; if (comma > 0) { String widths = format.substring(comma); int[] range = getWidths(widths); min = range[0]; } if (min < 6) { if (tz % 60 == 0) { // No minutes component in timezone fsb.setLength(fsb.length() - 3); } } if (min < fsb.length() - 3) { if (fsb.charAt(4) == '0') { fsb.removeCharAt(4); } } return fsb; } else { return ""; } case'F': // day of week if (ignoreDate) { XPathException error = new XPathException("In formatTime(): an xs:time value does not contain day-of-week component"); error.setErrorCode("XTDE1350"); error.setXPathContext(context); throw error; } else { int day = DateValue.getDayOfWeek(dtvalue.getYear(), dtvalue.getMonth(), dtvalue.getDay()); return formatNumber(component, day, format, defaultFormat, numberer, context); } case'P': // am/pm marker if (ignoreTime) { XPathException error = new XPathException("In formatDate(): an xs:date value does not contain an am/pm component"); error.setErrorCode("XTDE1350"); error.setXPathContext(context); throw error; } else { int minuteOfDay = dtvalue.getHour() * 60 + dtvalue.getMinute(); return formatNumber(component, minuteOfDay, format, defaultFormat, numberer, context); } case'C': // calendar return numberer.getCalendarName("AD"); case'E': // era if (ignoreDate) { XPathException error = new XPathException("In formatTime(): an xs:time value does not contain an AD/BC component"); error.setErrorCode("XTDE1350"); error.setXPathContext(context); throw error; } else { int year = dtvalue.getYear(); return numberer.getEraName(year); } default: XPathException e = new XPathException("Unknown formatDate/time component specifier '" + format.charAt(0) + '\''); e.setErrorCode("XTDE1340"); e.setXPathContext(context); throw e; } } private static RegExp formatPattern = RegExp.compile("([^,]*)(,.*)?"); // Note, the group numbers are different from above private static RegExp widthPattern = RegExp.compile(",(\\*|[0-9]+)(\\-(\\*|[0-9]+))?"); private static RegExp alphanumericPattern = RegExp.compile("([A-Za-z0-9])*"); private static RegExp digitsPattern = RegExp.compile("[0-9]+"); // was [0-9]* but this always returned a match - java: "\\p{Nd}*" private static CharSequence formatNumber(String component, int value, String format, boolean defaultFormat, Numberer numberer, XPathContext context) throws XPathException { MatchResult matcher = formatPattern.exec(format); if (matcher == null) { XPathException error = new XPathException("Unrecognized format picture [" + component + format + ']'); error.setErrorCode("XTDE1340"); error.setXPathContext(context); throw error; } //String primary = matcher.group(1); //String modifier = matcher.group(2); String primary = matcher.getGroup(1); if (primary == null) { primary = ""; } String modifier = null; if (primary.endsWith("t")) { primary = primary.substring(0, primary.length()-1); modifier = "t"; } else if (primary.endsWith("o")) { primary = primary.substring(0, primary.length()-1); modifier = "o"; } String letterValue = ("t".equals(modifier) ? "traditional" : null); String ordinal = ("o".equals(modifier) ? numberer.getOrdinalSuffixForDateTime(component) : null); String widths = matcher.getGroup(2); if (widths == null) { widths = ""; } if (!alphanumericPattern.test(primary)) { XPathException error = new XPathException("In format picture at '" + primary + "', primary format must be alphanumeric"); error.setErrorCode("XTDE1340"); error.setXPathContext(context); throw error; } int min = 1; int max = Integer.MAX_VALUE; if (widths==null || "".equals(widths)) { if (digitsPattern.test(primary)) { int len = StringValue.getStringLength(primary); if (len > 1) { // "A format token containing leading zeroes, such as 001, sets the minimum and maximum width..." // We interpret this literally: a format token of "1" does not set a maximum, because it would // cause the year 2006 to be formatted as "6". min = len; max = len; } } } else if (primary.equals("I") || primary.equals("i")) { // for roman numerals, ignore the width specifier min = 1; max = Integer.MAX_VALUE; } else { int[] range = getWidths(widths); min = range[0]; max = range[1]; if (defaultFormat) { // if format was defaulted, the explicit widths override the implicit format if (primary.endsWith("1") && min != primary.length()) { FastStringBuffer sb = new FastStringBuffer(min+1); for (int i=1; i<min; i++) { sb.append('0'); } sb.append('1'); primary = sb.toString(); } } } if ("P".equals(component)) { // A.M./P.M. can only be formatted as a name if (!("N".equals(primary) || "n".equals(primary) || "Nn".equals(primary))) { primary = "n"; } if (max == Integer.MAX_VALUE) { // if no max specified, use 4. An explicit greater value allows use of "noon" and "midnight" max = 4; } } else if ("f".equals(component)) { // value is supplied as integer number of microseconds String s; if (value==0) { s = "0"; } else { s = ((1000000 + value) + "").substring(1); if (s.length() > max) { DecimalValue dec = new DecimalValue(new BigDecimal("0." + s)); dec = (DecimalValue)dec.roundHalfToEven(max); s = dec.getStringValue(); if (s.length() > 2) { // strip the ".0" s = s.substring(2); } else { // fractional seconds value was 0 s = ""; } } } while (s.length() < min) { s = s + '0'; } while (s.length() > min && s.charAt(s.length()-1) == '0') { s = s.substring(0, s.length()-1); } return s; } if ("N".equals(primary) || "n".equals(primary) || "Nn".equals(primary)) { String s = ""; if ("M".equals(component)) { s = numberer.monthName(value, min, max); } else if ("F".equals(component)) { s = numberer.dayName(value, min, max); } else if ("P".equals(component)) { s = numberer.halfDayName(value, min, max); } else { primary = "1"; } if ("N".equals(primary)) { return s.toUpperCase(); } else if ("n".equals(primary)) { return s.toLowerCase(); } else { return s; } } String s = numberer.format(value, primary, null, letterValue, ordinal); int len = StringValue.getStringLength(s); while (len < min) { // assert: this can only happen as a result of width specifiers, in which case we're using ASCII digits s = ("00000000"+s).substring(s.length()+8-min); len = StringValue.getStringLength(s); } if (len > max) { // the year is the only field we allow to be truncated if (component.charAt(0) == 'Y') { if (len == s.length()) { // no wide characters s = s.substring(s.length() - max); } else { // assert: each character must be two bytes long s = s.substring(s.length() - 2*max); } } } return s; } private static int[] getWidths(String widths) throws XPathException { try { int min = -1; int max = -1; if (!"".equals(widths)) { MatchResult widthMatcher = widthPattern.exec(widths); if (widthMatcher != null) { String smin = widthMatcher.getGroup(1); if (smin==null || "".equals(smin) || "*".equals(smin)) { min = 1; } else { min = Integer.parseInt(smin); } String smax = widthMatcher.getGroup(3); if (smax==null || "".equals(smax) || "*".equals(smax)) { max = Integer.MAX_VALUE; } else { max = Integer.parseInt(smax); } } else { XPathException error = new XPathException("Unrecognized width specifier " + Err.wrap(widths, Err.VALUE)); error.setErrorCode("XTDE1340"); throw error; } } if (min>max && max!=-1) { XPathException e = new XPathException("Minimum width in date/time picture exceeds maximum width"); e.setErrorCode("XTDE1340"); throw e; } int[] result = new int[2]; result[0] = min; result[1] = max; return result; } catch (NumberFormatException err) { XPathException e = new XPathException("Invalid integer used as width in date/time picture"); e.setErrorCode("XTDE1340"); throw e; } } private static String getNamedTimeZone(DateTimeValue value, String country, String format) throws XPathException { int min = 1; int comma = format.indexOf(','); if (comma > 0) { String widths = format.substring(comma); int[] range = getWidths(widths); min = range[0]; } // if (format.charAt(0) == 'N' || format.charAt(0) == 'n') { // if (min <= 5) { // String tzname = NamedTimeZone.getTimeZoneNameForDate(value, country); // if (format.charAt(0) == 'n') { // tzname = tzname.toLowerCase(); // } // return tzname; // } else { // return NamedTimeZone.getOlsenTimeZoneName(value, country); // } // } FastStringBuffer sbz = new FastStringBuffer(8); value.appendTimezone(sbz); return sbz.toString(); } } // This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. // If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. // This Source Code Form is “Incompatible With Secondary Licenses”, as defined by the Mozilla Public License, v. 2.0.