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      */
dateTime()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      */
getZoneStart(String datetime)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      */
testFormats(String in, String[] formats)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      */
getNumber(String in, String[] formats, int calField)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      */
getNameOrAbbrev(String in, String[] formatsIn, String formatOut)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      */
getNameOrAbbrev(String format)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      */
formatDate(String dateTime, String pattern)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      */
strip(String symbols, String pattern)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