1 /*
   2  * Copyright (c) 2017, Oracle and/or its affiliates. All rights reserved.
   3  */
   4 /*
   5  * Licensed to the Apache Software Foundation (ASF) under one or more
   6  * contributor license agreements.  See the NOTICE file distributed with
   7  * this work for additional information regarding copyright ownership.
   8  * The ASF licenses this file to You under the Apache License, Version 2.0
   9  * (the "License"); you may not use this file except in compliance with
  10  * the License.  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 package com.sun.org.apache.xalan.internal.lib;
  22 
  23 
  24 import java.text.ParseException;
  25 import java.text.SimpleDateFormat;
  26 import java.util.Calendar;
  27 import java.util.Date;
  28 import java.util.Locale;
  29 import java.util.TimeZone;
  30 
  31 import com.sun.org.apache.xpath.internal.objects.XBoolean;
  32 import com.sun.org.apache.xpath.internal.objects.XNumber;
  33 import com.sun.org.apache.xpath.internal.objects.XObject;
  34 
  35 /**
  36  * This class contains EXSLT dates and times extension functions.
  37  * It is accessed by specifying a namespace URI as follows:
  38  * <pre>
  39  *    xmlns:datetime="http://exslt.org/dates-and-times"
  40  * </pre>
  41  *
  42  * The documentation for each function has been copied from the relevant
  43  * EXSLT Implementer page.
  44  *
  45  * @see <a href="http://www.exslt.org/">EXSLT</a>
  46  * @xsl.usage general
  47  * @LastModified: Nov 2017
  48  */
  49 
  50 public class ExsltDatetime
  51 {
  52     // Datetime formats (era and zone handled separately).
  53     static final String dt = "yyyy-MM-dd'T'HH:mm:ss";
  54     static final String d = "yyyy-MM-dd";
  55     static final String gym = "yyyy-MM";
  56     static final String gy = "yyyy";
  57     static final String gmd = "--MM-dd";
  58     static final String gm = "--MM--";
  59     static final String gd = "---dd";
  60     static final String t = "HH:mm:ss";
  61     static final String EMPTY_STR = "";
  62 
  63     /**
  64      * The date:date-time function returns the current date and time as a date/time string.
  65      * The date/time string that's returned must be a string in the format defined as the
  66      * lexical representation of xs:dateTime in
  67      * <a href="http://www.w3.org/TR/xmlschema-2/#dateTime">[3.2.7 dateTime]</a> of
  68      * <a href="http://www.w3.org/TR/xmlschema-2/">[XML Schema Part 2: Datatypes]</a>.
  69      * The date/time format is basically CCYY-MM-DDThh:mm:ss, although implementers should consult
  70      * <a href="http://www.w3.org/TR/xmlschema-2/">[XML Schema Part 2: Datatypes]</a> and
  71      * <a href="http://www.iso.ch/markete/8601.pdf">[ISO 8601]</a> for details.
  72      * The date/time string format must include a time zone, either a Z to indicate Coordinated
  73      * Universal Time or a + or - followed by the difference between the difference from UTC
  74      * represented as hh:mm.
  75      */
  76     public static String dateTime()
  77     {
  78       Calendar cal = Calendar.getInstance();
  79       Date datetime = cal.getTime();
  80       // Format for date and time.
  81       SimpleDateFormat dateFormat = new SimpleDateFormat(dt);
  82 
  83       StringBuffer buff = new StringBuffer(dateFormat.format(datetime));
  84       // Must also include offset from UTF.
  85       // Get the offset (in milliseconds).
  86       int offset = cal.get(Calendar.ZONE_OFFSET) + cal.get(Calendar.DST_OFFSET);
  87       // If there is no offset, we have "Coordinated
  88       // Universal Time."
  89       if (offset == 0)
  90         buff.append("Z");
  91       else
  92       {
  93         // Convert milliseconds to hours and minutes
  94         int hrs = offset/(60*60*1000);
  95         // In a few cases, the time zone may be +/-hh:30.
  96         int min = offset%(60*60*1000);
  97         char posneg = hrs < 0? '-': '+';
  98         buff.append(posneg).append(formatDigits(hrs)).append(':').append(formatDigits(min));
  99       }
 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().toString();
 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().toString();
 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 = 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       Calendar cal = Calendar.getInstance();
 661       String format = "MMMM";
 662       return getNameOrAbbrev(format);
 663     }
 664 
 665     /**
 666      * The date:month-abbreviation function returns the abbreviation of the month of
 667      * a date. If no argument is given, then the current local date/time, as returned
 668      * by date:date-time is used the default argument.
 669      * The date/time string specified as the argument is a left or right-truncated
 670      * string in the format defined as the lexical representation of xs:dateTime in
 671      * one of the formats defined in
 672      * <a href="http://www.w3.org/TR/xmlschema-2/">[XML Schema Part 2: Datatypes]</a>.
 673      * The permitted formats are as follows:
 674      *    xs:dateTime (CCYY-MM-DDThh:mm:ss)
 675      *    xs:date (CCYY-MM-DD)
 676      *    xs:gYearMonth (CCYY-MM)
 677      *    xs:gMonth (--MM--)
 678      * If the date/time string is not in one of these formats, then an empty string ('')
 679      * is returned.
 680      * The result is a three-letter English month abbreviation: one of 'Jan', 'Feb', 'Mar',
 681      * 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov' or 'Dec'.
 682      * An implementation of this extension function in the EXSLT date namespace must conform
 683      * to the behaviour described in this document.
 684      */
 685     public static String monthAbbreviation(String datetimeIn)
 686       throws ParseException
 687     {
 688       String[] edz = getEraDatetimeZone(datetimeIn);
 689       String datetime = edz[1];
 690       if (datetime == null)
 691         return EMPTY_STR;
 692 
 693       String[] formatsIn = {dt, d, gym, gm};
 694       String formatOut = "MMM";
 695       return getNameOrAbbrev(datetimeIn, formatsIn, formatOut);
 696     }
 697 
 698     /**
 699      * See above.
 700      */
 701     public static String monthAbbreviation()
 702     {
 703       String format = "MMM";
 704       return getNameOrAbbrev(format);
 705     }
 706 
 707     /**
 708      * The date:day-name function returns the full name of the day of the week
 709      * of a date.  If no argument is given, then the current local date/time,
 710      * as returned by date:date-time is used the default argument.
 711      * The date/time string specified as the argument is a left or right-truncated
 712      * string in the format defined as the lexical representation of xs:dateTime
 713      * in one of the formats defined in
 714      * <a href="http://www.w3.org/TR/xmlschema-2/">[XML Schema Part 2: Datatypes]</a>.
 715      * The permitted formats are as follows:
 716      *     xs:dateTime (CCYY-MM-DDThh:mm:ss)
 717      *     xs:date (CCYY-MM-DD)
 718      * If the date/time string is not in one of these formats, then the empty string ('')
 719      * is returned.
 720      * The result is an English day name: one of 'Sunday', 'Monday', 'Tuesday', 'Wednesday',
 721      * 'Thursday' or 'Friday'.
 722      * An implementation of this extension function in the EXSLT date namespace must conform
 723      * to the behaviour described in this document.
 724      */
 725     public static String dayName(String datetimeIn)
 726       throws ParseException
 727     {
 728       String[] edz = getEraDatetimeZone(datetimeIn);
 729       String datetime = edz[1];
 730       if (datetime == null)
 731         return EMPTY_STR;
 732 
 733       String[] formatsIn = {dt, d};
 734       String formatOut = "EEEE";
 735       return getNameOrAbbrev(datetimeIn, formatsIn, formatOut);
 736     }
 737 
 738     /**
 739      * See above.
 740      */
 741     public static String dayName()
 742     {
 743       String format = "EEEE";
 744       return getNameOrAbbrev(format);
 745     }
 746 
 747     /**
 748      * The date:day-abbreviation function returns the abbreviation of the day
 749      * of the week of a date. If no argument is given, then the current local
 750      * date/time, as returned  by date:date-time is used the default argument.
 751      * The date/time string specified as the argument is a left or right-truncated
 752      * string in the format defined as the lexical representation of xs:dateTime
 753      * in one of the formats defined in
 754      * <a href="http://www.w3.org/TR/xmlschema-2/">[XML Schema Part 2: Datatypes]</a>.
 755      * The permitted formats are as follows:
 756      *     xs:dateTime (CCYY-MM-DDThh:mm:ss)
 757      *     xs:date (CCYY-MM-DD)
 758      * If the date/time string is not in one of these formats, then the empty string
 759      * ('') is returned.
 760      * The result is a three-letter English day abbreviation: one of 'Sun', 'Mon', 'Tue',
 761      * 'Wed', 'Thu' or 'Fri'.
 762      * An implementation of this extension function in the EXSLT date namespace must conform
 763      * to the behaviour described in this document.
 764      */
 765     public static String dayAbbreviation(String datetimeIn)
 766       throws ParseException
 767     {
 768       String[] edz = getEraDatetimeZone(datetimeIn);
 769       String datetime = edz[1];
 770       if (datetime == null)
 771         return EMPTY_STR;
 772 
 773       String[] formatsIn = {dt, d};
 774       String formatOut = "EEE";
 775       return getNameOrAbbrev(datetimeIn, formatsIn, formatOut);
 776     }
 777 
 778     /**
 779      * See above.
 780      */
 781     public static String dayAbbreviation()
 782     {
 783       String format = "EEE";
 784       return getNameOrAbbrev(format);
 785     }
 786 
 787     /**
 788      * Returns an array with the 3 components that a datetime input string
 789      * may contain: - (for BC era), datetime, and zone. If the zone is not
 790      * valid, return null for that component.
 791      */
 792     private static String[] getEraDatetimeZone(String in)
 793     {
 794       String leader = "";
 795       String datetime = in;
 796       String zone = "";
 797       if (in.charAt(0)=='-' && !in.startsWith("--"))
 798       {
 799         leader = "-"; //  '+' is implicit , not allowed
 800         datetime = in.substring(1);
 801       }
 802       int z = getZoneStart(datetime);
 803       if (z > 0)
 804       {
 805         zone = datetime.substring(z);
 806         datetime = datetime.substring(0, z);
 807       }
 808       else if (z == -2)
 809         zone = null;
 810       //System.out.println("'" + leader + "' " + datetime + " " + zone);
 811       return new String[]{leader, datetime, zone};
 812     }
 813 
 814     /**
 815      * Get the start of zone information if the input ends
 816      * with 'Z' or +/-hh:mm. If a zone string is not
 817      * found, return -1; if the zone string is invalid,
 818      * return -2.
 819      */
 820     private static int getZoneStart (String datetime)
 821     {
 822       if (datetime.indexOf("Z") == datetime.length()-1)
 823         return datetime.length()-1;
 824       else if (datetime.length() >=6
 825                 && datetime.charAt(datetime.length()-3) == ':'
 826                 && (datetime.charAt(datetime.length()-6) == '+'
 827                     || datetime.charAt(datetime.length()-6) == '-'))
 828       {
 829         try
 830         {
 831           SimpleDateFormat dateFormat = new SimpleDateFormat("HH:mm");
 832           dateFormat.setLenient(false);
 833           Date d = dateFormat.parse(datetime.substring(datetime.length() -5));
 834           return datetime.length()-6;
 835         }
 836         catch (ParseException pe)
 837         {
 838           System.out.println("ParseException " + pe.getErrorOffset());
 839           return -2; // Invalid.
 840         }
 841 
 842       }
 843         return -1; // No zone information.
 844     }
 845 
 846     /**
 847      * Attempt to parse an input string with the allowed formats, returning
 848      * null if none of the formats work.
 849      */
 850     private static Date testFormats (String in, String[] formats)
 851       throws ParseException
 852     {
 853       for (int i = 0; i <formats.length; i++)
 854       {
 855         try
 856         {
 857           SimpleDateFormat dateFormat = new SimpleDateFormat(formats[i]);
 858           dateFormat.setLenient(false);
 859           return dateFormat.parse(in);
 860         }
 861         catch (ParseException pe)
 862         {
 863         }
 864       }
 865       return null;
 866     }
 867 
 868 
 869     /**
 870      * Parse the input string and return the corresponding calendar field
 871      * number.
 872      */
 873     private static double getNumber(String in, String[] formats, int calField)
 874       throws ParseException
 875     {
 876       Calendar cal = Calendar.getInstance();
 877       cal.setLenient(false);
 878       // Try the allowed formats, from longest to shortest.
 879       Date date = testFormats(in, formats);
 880       if (date == null) return Double.NaN;
 881       cal.setTime(date);
 882       return cal.get(calField);
 883     }
 884 
 885     /**
 886      *  Get the full name or abbreviation of the month or day.
 887      */
 888     private static String getNameOrAbbrev(String in,
 889                                          String[] formatsIn,
 890                                          String formatOut)
 891       throws ParseException
 892     {
 893       for (int i = 0; i <formatsIn.length; i++) // from longest to shortest.
 894       {
 895         try
 896         {
 897           SimpleDateFormat dateFormat = new SimpleDateFormat(formatsIn[i], Locale.ENGLISH);
 898           dateFormat.setLenient(false);
 899           Date dt = dateFormat.parse(in);
 900           dateFormat.applyPattern(formatOut);
 901           return dateFormat.format(dt);
 902         }
 903         catch (ParseException pe)
 904         {
 905         }
 906       }
 907       return "";
 908     }
 909     /**
 910      * Get the full name or abbreviation for the current month or day
 911      * (no input string).
 912      */
 913     private static String getNameOrAbbrev(String format)
 914     {
 915       Calendar cal = Calendar.getInstance();
 916       SimpleDateFormat dateFormat = new SimpleDateFormat(format, Locale.ENGLISH);
 917       return dateFormat.format(cal.getTime());
 918     }
 919 
 920     /**
 921      * The date:format-date function formats a date/time according to a pattern.
 922      * <p>
 923      * The first argument to date:format-date specifies the date/time to be
 924      * formatted. It must be right or left-truncated date/time strings in one of
 925      * the formats defined in
 926      * <a href="http://www.w3.org/TR/xmlschema-2/">[XML Schema Part 2: Datatypes]</a>.
 927      * The permitted formats are as follows:
 928      * <ul>
 929      * <li>xs:dateTime (CCYY-MM-DDThh:mm:ss)
 930      * <li>xs:date (CCYY-MM-DD)
 931      * <li>xs:time (hh:mm:ss)
 932      * <li>xs:gYearMonth (CCYY-MM)
 933      * <li>xs:gYear (CCYY)
 934      * <li>xs:gMonthDay (--MM-DD)
 935      * <li>xs:gMonth (--MM--)
 936      * <li>xs:gDay (---DD)
 937      * </ul>
 938      * The second argument is a string that gives the format pattern used to
 939      * format the date. The format pattern must be in the syntax specified by
 940      * the JDK 1.1 SimpleDateFormat class. The format pattern string is
 941      * interpreted as described for the JDK 1.1 SimpleDateFormat class.
 942      * <p>
 943      * If the date/time format is right-truncated (i.e. in a format other than
 944      * xs:time, or xs:dateTime) then any missing components are assumed to be as
 945      * follows: if no month is specified, it is given a month of 01; if no day
 946      * is specified, it is given a day of 01; if no time is specified, it is
 947      * given a time of 00:00:00.
 948      * <p>
 949      * If the date/time format is left-truncated (i.e. xs:time, xs:gMonthDay,
 950      * xs:gMonth or xs:gDay) and the format pattern has a token that uses a
 951      * component that is missing from the date/time format used, then that token
 952      * is replaced with an empty string ('') within the result.
 953      *
 954      * The author is Helg Bredow (helg.bredow@kalido.com)
 955      */
 956     public static String formatDate(String dateTime, String pattern)
 957     {
 958         final String yearSymbols = "Gy";
 959         final String monthSymbols = "M";
 960         final String daySymbols = "dDEFwW";
 961         TimeZone timeZone;
 962         String zone;
 963 
 964         // Get the timezone information if it was supplied and modify the
 965         // dateTime so that SimpleDateFormat will understand it.
 966         if (dateTime.endsWith("Z") || dateTime.endsWith("z"))
 967         {
 968             timeZone = TimeZone.getTimeZone("GMT");
 969             dateTime = dateTime.substring(0, dateTime.length()-1) + "GMT";
 970             zone = "z";
 971         }
 972         else if ((dateTime.length() >= 6)
 973                  && (dateTime.charAt(dateTime.length()-3) == ':')
 974                  && ((dateTime.charAt(dateTime.length()-6) == '+')
 975                     || (dateTime.charAt(dateTime.length()-6) == '-')))
 976         {
 977             String offset = dateTime.substring(dateTime.length()-6);
 978 
 979             if ("+00:00".equals(offset) || "-00:00".equals(offset))
 980             {
 981                 timeZone = TimeZone.getTimeZone("GMT");
 982             }
 983             else
 984             {
 985                 timeZone = TimeZone.getTimeZone("GMT" + offset);
 986             }
 987             zone = "z";
 988             // Need to adjust it since SimpleDateFormat requires GMT+hh:mm but
 989             // we have +hh:mm.
 990             dateTime = dateTime.substring(0, dateTime.length()-6) + "GMT" + offset;
 991         }
 992         else
 993         {
 994             // Assume local time.
 995             timeZone = TimeZone.getDefault();
 996             zone = "";
 997             // Leave off the timezone since SimpleDateFormat will assume local
 998             // time if time zone is not included.
 999         }
1000         String[] formats = {dt + zone, d, gym, gy};
1001 
1002         // Try the time format first. We need to do this to prevent
1003         // SimpleDateFormat from interpreting a time as a year. i.e we just need
1004         // to check if it's a time before we check it's a year.
1005         try
1006         {
1007             SimpleDateFormat inFormat = new SimpleDateFormat(t + zone);
1008             inFormat.setLenient(false);
1009             Date d= inFormat.parse(dateTime);
1010             SimpleDateFormat outFormat = new SimpleDateFormat(strip
1011                 (yearSymbols + monthSymbols + daySymbols, pattern));
1012             outFormat.setTimeZone(timeZone);
1013             return outFormat.format(d);
1014         }
1015         catch (ParseException pe)
1016         {
1017         }
1018 
1019         // Try the right truncated formats.
1020         for (int i = 0; i < formats.length; i++)
1021         {
1022             try
1023             {
1024                 SimpleDateFormat inFormat = new SimpleDateFormat(formats[i]);
1025                 inFormat.setLenient(false);
1026                 Date d = inFormat.parse(dateTime);
1027                 SimpleDateFormat outFormat = new SimpleDateFormat(pattern);
1028                 outFormat.setTimeZone(timeZone);
1029                 return outFormat.format(d);
1030             }
1031             catch (ParseException pe)
1032             {
1033             }
1034         }
1035 
1036         // Now try the left truncated ones. The Java format() function doesn't
1037         // return the correct strings in this case. We strip any pattern
1038         // symbols that shouldn't be output so that they are not defaulted to
1039         // inappropriate values in the output.
1040         try
1041         {
1042             SimpleDateFormat inFormat = new SimpleDateFormat(gmd);
1043             inFormat.setLenient(false);
1044             Date d = inFormat.parse(dateTime);
1045             SimpleDateFormat outFormat = new SimpleDateFormat(strip(yearSymbols, pattern));
1046             outFormat.setTimeZone(timeZone);
1047             return outFormat.format(d);
1048         }
1049         catch (ParseException pe)
1050         {
1051         }
1052         try
1053         {
1054             SimpleDateFormat inFormat = new SimpleDateFormat(gm);
1055             inFormat.setLenient(false);
1056             Date d = inFormat.parse(dateTime);
1057             SimpleDateFormat outFormat = new SimpleDateFormat(strip(yearSymbols, pattern));
1058             outFormat.setTimeZone(timeZone);
1059             return outFormat.format(d);
1060         }
1061         catch (ParseException pe)
1062         {
1063         }
1064         try
1065         {
1066             SimpleDateFormat inFormat = new SimpleDateFormat(gd);
1067             inFormat.setLenient(false);
1068             Date d = inFormat.parse(dateTime);
1069             SimpleDateFormat outFormat = new SimpleDateFormat(strip(yearSymbols + monthSymbols, pattern));
1070             outFormat.setTimeZone(timeZone);
1071             return outFormat.format(d);
1072         }
1073         catch (ParseException pe)
1074         {
1075         }
1076         return EMPTY_STR;
1077     }
1078 
1079     /**
1080      * Strips occurrences of the given character from a date format pattern.
1081      * @param symbols list of symbols to strip.
1082      * @param pattern
1083      * @return
1084      */
1085     private static String strip(String symbols, String pattern)
1086     {
1087         int quoteSemaphore = 0;
1088         int i = 0;
1089         StringBuffer result = new StringBuffer(pattern.length());
1090 
1091         while (i < pattern.length())
1092         {
1093             char ch = pattern.charAt(i);
1094             if (ch == '\'')
1095             {
1096                 // Assume it's an openening quote so simply copy the quoted
1097                 // text to the result. There is nothing to strip here.
1098                 int endQuote = pattern.indexOf('\'', i + 1);
1099                 if (endQuote == -1)
1100                 {
1101                     endQuote = pattern.length();
1102                 }
1103                 result.append(pattern.substring(i, endQuote));
1104                 i = endQuote++;
1105             }
1106             else if (symbols.indexOf(ch) > -1)
1107             {
1108                 // The char needs to be stripped.
1109                 i++;
1110             }
1111             else
1112             {
1113                 result.append(ch);
1114                 i++;
1115             }
1116         }
1117         return result.toString();
1118     }
1119 
1120 }