001 /*
002 * Licensed to the Apache Software Foundation (ASF) under one
003 * or more contributor license agreements. See the NOTICE file
004 * distributed with this work for additional information
005 * regarding copyright ownership. The ASF licenses this file
006 * to you under the Apache License, Version 2.0 (the "License");
007 * you may not use this file except in compliance with the License.
008 * You may obtain a copy of the License at
009 *
010 * http://www.apache.org/licenses/LICENSE-2.0
011 *
012 * Unless required by applicable law or agreed to in writing, software
013 * distributed under the License is distributed on an "AS IS" BASIS,
014 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
015 * See the License for the specific language governing permissions and
016 * limitations under the License.
017 */
018 /*
019 * $Id: ExsltDatetime.java 1225761 2011-12-30 06:04:51Z mrglavas $
020 */
021
022 package org.apache.xalan.lib;
023
024
025 import java.text.ParseException;
026 import java.text.SimpleDateFormat;
027 import java.util.Calendar;
028 import java.util.Date;
029 import java.util.Locale;
030 import java.util.TimeZone;
031
032 import org.apache.xpath.objects.XBoolean;
033 import org.apache.xpath.objects.XNumber;
034 import org.apache.xpath.objects.XObject;
035
036 /**
037 * This class contains EXSLT dates and times extension functions.
038 * It is accessed by specifying a namespace URI as follows:
039 * <pre>
040 * xmlns:datetime="http://exslt.org/dates-and-times"
041 * </pre>
042 *
043 * The documentation for each function has been copied from the relevant
044 * EXSLT Implementer page.
045 *
046 * @see <a href="http://www.exslt.org/">EXSLT</a>
047 * @xsl.usage general
048 */
049
050 public class ExsltDatetime
051 {
052 // Datetime formats (era and zone handled separately).
053 static final String dt = "yyyy-MM-dd'T'HH:mm:ss";
054 static final String d = "yyyy-MM-dd";
055 static final String gym = "yyyy-MM";
056 static final String gy = "yyyy";
057 static final String gmd = "--MM-dd";
058 static final String gm = "--MM--";
059 static final String gd = "---dd";
060 static final String t = "HH:mm:ss";
061 static final String EMPTY_STR = "";
062
063 /**
064 * The date:date-time function returns the current date and time as a date/time string.
065 * The date/time string that's returned must be a string in the format defined as the
066 * lexical representation of xs:dateTime in
067 * <a href="http://www.w3.org/TR/xmlschema-2/#dateTime">[3.2.7 dateTime]</a> of
068 * <a href="http://www.w3.org/TR/xmlschema-2/">[XML Schema Part 2: Datatypes]</a>.
069 * The date/time format is basically CCYY-MM-DDThh:mm:ss, although implementers should consult
070 * <a href="http://www.w3.org/TR/xmlschema-2/">[XML Schema Part 2: Datatypes]</a> and
071 * <a href="http://www.iso.ch/markete/8601.pdf">[ISO 8601]</a> for details.
072 * The date/time string format must include a time zone, either a Z to indicate Coordinated
073 * Universal Time or a + or - followed by the difference between the difference from UTC
074 * represented as hh:mm.
075 */
076 public static String dateTime()
077 {
078 Calendar cal = Calendar.getInstance();
079 Date datetime = cal.getTime();
080 // Format for date and time.
081 SimpleDateFormat dateFormat = new SimpleDateFormat(dt);
082
083 StringBuffer buff = new StringBuffer(dateFormat.format(datetime));
084 // Must also include offset from UTF.
085 // Get the offset (in milliseconds).
086 int offset = cal.get(Calendar.ZONE_OFFSET) + cal.get(Calendar.DST_OFFSET);
087 // If there is no offset, we have "Coordinated
088 // Universal Time."
089 if (offset == 0)
090 buff.append('Z');
091 else
092 {
093 // Convert milliseconds to hours and minutes
094 int hrs = offset/(60*60*1000);
095 // In a few cases, the time zone may be +/-hh:30.
096 int min = offset%(60*60*1000);
097 char posneg = hrs < 0? '-': '+';
098 buff.append(posneg + formatDigits(hrs) + ':' + formatDigits(min));
099 }
100 return buff.toString();
101 }
102
103 /**
104 * Represent the hours and minutes with two-digit strings.
105 * @param q hrs or minutes.
106 * @return two-digit String representation of hrs or minutes.
107 */
108 private static String formatDigits(int q)
109 {
110 String dd = String.valueOf(Math.abs(q));
111 return dd.length() == 1 ? '0' + dd : dd;
112 }
113
114 /**
115 * The date:date function returns the date specified in the date/time string given
116 * as the argument. If no argument is given, then the current local date/time, as
117 * returned by date:date-time is used as a default argument.
118 * The date/time string that's returned must be a string in the format defined as the
119 * lexical representation of xs:dateTime in
120 * <a href="http://www.w3.org/TR/xmlschema-2/#dateTime">[3.2.7 dateTime]</a> of
121 * <a href="http://www.w3.org/TR/xmlschema-2/">[XML Schema Part 2: Datatypes]</a>.
122 * If the argument is not in either of these formats, date:date returns an empty string ('').
123 * The date/time format is basically CCYY-MM-DDThh:mm:ss, although implementers should consult
124 * <a href="http://www.w3.org/TR/xmlschema-2/">[XML Schema Part 2: Datatypes]</a> and
125 * <a href="http://www.iso.ch/markete/8601.pdf">[ISO 8601]</a> for details.
126 * The date is returned as a string with a lexical representation as defined for xs:date in
127 * [3.2.9 date] of [XML Schema Part 2: Datatypes]. The date format is basically CCYY-MM-DD,
128 * although implementers should consult [XML Schema Part 2: Datatypes] and [ISO 8601] for details.
129 * If no argument is given or the argument date/time specifies a time zone, then the date string
130 * format must include a time zone, either a Z to indicate Coordinated Universal Time or a + or -
131 * followed by the difference between the difference from UTC represented as hh:mm. If an argument
132 * is specified and it does not specify a time zone, then the date string format must not include
133 * a time zone.
134 */
135 public static String date(String datetimeIn)
136 throws ParseException
137 {
138 String[] edz = getEraDatetimeZone(datetimeIn);
139 String leader = edz[0];
140 String datetime = edz[1];
141 String zone = edz[2];
142 if (datetime == null || zone == null)
143 return EMPTY_STR;
144
145 String[] formatsIn = {dt, d};
146 String formatOut = d;
147 Date date = testFormats(datetime, formatsIn);
148 if (date == null) return EMPTY_STR;
149
150 SimpleDateFormat dateFormat = new SimpleDateFormat(formatOut);
151 dateFormat.setLenient(false);
152 String dateOut = dateFormat.format(date);
153 if (dateOut.length() == 0)
154 return EMPTY_STR;
155 else
156 return (leader + dateOut + zone);
157 }
158
159
160 /**
161 * See above.
162 */
163 public static String date()
164 {
165 String datetime = dateTime();
166 String date = datetime.substring(0, datetime.indexOf("T"));
167 String zone = datetime.substring(getZoneStart(datetime));
168 return (date + zone);
169 }
170
171 /**
172 * The date:time function returns the time specified in the date/time string given
173 * as the argument. If no argument is given, then the current local date/time, as
174 * returned by date:date-time is used as a default argument.
175 * The date/time string that's returned must be a string in the format defined as the
176 * lexical representation of xs:dateTime in
177 * <a href="http://www.w3.org/TR/xmlschema-2/#dateTime">[3.2.7 dateTime]</a> of
178 * <a href="http://www.w3.org/TR/xmlschema-2/">[XML Schema Part 2: Datatypes]</a>.
179 * If the argument string is not in this format, date:time returns an empty string ('').
180 * The date/time format is basically CCYY-MM-DDThh:mm:ss, although implementers should consult
181 * <a href="http://www.w3.org/TR/xmlschema-2/">[XML Schema Part 2: Datatypes]</a> and
182 * <a href="http://www.iso.ch/markete/8601.pdf">[ISO 8601]</a> for details.
183 * The date is returned as a string with a lexical representation as defined for xs:time in
184 * <a href="http://www.w3.org/TR/xmlschema-2/#time">[3.2.8 time]</a> of [XML Schema Part 2: Datatypes].
185 * The time format is basically hh:mm:ss, although implementers should consult [XML Schema Part 2:
186 * Datatypes] and [ISO 8601] for details.
187 * If no argument is given or the argument date/time specifies a time zone, then the time string
188 * format must include a time zone, either a Z to indicate Coordinated Universal Time or a + or -
189 * followed by the difference between the difference from UTC represented as hh:mm. If an argument
190 * is specified and it does not specify a time zone, then the time string format must not include
191 * a time zone.
192 */
193 public static String time(String timeIn)
194 throws ParseException
195 {
196 String[] edz = getEraDatetimeZone(timeIn);
197 String time = edz[1];
198 String zone = edz[2];
199 if (time == null || zone == null)
200 return EMPTY_STR;
201
202 String[] formatsIn = {dt, d, t};
203 String formatOut = t;
204 Date date = testFormats(time, formatsIn);
205 if (date == null) return EMPTY_STR;
206 SimpleDateFormat dateFormat = new SimpleDateFormat(formatOut);
207 String out = dateFormat.format(date);
208 return (out + zone);
209 }
210
211 /**
212 * See above.
213 */
214 public static String time()
215 {
216 String datetime = dateTime();
217 String time = datetime.substring(datetime.indexOf("T")+1);
218
219 // The datetime() function returns the zone on the datetime string. If we
220 // append it, we get the zone substring duplicated.
221 // Fix for JIRA 2013
222
223 // String zone = datetime.substring(getZoneStart(datetime));
224 // return (time + zone);
225 return (time);
226 }
227
228 /**
229 * The date:year function returns the year of a date as a number. If no
230 * argument is given, then the current local date/time, as returned by
231 * date:date-time is used as a default argument.
232 * The date/time string specified as the first argument must be a right-truncated
233 * string in the format defined as the lexical representation of xs:dateTime in one
234 * of the formats defined in
235 * <a href="http://www.w3.org/TR/xmlschema-2/">[XML Schema Part 2: Datatypes]</a>.
236 * The permitted formats are as follows:
237 * xs:dateTime (CCYY-MM-DDThh:mm:ss)
238 * xs:date (CCYY-MM-DD)
239 * xs:gYearMonth (CCYY-MM)
240 * xs:gYear (CCYY)
241 * If the date/time string is not in one of these formats, then NaN is returned.
242 */
243 public static double year(String datetimeIn)
244 throws ParseException
245 {
246 String[] edz = getEraDatetimeZone(datetimeIn);
247 boolean ad = edz[0].length() == 0; // AD (Common Era -- empty leader)
248 String datetime = edz[1];
249 if (datetime == null)
250 return Double.NaN;
251
252 String[] formats = {dt, d, gym, gy};
253 double yr = getNumber(datetime, formats, Calendar.YEAR);
254 if (ad || yr == Double.NaN)
255 return yr;
256 else
257 return -yr;
258 }
259
260 /**
261 * See above.
262 */
263 public static double year()
264 {
265 Calendar cal = Calendar.getInstance();
266 return cal.get(Calendar.YEAR);
267 }
268
269 /**
270 * The date:month-in-year function returns the month of a date as a number. If no argument
271 * is given, then the current local date/time, as returned by date:date-time is used
272 * as a default argument.
273 * The date/time string specified as the first argument is a left or right-truncated
274 * string in the format defined as the lexical representation of xs:dateTime in one of
275 * the formats defined in
276 * <a href="http://www.w3.org/TR/xmlschema-2/">[XML Schema Part 2: Datatypes]</a>.
277 * The permitted formats are as follows:
278 * xs:dateTime (CCYY-MM-DDThh:mm:ss)
279 * xs:date (CCYY-MM-DD)
280 * xs:gYearMonth (CCYY-MM)
281 * xs:gMonth (--MM--)
282 * xs:gMonthDay (--MM-DD)
283 * If the date/time string is not in one of these formats, then NaN is returned.
284 */
285 public static double monthInYear(String datetimeIn)
286 throws ParseException
287 {
288 String[] edz = getEraDatetimeZone(datetimeIn);
289 String datetime = edz[1];
290 if (datetime == null)
291 return Double.NaN;
292
293 String[] formats = {dt, d, gym, gm, gmd};
294 return getNumber(datetime, formats, Calendar.MONTH) + 1;
295 }
296
297 /**
298 * See above.
299 */
300 public static double monthInYear()
301 {
302 Calendar cal = Calendar.getInstance();
303 return cal.get(Calendar.MONTH) + 1;
304 }
305
306 /**
307 * The date:week-in-year function returns the week of the year as a number. If no argument
308 * is given, then the current local date/time, as returned by date:date-time is used as the
309 * default argument. For the purposes of numbering, counting follows ISO 8601: week 1 in a year
310 * is the week containing the first Thursday of the year, with new weeks beginning on a Monday.
311 * The date/time string specified as the argument is a right-truncated string in the format
312 * defined as the lexical representation of xs:dateTime in one of the formats defined in
313 * <a href="http://www.w3.org/TR/xmlschema-2/">[XML Schema Part 2: Datatypes]</a>. The
314 * permitted formats are as follows:
315 * xs:dateTime (CCYY-MM-DDThh:mm:ss)
316 * xs:date (CCYY-MM-DD)
317 * If the date/time string is not in one of these formats, then NaN is returned.
318 */
319 public static double weekInYear(String datetimeIn)
320 throws ParseException
321 {
322 String[] edz = getEraDatetimeZone(datetimeIn);
323 String datetime = edz[1];
324 if (datetime == null)
325 return Double.NaN;
326
327 String[] formats = {dt, d};
328 return getNumber(datetime, formats, Calendar.WEEK_OF_YEAR);
329 }
330
331 /**
332 * See above.
333 */
334 public static double weekInYear()
335 {
336 Calendar cal = Calendar.getInstance();
337 return cal.get(Calendar.WEEK_OF_YEAR);
338 }
339
340 /**
341 * The date:day-in-year function returns the day of a date in a year
342 * as a number. If no argument is given, then the current local
343 * date/time, as returned by date:date-time is used the default argument.
344 * The date/time string specified as the argument is a right-truncated
345 * string in the format defined as the lexical representation of xs:dateTime
346 * in one of the formats defined in
347 * <a href="http://www.w3.org/TR/xmlschema-2/">[XML Schema Part 2: Datatypes]</a>.
348 * The permitted formats are as follows:
349 * xs:dateTime (CCYY-MM-DDThh:mm:ss)
350 * xs:date (CCYY-MM-DD)
351 * If the date/time string is not in one of these formats, then NaN is returned.
352 */
353 public static double dayInYear(String datetimeIn)
354 throws ParseException
355 {
356 String[] edz = getEraDatetimeZone(datetimeIn);
357 String datetime = edz[1];
358 if (datetime == null)
359 return Double.NaN;
360
361 String[] formats = {dt, d};
362 return getNumber(datetime, formats, Calendar.DAY_OF_YEAR);
363 }
364
365 /**
366 * See above.
367 */
368 public static double dayInYear()
369 {
370 Calendar cal = Calendar.getInstance();
371 return cal.get(Calendar.DAY_OF_YEAR);
372 }
373
374
375 /**
376 * The date:day-in-month function returns the day of a date as a number.
377 * If no argument is given, then the current local date/time, as returned
378 * by date:date-time is used the default argument.
379 * The date/time string specified as the argument is a left or right-truncated
380 * string in the format defined as the lexical representation of xs:dateTime
381 * in one of the formats defined in
382 * <a href="http://www.w3.org/TR/xmlschema-2/">[XML Schema Part 2: Datatypes]</a>.
383 * The permitted formats are as follows:
384 * xs:dateTime (CCYY-MM-DDThh:mm:ss)
385 * xs:date (CCYY-MM-DD)
386 * xs:gMonthDay (--MM-DD)
387 * xs:gDay (---DD)
388 * If the date/time string is not in one of these formats, then NaN is returned.
389 */
390 public static double dayInMonth(String datetimeIn)
391 throws ParseException
392 {
393 String[] edz = getEraDatetimeZone(datetimeIn);
394 String datetime = edz[1];
395 String[] formats = {dt, d, gmd, gd};
396 double day = getNumber(datetime, formats, Calendar.DAY_OF_MONTH);
397 return day;
398 }
399
400 /**
401 * See above.
402 */
403 public static double dayInMonth()
404 {
405 Calendar cal = Calendar.getInstance();
406 return cal.get(Calendar.DAY_OF_MONTH);
407 }
408
409 /**
410 * The date:day-of-week-in-month function returns the day-of-the-week
411 * in a month of a date as a number (e.g. 3 for the 3rd Tuesday in May).
412 * If no argument is given, then the current local date/time, as returned
413 * by date:date-time is used the default argument.
414 * The date/time string specified as the argument is a right-truncated string
415 * in the format defined as the lexical representation of xs:dateTime in one
416 * of the formats defined in
417 * <a href="http://www.w3.org/TR/xmlschema-2/">[XML Schema Part 2: Datatypes]</a>.
418 * The permitted formats are as follows:
419 * xs:dateTime (CCYY-MM-DDThh:mm:ss)
420 * xs:date (CCYY-MM-DD)
421 * If the date/time string is not in one of these formats, then NaN is returned.
422 */
423 public static double dayOfWeekInMonth(String datetimeIn)
424 throws ParseException
425 {
426 String[] edz = getEraDatetimeZone(datetimeIn);
427 String datetime = edz[1];
428 if (datetime == null)
429 return Double.NaN;
430
431 String[] formats = {dt, d};
432 return getNumber(datetime, formats, Calendar.DAY_OF_WEEK_IN_MONTH);
433 }
434
435 /**
436 * See above.
437 */
438 public static double dayOfWeekInMonth()
439 {
440 Calendar cal = Calendar.getInstance();
441 return cal.get(Calendar.DAY_OF_WEEK_IN_MONTH);
442 }
443
444
445 /**
446 * The date:day-in-week function returns the day of the week given in a
447 * date as a number. If no argument is given, then the current local date/time,
448 * as returned by date:date-time is used the default argument.
449 * The date/time string specified as the argument is a right-truncated string
450 * in the format defined as the lexical representation of xs:dateTime in one
451 * of the formats defined in
452 * <a href="http://www.w3.org/TR/xmlschema-2/">[XML Schema Part 2: Datatypes]</a>.
453 * The permitted formats are as follows:
454 * xs:dateTime (CCYY-MM-DDThh:mm:ss)
455 * xs:date (CCYY-MM-DD)
456 * If the date/time string is not in one of these formats, then NaN is returned.
457 The numbering of days of the week starts at 1 for Sunday, 2 for Monday and so on up to 7 for Saturday.
458 */
459 public static double dayInWeek(String datetimeIn)
460 throws ParseException
461 {
462 String[] edz = getEraDatetimeZone(datetimeIn);
463 String datetime = edz[1];
464 if (datetime == null)
465 return Double.NaN;
466
467 String[] formats = {dt, d};
468 return getNumber(datetime, formats, Calendar.DAY_OF_WEEK);
469 }
470
471 /**
472 * See above.
473 */
474 public static double dayInWeek()
475 {
476 Calendar cal = Calendar.getInstance();
477 return cal.get(Calendar.DAY_OF_WEEK);
478 }
479
480 /**
481 * The date:hour-in-day function returns the hour of the day as a number.
482 * If no argument is given, then the current local date/time, as returned
483 * by date:date-time is used the default argument.
484 * The date/time string specified as the argument is a right-truncated
485 * string in the format defined as the lexical representation of xs:dateTime
486 * in one of the formats defined in
487 * <a href="http://www.w3.org/TR/xmlschema-2/">[XML Schema Part 2: Datatypes]</a>.
488 * The permitted formats are as follows:
489 * xs:dateTime (CCYY-MM-DDThh:mm:ss)
490 * xs:time (hh:mm:ss)
491 * If the date/time string is not in one of these formats, then NaN is returned.
492 */
493 public static double hourInDay(String datetimeIn)
494 throws ParseException
495 {
496 String[] edz = getEraDatetimeZone(datetimeIn);
497 String datetime = edz[1];
498 if (datetime == null)
499 return Double.NaN;
500
501 String[] formats = {dt, t};
502 return getNumber(datetime, formats, Calendar.HOUR_OF_DAY);
503 }
504
505 /**
506 * See above.
507 */
508 public static double hourInDay()
509 {
510 Calendar cal = Calendar.getInstance();
511 return cal.get(Calendar.HOUR_OF_DAY);
512 }
513
514 /**
515 * The date:minute-in-hour function returns the minute of the hour
516 * as a number. If no argument is given, then the current local
517 * date/time, as returned by date:date-time is used the default argument.
518 * The date/time string specified as the argument is a right-truncated
519 * string in the format defined as the lexical representation of xs:dateTime
520 * in one of the formats defined in
521 * <a href="http://www.w3.org/TR/xmlschema-2/">[XML Schema Part 2: Datatypes]</a>.
522 * The permitted formats are as follows:
523 * xs:dateTime (CCYY-MM-DDThh:mm:ss)
524 * xs:time (hh:mm:ss)
525 * If the date/time string is not in one of these formats, then NaN is returned.
526 */
527 public static double minuteInHour(String datetimeIn)
528 throws ParseException
529 {
530 String[] edz = getEraDatetimeZone(datetimeIn);
531 String datetime = edz[1];
532 if (datetime == null)
533 return Double.NaN;
534
535 String[] formats = {dt,t};
536 return getNumber(datetime, formats, Calendar.MINUTE);
537 }
538
539 /**
540 * See above.
541 */
542 public static double minuteInHour()
543 {
544 Calendar cal = Calendar.getInstance();
545 return cal.get(Calendar.MINUTE);
546 }
547
548 /**
549 * The date:second-in-minute function returns the second of the minute
550 * as a number. If no argument is given, then the current local
551 * date/time, as returned by date:date-time is used the default argument.
552 * The date/time string specified as the argument is a right-truncated
553 * string in the format defined as the lexical representation of xs:dateTime
554 * in one of the formats defined in
555 * <a href="http://www.w3.org/TR/xmlschema-2/">[XML Schema Part 2: Datatypes]</a>.
556 * The permitted formats are as follows:
557 * xs:dateTime (CCYY-MM-DDThh:mm:ss)
558 * xs:time (hh:mm:ss)
559 * If the date/time string is not in one of these formats, then NaN is returned.
560 */
561 public static double secondInMinute(String datetimeIn)
562 throws ParseException
563 {
564 String[] edz = getEraDatetimeZone(datetimeIn);
565 String datetime = edz[1];
566 if (datetime == null)
567 return Double.NaN;
568
569 String[] formats = {dt, t};
570 return getNumber(datetime, formats, Calendar.SECOND);
571 }
572
573 /**
574 * See above.
575 */
576 public static double secondInMinute()
577 {
578 Calendar cal = Calendar.getInstance();
579 return cal.get(Calendar.SECOND);
580 }
581
582 /**
583 * The date:leap-year function returns true if the year given in a date
584 * is a leap year. If no argument is given, then the current local
585 * date/time, as returned by date:date-time is used as a default argument.
586 * The date/time string specified as the first argument must be a
587 * right-truncated string in the format defined as the lexical representation
588 * of xs:dateTime in one of the formats defined in
589 * <a href="http://www.w3.org/TR/xmlschema-2/">[XML Schema Part 2: Datatypes]</a>.
590 * The permitted formats are as follows:
591 * xs:dateTime (CCYY-MM-DDThh:mm:ss)
592 * xs:date (CCYY-MM-DD)
593 * xs:gYearMonth (CCYY-MM)
594 * xs:gYear (CCYY)
595 * If the date/time string is not in one of these formats, then NaN is returned.
596 */
597 public static XObject leapYear(String datetimeIn)
598 throws ParseException
599 {
600 String[] edz = getEraDatetimeZone(datetimeIn);
601 String datetime = edz[1];
602 if (datetime == null)
603 return new XNumber(Double.NaN);
604
605 String[] formats = {dt, d, gym, gy};
606 double dbl = getNumber(datetime, formats, Calendar.YEAR);
607 if (dbl == Double.NaN)
608 return new XNumber(Double.NaN);
609 int yr = (int)dbl;
610 return new XBoolean(yr % 400 == 0 || (yr % 100 != 0 && yr % 4 == 0));
611 }
612
613 /**
614 * See above.
615 */
616 public static boolean leapYear()
617 {
618 Calendar cal = Calendar.getInstance();
619 int yr = (int)cal.get(Calendar.YEAR);
620 return (yr % 400 == 0 || (yr % 100 != 0 && yr % 4 == 0));
621 }
622
623 /**
624 * The date:month-name function returns the full name of the month of a date.
625 * If no argument is given, then the current local date/time, as returned by
626 * date:date-time is used the default argument.
627 * The date/time string specified as the argument is a left or right-truncated
628 * string in the format defined as the lexical representation of xs:dateTime in
629 * one of the formats defined in
630 * <a href="http://www.w3.org/TR/xmlschema-2/">[XML Schema Part 2: Datatypes]</a>.
631 * The permitted formats are as follows:
632 * xs:dateTime (CCYY-MM-DDThh:mm:ss)
633 * xs:date (CCYY-MM-DD)
634 * xs:gYearMonth (CCYY-MM)
635 * xs:gMonth (--MM--)
636 * If the date/time string is not in one of these formats, then an empty string ('')
637 * is returned.
638 * The result is an English month name: one of 'January', 'February', 'March',
639 * 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November'
640 * or 'December'.
641 */
642 public static String monthName(String datetimeIn)
643 throws ParseException
644 {
645 String[] edz = getEraDatetimeZone(datetimeIn);
646 String datetime = edz[1];
647 if (datetime == null)
648 return EMPTY_STR;
649
650 String[] formatsIn = {dt, d, gym, gm};
651 String formatOut = "MMMM";
652 return getNameOrAbbrev(datetimeIn, formatsIn, formatOut);
653 }
654
655 /**
656 * See above.
657 */
658 public static String monthName()
659 {
660 String format = "MMMM";
661 return getNameOrAbbrev(format);
662 }
663
664 /**
665 * The date:month-abbreviation function returns the abbreviation of the month of
666 * a date. If no argument is given, then the current local date/time, as returned
667 * by date:date-time is used the default argument.
668 * The date/time string specified as the argument is a left or right-truncated
669 * string in the format defined as the lexical representation of xs:dateTime in
670 * one of the formats defined in
671 * <a href="http://www.w3.org/TR/xmlschema-2/">[XML Schema Part 2: Datatypes]</a>.
672 * The permitted formats are as follows:
673 * xs:dateTime (CCYY-MM-DDThh:mm:ss)
674 * xs:date (CCYY-MM-DD)
675 * xs:gYearMonth (CCYY-MM)
676 * xs:gMonth (--MM--)
677 * If the date/time string is not in one of these formats, then an empty string ('')
678 * is returned.
679 * The result is a three-letter English month abbreviation: one of 'Jan', 'Feb', 'Mar',
680 * 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov' or 'Dec'.
681 * An implementation of this extension function in the EXSLT date namespace must conform
682 * to the behaviour described in this document.
683 */
684 public static String monthAbbreviation(String datetimeIn)
685 throws ParseException
686 {
687 String[] edz = getEraDatetimeZone(datetimeIn);
688 String datetime = edz[1];
689 if (datetime == null)
690 return EMPTY_STR;
691
692 String[] formatsIn = {dt, d, gym, gm};
693 String formatOut = "MMM";
694 return getNameOrAbbrev(datetimeIn, formatsIn, formatOut);
695 }
696
697 /**
698 * See above.
699 */
700 public static String monthAbbreviation()
701 {
702 String format = "MMM";
703 return getNameOrAbbrev(format);
704 }
705
706 /**
707 * The date:day-name function returns the full name of the day of the week
708 * of a date. If no argument is given, then the current local date/time,
709 * as returned by date:date-time is used the default argument.
710 * The date/time string specified as the argument is a left or right-truncated
711 * string in the format defined as the lexical representation of xs:dateTime
712 * in one of the formats defined in
713 * <a href="http://www.w3.org/TR/xmlschema-2/">[XML Schema Part 2: Datatypes]</a>.
714 * The permitted formats are as follows:
715 * xs:dateTime (CCYY-MM-DDThh:mm:ss)
716 * xs:date (CCYY-MM-DD)
717 * If the date/time string is not in one of these formats, then the empty string ('')
718 * is returned.
719 * The result is an English day name: one of 'Sunday', 'Monday', 'Tuesday', 'Wednesday',
720 * 'Thursday' or 'Friday'.
721 * An implementation of this extension function in the EXSLT date namespace must conform
722 * to the behaviour described in this document.
723 */
724 public static String dayName(String datetimeIn)
725 throws ParseException
726 {
727 String[] edz = getEraDatetimeZone(datetimeIn);
728 String datetime = edz[1];
729 if (datetime == null)
730 return EMPTY_STR;
731
732 String[] formatsIn = {dt, d};
733 String formatOut = "EEEE";
734 return getNameOrAbbrev(datetimeIn, formatsIn, formatOut);
735 }
736
737 /**
738 * See above.
739 */
740 public static String dayName()
741 {
742 String format = "EEEE";
743 return getNameOrAbbrev(format);
744 }
745
746 /**
747 * The date:day-abbreviation function returns the abbreviation of the day
748 * of the week of a date. If no argument is given, then the current local
749 * date/time, as returned by date:date-time is used the default argument.
750 * The date/time string specified as the argument is a left or right-truncated
751 * string in the format defined as the lexical representation of xs:dateTime
752 * in one of the formats defined in
753 * <a href="http://www.w3.org/TR/xmlschema-2/">[XML Schema Part 2: Datatypes]</a>.
754 * The permitted formats are as follows:
755 * xs:dateTime (CCYY-MM-DDThh:mm:ss)
756 * xs:date (CCYY-MM-DD)
757 * If the date/time string is not in one of these formats, then the empty string
758 * ('') is returned.
759 * The result is a three-letter English day abbreviation: one of 'Sun', 'Mon', 'Tue',
760 * 'Wed', 'Thu' or 'Fri'.
761 * An implementation of this extension function in the EXSLT date namespace must conform
762 * to the behaviour described in this document.
763 */
764 public static String dayAbbreviation(String datetimeIn)
765 throws ParseException
766 {
767 String[] edz = getEraDatetimeZone(datetimeIn);
768 String datetime = edz[1];
769 if (datetime == null)
770 return EMPTY_STR;
771
772 String[] formatsIn = {dt, d};
773 String formatOut = "EEE";
774 return getNameOrAbbrev(datetimeIn, formatsIn, formatOut);
775 }
776
777 /**
778 * See above.
779 */
780 public static String dayAbbreviation()
781 {
782 String format = "EEE";
783 return getNameOrAbbrev(format);
784 }
785
786 /**
787 * Returns an array with the 3 components that a datetime input string
788 * may contain: - (for BC era), datetime, and zone. If the zone is not
789 * valid, return null for that component.
790 */
791 private static String[] getEraDatetimeZone(String in)
792 {
793 String leader = "";
794 String datetime = in;
795 String zone = "";
796 if (in.charAt(0)=='-' && !in.startsWith("--"))
797 {
798 leader = "-"; // '+' is implicit , not allowed
799 datetime = in.substring(1);
800 }
801 int z = getZoneStart(datetime);
802 if (z > 0)
803 {
804 zone = datetime.substring(z);
805 datetime = datetime.substring(0, z);
806 }
807 else if (z == -2)
808 zone = null;
809 //System.out.println("'" + leader + "' " + datetime + " " + zone);
810 return new String[]{leader, datetime, zone};
811 }
812
813 /**
814 * Get the start of zone information if the input ends
815 * with 'Z' or +/-hh:mm. If a zone string is not
816 * found, return -1; if the zone string is invalid,
817 * return -2.
818 */
819 private static int getZoneStart (String datetime)
820 {
821 if (datetime.indexOf('Z') == datetime.length()-1)
822 return datetime.length()-1;
823 else if (datetime.length() >=6
824 && datetime.charAt(datetime.length()-3) == ':'
825 && (datetime.charAt(datetime.length()-6) == '+'
826 || datetime.charAt(datetime.length()-6) == '-'))
827 {
828 try
829 {
830 SimpleDateFormat dateFormat = new SimpleDateFormat("HH:mm");
831 dateFormat.setLenient(false);
832 Date d = dateFormat.parse(datetime.substring(datetime.length() -5));
833 return datetime.length()-6;
834 }
835 catch (ParseException pe)
836 {
837 System.out.println("ParseException " + pe.getErrorOffset());
838 return -2; // Invalid.
839 }
840
841 }
842 return -1; // No zone information.
843 }
844
845 /**
846 * Attempt to parse an input string with the allowed formats, returning
847 * null if none of the formats work.
848 */
849 private static Date testFormats (String in, String[] formats)
850 throws ParseException
851 {
852 for (int i = 0; i <formats.length; i++)
853 {
854 try
855 {
856 SimpleDateFormat dateFormat = new SimpleDateFormat(formats[i]);
857 dateFormat.setLenient(false);
858 return dateFormat.parse(in);
859 }
860 catch (ParseException pe)
861 {
862 }
863 }
864 return null;
865 }
866
867
868 /**
869 * Parse the input string and return the corresponding calendar field
870 * number.
871 */
872 private static double getNumber(String in, String[] formats, int calField)
873 throws ParseException
874 {
875 Calendar cal = Calendar.getInstance();
876 cal.setLenient(false);
877 // Try the allowed formats, from longest to shortest.
878 Date date = testFormats(in, formats);
879 if (date == null) return Double.NaN;
880 cal.setTime(date);
881 return cal.get(calField);
882 }
883
884 /**
885 * Get the full name or abbreviation of the month or day.
886 */
887 private static String getNameOrAbbrev(String in,
888 String[] formatsIn,
889 String formatOut)
890 throws ParseException
891 {
892 for (int i = 0; i <formatsIn.length; i++) // from longest to shortest.
893 {
894 try
895 {
896 SimpleDateFormat dateFormat = new SimpleDateFormat(formatsIn[i], Locale.ENGLISH);
897 dateFormat.setLenient(false);
898 Date dt = dateFormat.parse(in);
899 dateFormat.applyPattern(formatOut);
900 return dateFormat.format(dt);
901 }
902 catch (ParseException pe)
903 {
904 }
905 }
906 return "";
907 }
908 /**
909 * Get the full name or abbreviation for the current month or day
910 * (no input string).
911 */
912 private static String getNameOrAbbrev(String format)
913 {
914 Calendar cal = Calendar.getInstance();
915 SimpleDateFormat dateFormat = new SimpleDateFormat(format, Locale.ENGLISH);
916 return dateFormat.format(cal.getTime());
917 }
918
919 /**
920 * The date:format-date function formats a date/time according to a pattern.
921 * <p>
922 * The first argument to date:format-date specifies the date/time to be
923 * formatted. It must be right or left-truncated date/time strings in one of
924 * the formats defined in
925 * <a href="http://www.w3.org/TR/xmlschema-2/">[XML Schema Part 2: Datatypes]</a>.
926 * The permitted formats are as follows:
927 * <ul>
928 * <li>xs:dateTime (CCYY-MM-DDThh:mm:ss)
929 * <li>xs:date (CCYY-MM-DD)
930 * <li>xs:time (hh:mm:ss)
931 * <li>xs:gYearMonth (CCYY-MM)
932 * <li>xs:gYear (CCYY)
933 * <li>xs:gMonthDay (--MM-DD)
934 * <li>xs:gMonth (--MM--)
935 * <li>xs:gDay (---DD)
936 * </ul>
937 * The second argument is a string that gives the format pattern used to
938 * format the date. The format pattern must be in the syntax specified by
939 * the JDK 1.1 SimpleDateFormat class. The format pattern string is
940 * interpreted as described for the JDK 1.1 SimpleDateFormat class.
941 * <p>
942 * If the date/time format is right-truncated (i.e. in a format other than
943 * xs:time, or xs:dateTime) then any missing components are assumed to be as
944 * follows: if no month is specified, it is given a month of 01; if no day
945 * is specified, it is given a day of 01; if no time is specified, it is
946 * given a time of 00:00:00.
947 * <p>
948 * If the date/time format is left-truncated (i.e. xs:time, xs:gMonthDay,
949 * xs:gMonth or xs:gDay) and the format pattern has a token that uses a
950 * component that is missing from the date/time format used, then that token
951 * is replaced with an empty string ('') within the result.
952 *
953 * The author is Helg Bredow (helg.bredow@kalido.com)
954 */
955 public static String formatDate(String dateTime, String pattern)
956 {
957 final String yearSymbols = "Gy";
958 final String monthSymbols = "M";
959 final String daySymbols = "dDEFwW";
960 TimeZone timeZone;
961 String zone;
962
963 // Get the timezone information if it was supplied and modify the
964 // dateTime so that SimpleDateFormat will understand it.
965 if (dateTime.endsWith("Z") || dateTime.endsWith("z"))
966 {
967 timeZone = TimeZone.getTimeZone("GMT");
968 dateTime = dateTime.substring(0, dateTime.length()-1) + "GMT";
969 zone = "z";
970 }
971 else if ((dateTime.length() >= 6)
972 && (dateTime.charAt(dateTime.length()-3) == ':')
973 && ((dateTime.charAt(dateTime.length()-6) == '+')
974 || (dateTime.charAt(dateTime.length()-6) == '-')))
975 {
976 String offset = dateTime.substring(dateTime.length()-6);
977
978 if ("+00:00".equals(offset) || "-00:00".equals(offset))
979 {
980 timeZone = TimeZone.getTimeZone("GMT");
981 }
982 else
983 {
984 timeZone = TimeZone.getTimeZone("GMT" + offset);
985 }
986 zone = "z";
987 // Need to adjust it since SimpleDateFormat requires GMT+hh:mm but
988 // we have +hh:mm.
989 dateTime = dateTime.substring(0, dateTime.length()-6) + "GMT" + offset;
990 }
991 else
992 {
993 // Assume local time.
994 timeZone = TimeZone.getDefault();
995 zone = "";
996 // Leave off the timezone since SimpleDateFormat will assume local
997 // time if time zone is not included.
998 }
999 String[] formats = {dt + zone, d, gym, gy};
1000
1001 // Try the time format first. We need to do this to prevent
1002 // SimpleDateFormat from interpreting a time as a year. i.e we just need
1003 // to check if it's a time before we check it's a year.
1004 try
1005 {
1006 SimpleDateFormat inFormat = new SimpleDateFormat(t + zone);
1007 inFormat.setLenient(false);
1008 Date d= inFormat.parse(dateTime);
1009 SimpleDateFormat outFormat = new SimpleDateFormat(strip
1010 (yearSymbols + monthSymbols + daySymbols, pattern));
1011 outFormat.setTimeZone(timeZone);
1012 return outFormat.format(d);
1013 }
1014 catch (ParseException pe)
1015 {
1016 }
1017
1018 // Try the right truncated formats.
1019 for (int i = 0; i < formats.length; i++)
1020 {
1021 try
1022 {
1023 SimpleDateFormat inFormat = new SimpleDateFormat(formats[i]);
1024 inFormat.setLenient(false);
1025 Date d = inFormat.parse(dateTime);
1026 SimpleDateFormat outFormat = new SimpleDateFormat(pattern);
1027 outFormat.setTimeZone(timeZone);
1028 return outFormat.format(d);
1029 }
1030 catch (ParseException pe)
1031 {
1032 }
1033 }
1034
1035 // Now try the left truncated ones. The Java format() function doesn't
1036 // return the correct strings in this case. We strip any pattern
1037 // symbols that shouldn't be output so that they are not defaulted to
1038 // inappropriate values in the output.
1039 try
1040 {
1041 SimpleDateFormat inFormat = new SimpleDateFormat(gmd);
1042 inFormat.setLenient(false);
1043 Date d = inFormat.parse(dateTime);
1044 SimpleDateFormat outFormat = new SimpleDateFormat(strip(yearSymbols, pattern));
1045 outFormat.setTimeZone(timeZone);
1046 return outFormat.format(d);
1047 }
1048 catch (ParseException pe)
1049 {
1050 }
1051 try
1052 {
1053 SimpleDateFormat inFormat = new SimpleDateFormat(gm);
1054 inFormat.setLenient(false);
1055 Date d = inFormat.parse(dateTime);
1056 SimpleDateFormat outFormat = new SimpleDateFormat(strip(yearSymbols, pattern));
1057 outFormat.setTimeZone(timeZone);
1058 return outFormat.format(d);
1059 }
1060 catch (ParseException pe)
1061 {
1062 }
1063 try
1064 {
1065 SimpleDateFormat inFormat = new SimpleDateFormat(gd);
1066 inFormat.setLenient(false);
1067 Date d = inFormat.parse(dateTime);
1068 SimpleDateFormat outFormat = new SimpleDateFormat(strip(yearSymbols + monthSymbols, pattern));
1069 outFormat.setTimeZone(timeZone);
1070 return outFormat.format(d);
1071 }
1072 catch (ParseException pe)
1073 {
1074 }
1075 return EMPTY_STR;
1076 }
1077
1078 /**
1079 * Strips occurrences of the given character from a date format pattern.
1080 * @param symbols list of symbols to strip.
1081 * @param pattern
1082 * @return
1083 */
1084 private static String strip(String symbols, String pattern)
1085 {
1086 int i = 0;
1087 StringBuffer result = new StringBuffer(pattern.length());
1088
1089 while (i < pattern.length())
1090 {
1091 char ch = pattern.charAt(i);
1092 if (ch == '\'')
1093 {
1094 // Assume it's an opening quote so simply copy the quoted
1095 // text to the result. There is nothing to strip here.
1096 int endQuote = pattern.indexOf('\'', i + 1);
1097 if (endQuote == -1)
1098 {
1099 endQuote = pattern.length();
1100 }
1101 result.append(pattern.substring(i, endQuote));
1102 i = endQuote++;
1103 }
1104 else if (symbols.indexOf(ch) > -1)
1105 {
1106 // The char needs to be stripped.
1107 i++;
1108 }
1109 else
1110 {
1111 result.append(ch);
1112 i++;
1113 }
1114 }
1115 return result.toString();
1116 }
1117
1118 }