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