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