001package org.hl7.fhir.dstu2.model;
002
003/*
004  Copyright (c) 2011+, HL7, Inc.
005  All rights reserved.
006  
007  Redistribution and use in source and binary forms, with or without modification, 
008  are permitted provided that the following conditions are met:
009    
010   * Redistributions of source code must retain the above copyright notice, this 
011     list of conditions and the following disclaimer.
012   * Redistributions in binary form must reproduce the above copyright notice, 
013     this list of conditions and the following disclaimer in the documentation 
014     and/or other materials provided with the distribution.
015   * Neither the name of HL7 nor the names of its contributors may be used to 
016     endorse or promote products derived from this software without specific 
017     prior written permission.
018  
019  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 
020  ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 
021  WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. 
022  IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, 
023  INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT 
024  NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 
025  PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, 
026  WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 
027  ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 
028  POSSIBILITY OF SUCH DAMAGE.
029  
030 */
031
032
033import ca.uhn.fhir.model.api.TemporalPrecisionEnum;
034import org.apache.commons.lang3.StringUtils;
035import org.apache.commons.lang3.Validate;
036import org.apache.commons.lang3.time.DateUtils;
037import org.apache.commons.lang3.time.FastDateFormat;
038import org.hl7.fhir.utilities.DateTimeUtil;
039
040import java.text.ParseException;
041import java.util.*;
042import java.util.concurrent.ConcurrentHashMap;
043import java.util.regex.Pattern;
044
045import static ca.uhn.fhir.model.api.TemporalPrecisionEnum.*;
046
047public abstract class BaseDateTimeType extends PrimitiveType<Date> {
048
049        private static final long serialVersionUID = 1L;
050
051        /*
052         * Add any new formatters to the static block below!!
053         */
054        private static final List<FastDateFormat> ourFormatters;
055
056        private static final Pattern ourYearDashMonthDashDayPattern = Pattern.compile("[0-9]{4}-[0-9]{2}-[0-9]{2}");
057        private static final Pattern ourYearDashMonthPattern = Pattern.compile("[0-9]{4}-[0-9]{2}");
058        private static final FastDateFormat ourYearFormat = FastDateFormat.getInstance("yyyy");
059        private static final FastDateFormat ourYearMonthDayFormat = FastDateFormat.getInstance("yyyy-MM-dd");
060        private static final FastDateFormat ourYearMonthDayNoDashesFormat = FastDateFormat.getInstance("yyyyMMdd");
061        private static final Pattern ourYearMonthDayPattern = Pattern.compile("[0-9]{4}[0-9]{2}[0-9]{2}");
062        private static final FastDateFormat ourYearMonthDayTimeFormat = FastDateFormat.getInstance("yyyy-MM-dd'T'HH:mm:ss");
063        private static final FastDateFormat ourYearMonthDayTimeMilliFormat = FastDateFormat.getInstance("yyyy-MM-dd'T'HH:mm:ss.SSS");
064        private static final FastDateFormat ourYearMonthDayTimeMilliUTCZFormat = FastDateFormat.getInstance("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", TimeZone.getTimeZone("UTC"));
065        private static final FastDateFormat ourYearMonthDayTimeMilliZoneFormat = FastDateFormat.getInstance("yyyy-MM-dd'T'HH:mm:ss.SSSZZ");
066        private static final FastDateFormat ourYearMonthDayTimeUTCZFormat = FastDateFormat.getInstance("yyyy-MM-dd'T'HH:mm:ss'Z'", TimeZone.getTimeZone("UTC"));
067        private static final FastDateFormat ourYearMonthDayTimeZoneFormat = FastDateFormat.getInstance("yyyy-MM-dd'T'HH:mm:ssZZ");
068        private static final FastDateFormat ourYearMonthFormat = FastDateFormat.getInstance("yyyy-MM");
069        private static final FastDateFormat ourYearMonthNoDashesFormat = FastDateFormat.getInstance("yyyyMM");
070        private static final Pattern ourYearMonthPattern = Pattern.compile("[0-9]{4}[0-9]{2}");
071        private static final Pattern ourYearPattern = Pattern.compile("[0-9]{4}");
072        private static final FastDateFormat ourYearMonthDayTimeMinsFormat = FastDateFormat.getInstance("yyyy-MM-dd'T'HH:mm");
073        private static final FastDateFormat ourYearMonthDayTimeMinsUTCZFormat = FastDateFormat.getInstance("yyyy-MM-dd'T'HH:mm'Z'", TimeZone.getTimeZone("UTC"));
074        private static final FastDateFormat ourYearMonthDayTimeMinsZoneFormat = FastDateFormat.getInstance("yyyy-MM-dd'T'HH:mmZZ");
075
076        private static final FastDateFormat ourHumanDateTimeFormat = FastDateFormat.getDateTimeInstance(FastDateFormat.MEDIUM, FastDateFormat.MEDIUM);
077        private static final FastDateFormat ourHumanDateFormat = FastDateFormat.getDateInstance(FastDateFormat.MEDIUM);
078  private static final Map<String, TimeZone> timezoneCache = new ConcurrentHashMap<>();
079
080        static {
081                ArrayList<FastDateFormat> formatters = new ArrayList<FastDateFormat>();
082                formatters.add(ourYearFormat);
083                formatters.add(ourYearMonthDayFormat);
084                formatters.add(ourYearMonthDayNoDashesFormat);
085                formatters.add(ourYearMonthDayTimeFormat);
086                formatters.add(ourYearMonthDayTimeUTCZFormat);
087                formatters.add(ourYearMonthDayTimeZoneFormat);
088                formatters.add(ourYearMonthDayTimeMilliFormat);
089                formatters.add(ourYearMonthDayTimeMilliUTCZFormat);
090                formatters.add(ourYearMonthDayTimeMilliZoneFormat);
091                formatters.add(ourYearMonthDayTimeMinsFormat);
092                formatters.add(ourYearMonthDayTimeMinsUTCZFormat);
093                formatters.add(ourYearMonthDayTimeMinsZoneFormat);
094                formatters.add(ourYearMonthFormat);
095                formatters.add(ourYearMonthNoDashesFormat);
096                ourFormatters = Collections.unmodifiableList(formatters);
097        }
098
099        private TemporalPrecisionEnum myPrecision = TemporalPrecisionEnum.SECOND;
100
101        private TimeZone myTimeZone;
102        private boolean myTimeZoneZulu = false;
103
104        /**
105         * Constructor
106         */
107        public BaseDateTimeType() {
108                // nothing
109        }
110
111        /**
112         * Constructor
113         * 
114         * @throws IllegalArgumentException
115         *             If the specified precision is not allowed for this type
116         */
117        public BaseDateTimeType(Date theDate, TemporalPrecisionEnum thePrecision) {
118                setValue(theDate, thePrecision);
119                if (isPrecisionAllowed(thePrecision) == false) {
120                        throw new IllegalArgumentException("Invalid date/time string (datatype " + getClass().getSimpleName() + " does not support " + thePrecision + " precision): " + theDate);
121                }
122        }
123
124        /**
125         * Constructor
126         * 
127         * @throws IllegalArgumentException
128         *             If the specified precision is not allowed for this type
129         */
130        public BaseDateTimeType(String theString) {
131                setValueAsString(theString);
132                if (isPrecisionAllowed(getPrecision()) == false) {
133                        throw new IllegalArgumentException("Invalid date/time string (datatype " + getClass().getSimpleName() + " does not support " + getPrecision() + " precision): " + theString);
134                }
135        }
136
137        /**
138         * Constructor
139         */
140        public BaseDateTimeType(Date theDate, TemporalPrecisionEnum thePrecision, TimeZone theTimeZone) {
141                this(theDate, thePrecision);
142                setTimeZone(theTimeZone);
143        }
144
145        private void clearTimeZone() {
146                myTimeZone = null;
147                myTimeZoneZulu = false;
148        }
149
150        @Override
151        protected String encode(Date theValue) {
152                if (theValue == null) {
153                        return null;
154                } else {
155                        switch (myPrecision) {
156                        case DAY:
157                                return ourYearMonthDayFormat.format(theValue);
158                        case MONTH:
159                                return ourYearMonthFormat.format(theValue);
160                        case YEAR:
161                                return ourYearFormat.format(theValue);
162                        case MINUTE:
163                                if (myTimeZoneZulu) {
164                                        GregorianCalendar cal = new GregorianCalendar(getTimeZone("GMT"));
165                                        cal.setTime(theValue);
166                                        return ourYearMonthDayTimeMinsFormat.format(cal) + "Z";
167                                } else if (myTimeZone != null) {
168                                        GregorianCalendar cal = new GregorianCalendar(myTimeZone);
169                                        cal.setTime(theValue);
170                                        return (ourYearMonthDayTimeMinsZoneFormat.format(cal));
171                                } else {
172                                        return ourYearMonthDayTimeMinsFormat.format(theValue);
173                                }
174                        case SECOND:
175                                if (myTimeZoneZulu) {
176                                        GregorianCalendar cal = new GregorianCalendar(getTimeZone("GMT"));
177                                        cal.setTime(theValue);
178                                        return ourYearMonthDayTimeFormat.format(cal) + "Z";
179                                } else if (myTimeZone != null) {
180                                        GregorianCalendar cal = new GregorianCalendar(myTimeZone);
181                                        cal.setTime(theValue);
182                                        return (ourYearMonthDayTimeZoneFormat.format(cal));
183                                } else {
184                                        return ourYearMonthDayTimeFormat.format(theValue);
185                                }
186                        case MILLI:
187                                if (myTimeZoneZulu) {
188                                        GregorianCalendar cal = new GregorianCalendar(getTimeZone("GMT"));
189                                        cal.setTime(theValue);
190                                        return ourYearMonthDayTimeMilliFormat.format(cal) + "Z";
191                                } else if (myTimeZone != null) {
192                                        GregorianCalendar cal = new GregorianCalendar(myTimeZone);
193                                        cal.setTime(theValue);
194                                        return (ourYearMonthDayTimeMilliZoneFormat.format(cal));
195                                } else {
196                                        return ourYearMonthDayTimeMilliFormat.format(theValue);
197                                }
198                        }
199                        throw new IllegalStateException("Invalid precision (this is a bug, shouldn't happen https://xkcd.com/2200/): " + myPrecision);
200                }
201        }
202
203        /**
204         * Returns the default precision for the given datatype
205         */
206        protected abstract TemporalPrecisionEnum getDefaultPrecisionForDatatype();
207
208        /**
209         * Gets the precision for this datatype (using the default for the given type if not set)
210         * 
211         * @see #setPrecision(TemporalPrecisionEnum)
212         */
213        public TemporalPrecisionEnum getPrecision() {
214                if (myPrecision == null) {
215                        return getDefaultPrecisionForDatatype();
216                }
217                return myPrecision;
218        }
219
220        /**
221         * Returns the TimeZone associated with this dateTime's value. May return <code>null</code> if no timezone was
222         * supplied.
223         */
224        public TimeZone getTimeZone() {
225                return myTimeZone;
226        }
227
228        private boolean hasOffset(String theValue) {
229                boolean inTime = false;
230                for (int i = 0; i < theValue.length(); i++) {
231                        switch (theValue.charAt(i)) {
232                        case 'T':
233                                inTime = true;
234                                break;
235                        case '+':
236                        case '-':
237                                if (inTime) {
238                                        return true;
239                                }
240                                break;
241                        }
242                }
243                return false;
244        }
245
246        /**
247         * To be implemented by subclasses to indicate whether the given precision is allowed by this type
248         */
249        abstract boolean isPrecisionAllowed(TemporalPrecisionEnum thePrecision);
250
251        public boolean isTimeZoneZulu() {
252                return myTimeZoneZulu;
253        }
254
255        /**
256         * Returns <code>true</code> if this object represents a date that is today's date
257         * 
258         * @throws NullPointerException
259         *             if {@link #getValue()} returns <code>null</code>
260         */
261        public boolean isToday() {
262                Validate.notNull(getValue(), getClass().getSimpleName() + " contains null value");
263                return DateUtils.isSameDay(new Date(), getValue());
264        }
265
266        @Override
267        protected Date parse(String theValue) throws IllegalArgumentException {
268                try {
269                        if (theValue.length() == 4 && ourYearPattern.matcher(theValue).matches()) {
270                                if (!isPrecisionAllowed(YEAR)) {
271                                        // ourLog.debug("Invalid date/time string (datatype " + getClass().getSimpleName() +
272                                        // " does not support YEAR precision): " + theValue);
273                                }
274                                setPrecision(YEAR);
275                                clearTimeZone();
276                                return ((ourYearFormat).parse(theValue));
277                        } else if (theValue.length() == 6 && ourYearMonthPattern.matcher(theValue).matches()) {
278                                // Eg. 198401 (allow this just to be lenient)
279                                if (!isPrecisionAllowed(MONTH)) {
280                                        // ourLog.debug("Invalid date/time string (datatype " + getClass().getSimpleName() +
281                                        // " does not support DAY precision): " + theValue);
282                                }
283                                setPrecision(MONTH);
284                                clearTimeZone();
285                                return ((ourYearMonthNoDashesFormat).parse(theValue));
286                        } else if (theValue.length() == 7 && ourYearDashMonthPattern.matcher(theValue).matches()) {
287                                // E.g. 1984-01 (this is valid according to the spec)
288                                if (!isPrecisionAllowed(MONTH)) {
289                                        // ourLog.debug("Invalid date/time string (datatype " + getClass().getSimpleName() +
290                                        // " does not support MONTH precision): " + theValue);
291                                }
292                                setPrecision(MONTH);
293                                clearTimeZone();
294                                return ((ourYearMonthFormat).parse(theValue));
295                        } else if (theValue.length() == 8 && ourYearMonthDayPattern.matcher(theValue).matches()) {
296                                // Eg. 19840101 (allow this just to be lenient)
297                                if (!isPrecisionAllowed(DAY)) {
298                                        // ourLog.debug("Invalid date/time string (datatype " + getClass().getSimpleName() +
299                                        // " does not support DAY precision): " + theValue);
300                                }
301                                setPrecision(DAY);
302                                clearTimeZone();
303                                return ((ourYearMonthDayNoDashesFormat).parse(theValue));
304                        } else if (theValue.length() == 10 && ourYearDashMonthDashDayPattern.matcher(theValue).matches()) {
305                                // E.g. 1984-01-01 (this is valid according to the spec)
306                                if (!isPrecisionAllowed(DAY)) {
307                                        // ourLog.debug("Invalid date/time string (datatype " + getClass().getSimpleName() +
308                                        // " does not support DAY precision): " + theValue);
309                                }
310                                setPrecision(DAY);
311                                clearTimeZone();
312                                return ((ourYearMonthDayFormat).parse(theValue));
313                        } else if (theValue.length() >= 16) { // date and time with possible time zone
314                                int firstColonIndex = theValue.indexOf(':');
315                                if (firstColonIndex == -1) {
316                                        throw new IllegalArgumentException("Invalid date/time string: " + theValue);
317                                }
318                                
319                                boolean hasSeconds = theValue.length() > firstColonIndex+3 ? theValue.charAt(firstColonIndex+3) == ':' : false; 
320                                
321                                int dotIndex = theValue.length() >= 18 ? theValue.indexOf('.', 18): -1;
322                                boolean hasMillis = dotIndex > -1;
323
324//                              if (!hasMillis && !isPrecisionAllowed(SECOND)) {
325                                        // ourLog.debug("Invalid date/time string (data type does not support SECONDS precision): " +
326                                        // theValue);
327//                              } else if (hasMillis && !isPrecisionAllowed(MILLI)) {
328                                        // ourLog.debug("Invalid date/time string (data type " + getClass().getSimpleName() +
329                                        // " does not support MILLIS precision):" + theValue);
330//                              }
331
332                                Date retVal;
333                                if (hasMillis) {
334                                        try {
335                                                if (hasOffset(theValue)) {
336                                                        retVal = ourYearMonthDayTimeMilliZoneFormat.parse(theValue);
337                                                } else if (theValue.endsWith("Z")) {
338                                                        retVal = ourYearMonthDayTimeMilliUTCZFormat.parse(theValue);
339                                                } else {
340                                                        retVal = ourYearMonthDayTimeMilliFormat.parse(theValue);
341                                                }
342                                        } catch (ParseException p2) {
343                                                throw new IllegalArgumentException("Invalid data/time string (" + p2.getMessage() + "): " + theValue);
344                                        }
345                                        setTimeZone(theValue, hasMillis);
346                                        setPrecision(TemporalPrecisionEnum.MILLI);
347                                } else if (hasSeconds) {
348                                        try {
349                                                if (hasOffset(theValue)) {
350                                                        retVal = ourYearMonthDayTimeZoneFormat.parse(theValue);
351                                                } else if (theValue.endsWith("Z")) {
352                                                        retVal = ourYearMonthDayTimeUTCZFormat.parse(theValue);
353                                                } else {
354                                                        retVal = ourYearMonthDayTimeFormat.parse(theValue);
355                                                }
356                                        } catch (ParseException p2) {
357                                                throw new IllegalArgumentException("Invalid data/time string (" + p2.getMessage() + "): " + theValue);
358                                        }
359
360                                        setTimeZone(theValue, hasMillis);
361                                        setPrecision(TemporalPrecisionEnum.SECOND);
362                                } else {
363                                        try {
364                                                if (hasOffset(theValue)) {
365                                                        retVal = ourYearMonthDayTimeMinsZoneFormat.parse(theValue);
366                                                } else if (theValue.endsWith("Z")) {
367                                                        retVal = ourYearMonthDayTimeMinsUTCZFormat.parse(theValue);
368                                                } else {
369                                                        retVal = ourYearMonthDayTimeMinsFormat.parse(theValue);
370                                                }
371                                        } catch (ParseException p2) {
372                                                throw new IllegalArgumentException("Invalid data/time string (" + p2.getMessage() + "): " + theValue, p2);
373                                        }
374
375                                        setTimeZone(theValue, hasMillis);
376                                        setPrecision(TemporalPrecisionEnum.MINUTE);
377                                }
378
379                                return retVal;
380                        } else {
381                                throw new IllegalArgumentException("Invalid date/time string (invalid length): " + theValue);
382                        }
383                } catch (ParseException e) {
384                        throw new IllegalArgumentException("Invalid date string (" + e.getMessage() + "): " + theValue);
385                }
386        }
387
388        /**
389         * Sets the precision for this datatype using field values from {@link Calendar}. Valid values are:
390         * <ul>
391         * <li>{@link Calendar#SECOND}
392         * <li>{@link Calendar#DAY_OF_MONTH}
393         * <li>{@link Calendar#MONTH}
394         * <li>{@link Calendar#YEAR}
395         * </ul>
396         * 
397         * @throws IllegalArgumentException
398         */
399        public void setPrecision(TemporalPrecisionEnum thePrecision) throws IllegalArgumentException {
400                if (thePrecision == null) {
401                        throw new NullPointerException("Precision may not be null");
402                }
403                myPrecision = thePrecision;
404                updateStringValue();
405        }
406
407        private void setTimeZone(String theValueString, boolean hasMillis) {
408                clearTimeZone();
409                int timeZoneStart = 19;
410                if (hasMillis)
411                        timeZoneStart += 4;
412                if (theValueString.endsWith("Z")) {
413                        setTimeZoneZulu(true);
414                } else if (theValueString.indexOf("GMT", timeZoneStart) != -1) {
415                        setTimeZone(getTimeZone(theValueString.substring(timeZoneStart)));
416                } else if (theValueString.indexOf('+', timeZoneStart) != -1 || theValueString.indexOf('-', timeZoneStart) != -1) {
417                        setTimeZone(getTimeZone("GMT" + theValueString.substring(timeZoneStart)));
418                }
419        }
420
421        public void setTimeZone(TimeZone theTimeZone) {
422                myTimeZone = theTimeZone;
423                updateStringValue();
424        }
425
426        public void setTimeZoneZulu(boolean theTimeZoneZulu) {
427                myTimeZoneZulu = theTimeZoneZulu;
428                updateStringValue();
429        }
430
431        /**
432         * Sets the value of this date/time using the default level of precision
433         * for this datatype
434         * using the system local time zone
435         * 
436         * @param theValue
437         *            The date value
438         */
439        @Override
440        public BaseDateTimeType setValue(Date theValue) {
441                if (myTimeZoneZulu == false && myTimeZone == null) {
442                        myTimeZone = TimeZone.getDefault();
443                }
444                myPrecision = getDefaultPrecisionForDatatype();
445                BaseDateTimeType retVal = (BaseDateTimeType) super.setValue(theValue);
446                return retVal;
447        }
448
449        /**
450         * Sets the value of this date/time using the specified level of precision
451         * using the system local time zone
452         * 
453         * @param theValue
454         *            The date value
455         * @param thePrecision
456         *            The precision
457         * @throws IllegalArgumentException
458         */
459        public void setValue(Date theValue, TemporalPrecisionEnum thePrecision) throws IllegalArgumentException {
460                if (myTimeZoneZulu == false && myTimeZone == null) {
461                        myTimeZone = TimeZone.getDefault();
462                }
463                myPrecision = thePrecision;
464                super.setValue(theValue);
465        }
466
467        @Override
468        public void setValueAsString(String theValue) throws IllegalArgumentException {
469                clearTimeZone();
470                super.setValueAsString(theValue);
471        }
472
473        /**
474         * For unit tests only
475         */
476        static List<FastDateFormat> getFormatters() {
477                return ourFormatters;
478        }
479
480        public boolean before(DateTimeType theDateTimeType) {
481                return getValue().before(theDateTimeType.getValue());
482        }
483
484        public boolean after(DateTimeType theDateTimeType) {
485                return getValue().after(theDateTimeType.getValue());
486        }
487
488  /**
489   * Returns a human readable version of this date/time using the system local format.
490   * <p>
491   * <b>Note on time zones:</b> This method renders the value using the time zone that is contained within the value.
492   * For example, if this date object contains the value "2012-01-05T12:00:00-08:00",
493   * the human display will be rendered as "12:00:00" even if the application is being executed on a system in a
494   * different time zone. If this behaviour is not what you want, use
495   * {@link #toHumanDisplayLocalTimezone()} instead.
496   * </p>
497   */
498  public String toHumanDisplay() {
499    return DateTimeUtil.toHumanDisplay(getTimeZone(), getPrecision(), getValue(), getValueAsString());
500  }
501
502  /**
503   * Returns a human readable version of this date/time using the system local format, converted to the local timezone
504   * if neccesary.
505   *
506   * @see #toHumanDisplay() for a method which does not convert the time to the local timezone before rendering it.
507   */
508  public String toHumanDisplayLocalTimezone() {
509    return DateTimeUtil.toHumanDisplayLocalTimezone(getPrecision(), getValue(), getValueAsString());
510  }
511
512
513        /**
514         * Returns a view of this date/time as a Calendar object
515         */
516        public Calendar toCalendar() {
517                Calendar retVal = Calendar.getInstance();
518                retVal.setTime(getValue());
519                retVal.setTimeZone(getTimeZone());
520                return retVal;
521        }
522
523        /**
524         * Sets the TimeZone offset in minutes relative to GMT
525         */
526        public void setOffsetMinutes(int theZoneOffsetMinutes) {
527                int offsetAbs = Math.abs(theZoneOffsetMinutes);
528
529                int mins = offsetAbs % 60;
530                int hours = offsetAbs / 60;
531
532                if (theZoneOffsetMinutes < 0) {
533                        setTimeZone(getTimeZone("GMT-" + hours + ":" + mins));
534                } else {
535                        setTimeZone(getTimeZone("GMT+" + hours + ":" + mins));
536                }
537        }
538
539        /**
540         * Returns the time in millis as represented by this Date/Time
541         */
542        public long getTime() {
543                return getValue().getTime();
544        }
545
546        /**
547         * Adds the given amount to the field specified by theField
548         * 
549         * @param theField
550         *            The field, uses constants from {@link Calendar} such as {@link Calendar#YEAR}
551         * @param theValue
552         *            The number to add (or subtract for a negative number)
553         */
554        public void add(int theField, int theValue) {
555                switch (theField) {
556                case Calendar.YEAR:
557                        setValue(DateUtils.addYears(getValue(), theValue), getPrecision());
558                        break;
559                case Calendar.MONTH:
560                        setValue(DateUtils.addMonths(getValue(), theValue), getPrecision());
561                        break;
562                case Calendar.DATE:
563                        setValue(DateUtils.addDays(getValue(), theValue), getPrecision());
564                        break;
565                case Calendar.HOUR:
566                        setValue(DateUtils.addHours(getValue(), theValue), getPrecision());
567                        break;
568                case Calendar.MINUTE:
569                        setValue(DateUtils.addMinutes(getValue(), theValue), getPrecision());
570                        break;
571                case Calendar.SECOND:
572                        setValue(DateUtils.addSeconds(getValue(), theValue), getPrecision());
573                        break;
574                case Calendar.MILLISECOND:
575                        setValue(DateUtils.addMilliseconds(getValue(), theValue), getPrecision());
576                        break;
577                default:
578                        throw new IllegalArgumentException("Unknown field constant: " + theField);
579                }
580        }
581
582        protected void setValueAsV3String(String theV3String) {
583                if (StringUtils.isBlank(theV3String)) {
584                        setValue(null);
585                } else {
586                        StringBuilder b = new StringBuilder();
587                        String timeZone = null;
588                        for (int i = 0; i < theV3String.length(); i++) {
589                                char nextChar = theV3String.charAt(i);
590                                if (nextChar == '+' || nextChar == '-' || nextChar == 'Z') {
591                                        timeZone = (theV3String.substring(i));
592                                        break;
593                                }
594                                
595                                // assertEquals("2013-02-02T20:13:03-05:00", DateAndTime.parseV3("20130202201303-0500").toString());
596                                if (i == 4 || i == 6) {
597                                        b.append('-');
598                                } else if (i == 8) {
599                                        b.append('T');
600                                } else if (i == 10 || i == 12) {
601                                        b.append(':');
602                                }
603                                
604                                b.append(nextChar);
605                        }
606
607                        if (b.length() == 16)
608                                b.append(":00"); // schema rule, must have seconds
609                        if (timeZone != null && b.length() > 10) {
610                                if (timeZone.length() ==5) {
611                                        b.append(timeZone.substring(0, 3));
612                                        b.append(':');
613                                        b.append(timeZone.substring(3));
614                                }else {
615                                        b.append(timeZone);
616                                }
617                        }
618                        
619                        setValueAsString(b.toString());
620                }
621        }
622
623  private TimeZone getTimeZone(String offset) {
624    return timezoneCache.computeIfAbsent(offset, TimeZone::getTimeZone);
625  }
626
627}