1/*
2 * reserved comment block
3 * DO NOT REMOVE OR ALTER!
4 */
5/*
6 * Licensed to the Apache Software Foundation (ASF) under one or more
7 * contributor license agreements.  See the NOTICE file distributed with
8 * this work for additional information regarding copyright ownership.
9 * The ASF licenses this file to You under the Apache License, Version 2.0
10 * (the "License"); you may not use this file except in compliance with
11 * the License.  You may obtain a copy of the License at
12 *
13 *      http://www.apache.org/licenses/LICENSE-2.0
14 *
15 * Unless required by applicable law or agreed to in writing, software
16 * distributed under the License is distributed on an "AS IS" BASIS,
17 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
18 * See the License for the specific language governing permissions and
19 * limitations under the License.
20 */
21
22package com.sun.org.apache.xalan.internal.lib;
23
24
25import java.text.ParseException;
26import java.text.SimpleDateFormat;
27import java.util.Calendar;
28import java.util.Date;
29import java.util.Locale;
30import java.util.TimeZone;
31
32import com.sun.org.apache.xpath.internal.objects.XBoolean;
33import com.sun.org.apache.xpath.internal.objects.XNumber;
34import com.sun.org.apache.xpath.internal.objects.XObject;
35
36/**
37 * This class contains EXSLT dates and times extension functions.
38 * It is accessed by specifying a namespace URI as follows:
39 * <pre>
40 *    xmlns:datetime="http://exslt.org/dates-and-times"
41 * </pre>
42 *
43 * The documentation for each function has been copied from the relevant
44 * EXSLT Implementer page.
45 *
46 * @see <a href="http://www.exslt.org/">EXSLT</a>
47 * @xsl.usage general
48 */
49
50public 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 = (int)cal.get(Calendar.YEAR);
620      return (yr % 400 == 0 || (yr % 100 != 0 && yr % 4 == 0));
621    }
622
623    /**
624     * The date:month-name function returns the full name of the month of a date.
625     * If no argument is given, then the current local date/time, as returned by
626     * date:date-time is used the default argument.
627     * The date/time string specified as the argument is a left or right-truncated
628     * string in the format defined as the lexical representation of xs:dateTime in
629     *  one of the formats defined in
630     * <a href="http://www.w3.org/TR/xmlschema-2/">[XML Schema Part 2: Datatypes]</a>.
631     * The permitted formats are as follows:
632     *    xs:dateTime (CCYY-MM-DDThh:mm:ss)
633     *    xs:date (CCYY-MM-DD)
634     *    xs:gYearMonth (CCYY-MM)
635     *    xs:gMonth (--MM--)
636     * If the date/time string is not in one of these formats, then an empty string ('')
637     * is returned.
638     * The result is an English month name: one of 'January', 'February', 'March',
639     * 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November'
640     * or 'December'.
641     */
642    public static String monthName(String datetimeIn)
643      throws ParseException
644    {
645      String[] edz = getEraDatetimeZone(datetimeIn);
646      String datetime = edz[1];
647      if (datetime == null)
648        return EMPTY_STR;
649
650      String[] formatsIn = {dt, d, gym, gm};
651      String formatOut = "MMMM";
652      return getNameOrAbbrev(datetimeIn, formatsIn, formatOut);
653    }
654
655    /**
656     * See above.
657     */
658    public static String monthName()
659    {
660      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}
1121