001: package net.sf.saxon.functions;
002:
003: import net.sf.saxon.expr.ExpressionTool;
004: import net.sf.saxon.expr.StaticContext;
005: import net.sf.saxon.expr.XPathContext;
006: import net.sf.saxon.instruct.NumberInstruction;
007: import net.sf.saxon.number.Numberer;
008: import net.sf.saxon.om.FastStringBuffer;
009: import net.sf.saxon.om.Item;
010: import net.sf.saxon.trans.DynamicError;
011: import net.sf.saxon.trans.StaticError;
012: import net.sf.saxon.trans.XPathException;
013: import net.sf.saxon.value.*;
014:
015: import java.util.Locale;
016: import java.util.regex.Matcher;
017: import java.util.regex.Pattern;
018:
019: /**
020: * Implement the format-date() function in XPath 2.0
021: */
022:
023: public class FormatDate extends SystemFunction implements XSLTFunction {
024:
025: public void checkArguments(StaticContext env) throws XPathException {
026: int numArgs = argument.length;
027: if (numArgs != 2 && numArgs != 5) {
028: throw new StaticError("Function "
029: + getDisplayName(env.getNamePool())
030: + " must have either two or five arguments",
031: ExpressionTool.getLocator(this ));
032: }
033: super .checkArguments(env);
034: }
035:
036: /**
037: * Evaluate in a general context
038: */
039:
040: public Item evaluateItem(XPathContext context)
041: throws XPathException {
042: CalendarValue value = (CalendarValue) argument[0]
043: .evaluateItem(context);
044: if (value == null) {
045: return null;
046: }
047: String format = argument[1].evaluateItem(context)
048: .getStringValue();
049:
050: String language;
051:
052: StringValue calendarVal = null;
053: StringValue countryVal = null;
054: if (argument.length > 2) {
055: AtomicValue languageVal = (AtomicValue) argument[2]
056: .evaluateItem(context);
057: calendarVal = (StringValue) argument[3]
058: .evaluateItem(context);
059: countryVal = (StringValue) argument[4]
060: .evaluateItem(context);
061: if (languageVal == null) {
062: language = Locale.getDefault().getLanguage();
063: } else {
064: language = languageVal.getStringValue();
065: if (language.length() >= 2) {
066: language = language.substring(0, 2);
067: } else {
068: language = Locale.getDefault().getLanguage();
069: }
070: }
071: } else {
072: language = Locale.getDefault().getLanguage();
073: }
074:
075: String country = (countryVal == null ? null : countryVal
076: .getStringValue());
077: CharSequence result = formatDate(value, format, language,
078: country, context);
079: if (calendarVal != null) {
080: String cal = calendarVal.getStringValue();
081: if (!cal.equals("AD") && !cal.equals("ISO")) {
082: result = "[Calendar: AD]" + result.toString();
083: }
084: }
085: return new StringValue(result);
086: }
087:
088: /**
089: * This method analyzes the formatting picture and delegates the work of formatting
090: * individual parts of the date.
091: */
092:
093: private static CharSequence formatDate(CalendarValue value,
094: String format, String language, String country,
095: XPathContext context) throws XPathException {
096:
097: Numberer numberer = NumberInstruction.makeNumberer(language,
098: country, context);
099: FastStringBuffer sb = new FastStringBuffer(32);
100: if (!numberer.getClass().getName().endsWith(
101: "Numberer_" + language)) {
102: sb.append("[Language: en]");
103: }
104: int i = 0;
105: while (true) {
106: while (i < format.length() && format.charAt(i) != '[') {
107: sb.append(format.charAt(i));
108: if (format.charAt(i) == ']') {
109: i++;
110: if (i == format.length() || format.charAt(i) != ']') {
111: DynamicError e = new DynamicError(
112: "Closing ']' in date picture must be written as ']]'");
113: e.setErrorCode("XTDE1340");
114: e.setXPathContext(context);
115: throw e;
116: }
117: }
118: i++;
119: }
120: if (i == format.length()) {
121: break;
122: }
123: // look for '[['
124: i++;
125: if (format.charAt(i) == '[') {
126: sb.append('[');
127: i++;
128: } else {
129: int close = format.indexOf("]", i);
130: if (close == -1) {
131: DynamicError e = new DynamicError(
132: "Date format contains a '[' with no matching ']'");
133: e.setErrorCode("XTDE1340");
134: e.setXPathContext(context);
135: throw e;
136: }
137: String componentFormat = format.substring(i, close);
138: sb.append(formatComponent(value, Whitespace
139: .removeAllWhitespace(componentFormat),
140: numberer, context));
141: i = close + 1;
142: }
143: }
144: return sb;
145: }
146:
147: private static Pattern componentPattern = Pattern
148: .compile("([YMDdWwFHhmsfZzPCE])\\s*(.*)");
149:
150: private static CharSequence formatComponent(CalendarValue value,
151: CharSequence specifier, Numberer numberer,
152: XPathContext context) throws XPathException {
153: boolean ignoreDate = (value instanceof TimeValue);
154: boolean ignoreTime = (value instanceof DateValue);
155: DateTimeValue dtvalue = value.toDateTime();
156:
157: Matcher matcher = componentPattern.matcher(specifier);
158: if (!matcher.matches()) {
159: DynamicError error = new DynamicError(
160: "Unrecognized date/time component [" + specifier
161: + ']');
162: error.setErrorCode("XTDE1340");
163: error.setXPathContext(context);
164: throw error;
165: }
166: String component = matcher.group(1);
167: String format = matcher.group(2);
168: if (format == null) {
169: format = "";
170: }
171: boolean defaultFormat = false;
172: if ("".equals(format) || format.startsWith(",")) {
173: defaultFormat = true;
174: switch (component.charAt(0)) {
175: case 'F':
176: format = "Nn" + format;
177: break;
178: case 'P':
179: format = 'n' + format;
180: break;
181: case 'C':
182: case 'E':
183: format = 'N' + format;
184: break;
185: case 'm':
186: case 's':
187: format = "01" + format;
188: break;
189: default:
190: format = '1' + format;
191: }
192: }
193:
194: switch (component.charAt(0)) {
195: case 'Y': // year
196: if (ignoreDate) {
197: DynamicError error = new DynamicError(
198: "In formatTime(): an xs:time value does not contain a year component");
199: error.setErrorCode("XTDE1350");
200: error.setXPathContext(context);
201: throw error;
202: } else {
203: int year = dtvalue.getYear();
204: if (year < 0) {
205: year = 1 - year;
206: }
207: return formatNumber(component, year, format,
208: defaultFormat, numberer, context);
209: }
210: case 'M': // month
211: if (ignoreDate) {
212: DynamicError error = new DynamicError(
213: "In formatTime(): an xs:time value does not contain a month component");
214: error.setErrorCode("XTDE1350");
215: error.setXPathContext(context);
216: throw error;
217: } else {
218: int month = dtvalue.getMonth();
219: return formatNumber(component, month, format,
220: defaultFormat, numberer, context);
221: }
222: case 'D': // day in month
223: if (ignoreDate) {
224: DynamicError error = new DynamicError(
225: "In formatTime(): an xs:time value does not contain a day component");
226: error.setErrorCode("XTDE1350");
227: error.setXPathContext(context);
228: throw error;
229: } else {
230: int day = dtvalue.getDay();
231: return formatNumber(component, day, format,
232: defaultFormat, numberer, context);
233: }
234: case 'd': // day in year
235: if (ignoreDate) {
236: DynamicError error = new DynamicError(
237: "In formatTime(): an xs:time value does not contain a day component");
238: error.setErrorCode("XTDE1350");
239: error.setXPathContext(context);
240: throw error;
241: } else {
242: int day = DateValue.getDayWithinYear(dtvalue.getYear(),
243: dtvalue.getMonth(), dtvalue.getDay());
244: return formatNumber(component, day, format,
245: defaultFormat, numberer, context);
246: }
247: case 'W': // week of year
248: if (ignoreDate) {
249: DynamicError error = new DynamicError(
250: "In formatTime(): cannot obtain the week number from an xs:time value");
251: error.setErrorCode("XTDE1350");
252: error.setXPathContext(context);
253: throw error;
254: } else {
255: int week = DateValue.getWeekNumber(dtvalue.getYear(),
256: dtvalue.getMonth(), dtvalue.getDay());
257: return formatNumber(component, week, format,
258: defaultFormat, numberer, context);
259: }
260: case 'w': // week in month
261: if (ignoreDate) {
262: DynamicError error = new DynamicError(
263: "In formatTime(): cannot obtain the week number from an xs:time value");
264: error.setErrorCode("XTDE1350");
265: error.setXPathContext(context);
266: throw error;
267: } else {
268: int week = DateValue.getWeekNumberWithinMonth(dtvalue
269: .getYear(), dtvalue.getMonth(), dtvalue
270: .getDay());
271: return formatNumber(component, week, format,
272: defaultFormat, numberer, context);
273: }
274: case 'H': // hour in day
275: if (ignoreTime) {
276: DynamicError error = new DynamicError(
277: "In formatDate(): an xs:date value does not contain an hour component");
278: error.setErrorCode("XTDE1350");
279: error.setXPathContext(context);
280: throw error;
281: } else {
282: IntegerValue hour = (IntegerValue) value
283: .getComponent(Component.HOURS);
284: return formatNumber(component, (int) hour.longValue(),
285: format, defaultFormat, numberer, context);
286: }
287: case 'h': // hour in half-day (12 hour clock)
288: if (ignoreTime) {
289: DynamicError error = new DynamicError(
290: "In formatDate(): an xs:date value does not contain an hour component");
291: error.setErrorCode("XTDE1350");
292: error.setXPathContext(context);
293: throw error;
294: } else {
295: IntegerValue hour = (IntegerValue) value
296: .getComponent(Component.HOURS);
297: int hr = (int) hour.longValue();
298: if (hr > 12) {
299: hr = hr - 12;
300: }
301: if (hr == 0) {
302: hr = 12;
303: }
304: return formatNumber(component, hr, format,
305: defaultFormat, numberer, context);
306: }
307: case 'm': // minutes
308: if (ignoreTime) {
309: DynamicError error = new DynamicError(
310: "In formatDate(): an xs:date value does not contain a minutes component");
311: error.setErrorCode("XTDE1350");
312: error.setXPathContext(context);
313: throw error;
314: } else {
315: IntegerValue min = (IntegerValue) value
316: .getComponent(Component.MINUTES);
317: return formatNumber(component, (int) min.longValue(),
318: format, defaultFormat, numberer, context);
319: }
320: case 's': // seconds
321: if (ignoreTime) {
322: DynamicError error = new DynamicError(
323: "In formatDate(): an xs:date value does not contain a seconds component");
324: error.setErrorCode("XTDE1350");
325: error.setXPathContext(context);
326: throw error;
327: } else {
328: DecimalValue sec = (DecimalValue) value
329: .getComponent(Component.SECONDS);
330: return formatNumber(component, sec.getValue()
331: .intValue(), format, defaultFormat, numberer,
332: context);
333: }
334: case 'f': // fractional seconds
335: // ignore the format
336: if (ignoreTime) {
337: DynamicError error = new DynamicError(
338: "In formatDate(): an xs:date value does not contain a fractional seconds component");
339: error.setErrorCode("XTDE1350");
340: error.setXPathContext(context);
341: throw error;
342: } else {
343: int micros = (int) ((IntegerValue) value
344: .getComponent(Component.MICROSECONDS))
345: .longValue();
346: return formatNumber(component, micros, format,
347: defaultFormat, numberer, context);
348: }
349: case 'Z': // timezone in +hh:mm format, unless format=N in which case use timezone name
350: if (value.hasTimezone()) {
351: if (format.startsWith("N")) {
352: return numberer.getTimezoneName(value
353: .getTimezoneInMinutes());
354: } else if (format.startsWith("n")) {
355: return numberer.getTimezoneName(
356: value.getTimezoneInMinutes()).toLowerCase();
357: } else {
358: FastStringBuffer sbz = new FastStringBuffer(8);
359: value.appendTimezone(sbz);
360: return sbz.toString();
361: }
362: } else {
363: return "";
364: }
365: case 'z': // timezone
366: if (value.hasTimezone()) {
367: int tz = value.getTimezoneInMinutes();
368: return "GMT"
369: + (tz == 0 ? ""
370: : ((tz > 0 ? "+" : "-")
371: + Math.abs(tz / 60) + (tz % 60 == 0 ? ""
372: : ".5")));
373: } else {
374: return "";
375: }
376: case 'F': // day of week
377: if (ignoreDate) {
378: DynamicError error = new DynamicError(
379: "In formatTime(): an xs:time value does not contain day-of-week component");
380: error.setErrorCode("XTDE1350");
381: error.setXPathContext(context);
382: throw error;
383: } else {
384: int day = DateValue.getDayOfWeek(dtvalue.getYear(),
385: dtvalue.getMonth(), dtvalue.getDay());
386: return formatNumber(component, day, format,
387: defaultFormat, numberer, context);
388: }
389: case 'P': // am/pm marker
390: if (ignoreTime) {
391: DynamicError error = new DynamicError(
392: "In formatDate(): an xs:date value does not contain an am/pm component");
393: error.setErrorCode("XTDE1350");
394: error.setXPathContext(context);
395: throw error;
396: } else {
397: int minuteOfDay = dtvalue.getHour() * 60
398: + dtvalue.getMinute();
399: return formatNumber(component, minuteOfDay, format,
400: defaultFormat, numberer, context);
401: }
402: case 'C': // calendar
403: return "Gregorian";
404: case 'E': // era
405: if (ignoreDate) {
406: DynamicError error = new DynamicError(
407: "In formatTime(): an xs:time value does not contain an AD/BC component");
408: error.setErrorCode("XTDE1350");
409: error.setXPathContext(context);
410: throw error;
411: } else {
412: int year = dtvalue.getYear();
413: return numberer.getEraName(year);
414: }
415: default:
416: DynamicError e = new DynamicError(
417: "Unknown formatDate/time component specifier '"
418: + format.charAt(0) + '\'');
419: e.setErrorCode("XTDE1340");
420: e.setXPathContext(context);
421: throw e;
422: }
423: }
424:
425: private static Pattern formatPattern = Pattern
426: .compile("([^ot,]*?)([ot]?)(,.*)?");
427:
428: private static Pattern widthPattern = Pattern
429: .compile(",(\\*|[0-9]+)(\\-(\\*|[0-9]+))?");
430:
431: private static Pattern alphanumericPattern = Pattern
432: .compile("(\\p{L}|\\p{N})*");
433:
434: private static CharSequence formatNumber(String component,
435: int value, String format, boolean defaultFormat,
436: Numberer numberer, XPathContext context)
437: throws XPathException {
438: Matcher matcher = formatPattern.matcher(format);
439: if (!matcher.matches()) {
440: DynamicError error = new DynamicError(
441: "Unrecognized format picture [" + component
442: + format + ']');
443: error.setErrorCode("XTDE1340");
444: error.setXPathContext(context);
445: throw error;
446: }
447: String primary = matcher.group(1);
448: String modifier = matcher.group(2);
449: String letterValue = ("t".equals(modifier) ? "traditional"
450: : null);
451: String ordinal = ("o".equals(modifier) ? numberer
452: .getOrdinalSuffixForDateTime(component) : null);
453: String widths = matcher.group(3);
454:
455: if (!alphanumericPattern.matcher(primary).matches()) {
456: DynamicError error = new DynamicError(
457: "In format picture at '" + primary
458: + "', primary format must be alphanumeric");
459: error.setErrorCode("XTDE1340");
460: error.setXPathContext(context);
461: throw error;
462: }
463: int min, max;
464:
465: if (widths == null || "".equals(widths)) {
466: min = 1;
467: max = Integer.MAX_VALUE;
468: } else {
469: int[] range = getWidths(widths, context);
470: min = range[0];
471: max = range[1];
472: if (defaultFormat) {
473: // if format was defaulted, the explicit widths override the implicit format
474: if (primary.endsWith("1") && min != primary.length()) {
475: FastStringBuffer sb = new FastStringBuffer(min + 1);
476: for (int i = 1; i < min; i++) {
477: sb.append('0');
478: }
479: sb.append('1');
480: primary = sb.toString();
481: }
482: }
483: }
484:
485: if ("P".equals(component)) {
486: // A.M./P.M. can only be formatted as a name
487: if (!("N".equals(primary) || "n".equals(primary) || "Nn"
488: .equals(primary))) {
489: primary = "n";
490: }
491: } else if ("f".equals(component)) {
492: // value is supplied as integer number of microseconds
493: String s = ((1000000 + value) + "").substring(1);
494: if (s.length() > max) {
495: s = s.substring(0, max);
496: }
497: while (s.length() < min) {
498: s = s + '0';
499: }
500: while (s.length() > min && s.charAt(s.length() - 1) == '0') {
501: s = s.substring(0, s.length() - 1);
502: }
503: return s;
504: }
505:
506: if ("N".equals(primary) || "n".equals(primary)
507: || "Nn".equals(primary)) {
508: String s = "";
509: if ("M".equals(component)) {
510: s = numberer.monthName(value, min, max);
511: } else if ("F".equals(component)) {
512: s = numberer.dayName(value, min, max);
513: } else if ("P".equals(component)) {
514: s = numberer.halfDayName(value, min, max);
515: } else {
516: primary = "1";
517: }
518: if ("N".equals(primary)) {
519: return s.toUpperCase();
520: } else if ("n".equals(primary)) {
521: return s.toLowerCase();
522: } else {
523: return s;
524: }
525: }
526:
527: String s = numberer.format(value, primary, 0, ",", letterValue,
528: ordinal);
529:
530: while (s.length() < min) {
531: s = ("00000000" + s).substring(s.length() + 8 - min);
532: }
533: if (s.length() > max) {
534: // the year is the only field we allow to be truncated
535: if (component.charAt(0) == 'Y') {
536: s = s.substring(s.length() - max);
537: }
538: }
539: return s;
540: }
541:
542: private static int[] getWidths(String widths, XPathContext context)
543: throws XPathException {
544: try {
545: int min = -1;
546: int max = -1;
547:
548: if (!"".equals(widths)) {
549: Matcher widthMatcher = widthPattern.matcher(widths);
550: if (widthMatcher.matches()) {
551: String smin = widthMatcher.group(1);
552: if (smin == null || "".equals(smin)
553: || "*".equals(smin)) {
554: min = 1;
555: } else {
556: min = Integer.parseInt(smin);
557: }
558: String smax = widthMatcher.group(3);
559: if (smax == null || "".equals(smax)
560: || "*".equals(smax)) {
561: max = Integer.MAX_VALUE;
562: } else {
563: max = Integer.parseInt(smax);
564: }
565: } else {
566: DynamicError error = new DynamicError(
567: "Unrecognized width specifier");
568: error.setErrorCode("XTDE1340");
569: error.setXPathContext(context);
570: throw error;
571: }
572: }
573:
574: if (min > max && max != -1) {
575: DynamicError e = new DynamicError(
576: "Minimum width in date/time picture exceeds maximum width");
577: e.setErrorCode("XTDE1340");
578: e.setXPathContext(context);
579: throw e;
580: }
581: int[] result = new int[2];
582: result[0] = min;
583: result[1] = max;
584: return result;
585: } catch (NumberFormatException err) {
586: DynamicError e = new DynamicError(
587: "Invalid integer used as width in date/time picture");
588: e.setErrorCode("XTDE1340");
589: e.setXPathContext(context);
590: throw e;
591: }
592: }
593:
594: }
595:
596: //
597: // The contents of this file are subject to the Mozilla Public License Version 1.0 (the "License");
598: // you may not use this file except in compliance with the License. You may obtain a copy of the
599: // License at http://www.mozilla.org/MPL/
600: //
601: // Software distributed under the License is distributed on an "AS IS" basis,
602: // WITHOUT WARRANTY OF ANY KIND, either express or implied.
603: // See the License for the specific language governing rights and limitations under the License.
604: //
605: // The Original Code is: all this file.
606: //
607: // The Initial Developer of the Original Code is Michael H. Kay
608: //
609: // Portions created by (your name) are Copyright (C) (your legal entity). All Rights Reserved.
610: //
611: // Contributor(s): none.
612: //
|