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