001package org.hl7.fhir.dstu2016may.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 static org.apache.commons.lang3.StringUtils.isBlank;
034
035import java.util.Calendar;
036import java.util.Date;
037import java.util.GregorianCalendar;
038import java.util.Map;
039import java.util.TimeZone;
040import java.util.concurrent.ConcurrentHashMap;
041
042import ca.uhn.fhir.model.api.TemporalPrecisionEnum;
043import org.apache.commons.lang3.StringUtils;
044import org.apache.commons.lang3.Validate;
045import org.apache.commons.lang3.time.DateUtils;
046import org.apache.commons.lang3.time.FastDateFormat;
047
048import ca.uhn.fhir.parser.DataFormatException;
049import org.hl7.fhir.utilities.DateTimeUtil;
050
051public abstract class BaseDateTimeType extends PrimitiveType<Date> {
052
053  private static final long serialVersionUID = 1L;
054
055  static final long NANOS_PER_MILLIS = 1000000L;
056  static final long NANOS_PER_SECOND = 1000000000L;
057  private static final Map<String, TimeZone> timezoneCache = new ConcurrentHashMap<>();
058
059  private static final FastDateFormat ourHumanDateFormat = FastDateFormat.getDateInstance(FastDateFormat.MEDIUM);
060  private static final FastDateFormat ourHumanDateTimeFormat = FastDateFormat.getDateTimeInstance(FastDateFormat.MEDIUM, FastDateFormat.MEDIUM);
061
062  private String myFractionalSeconds;
063  private TemporalPrecisionEnum myPrecision = null;
064  private TimeZone myTimeZone;
065  private boolean myTimeZoneZulu = false;
066
067  /**
068   * Constructor
069   */
070  public BaseDateTimeType() {
071    // nothing
072  }
073
074  /**
075   * Constructor
076   *
077   * @throws DataFormatException
078   *            If the specified precision is not allowed for this type
079   */
080  public BaseDateTimeType(Date theDate, TemporalPrecisionEnum thePrecision) {
081    setValue(theDate, thePrecision);
082    if (isPrecisionAllowed(thePrecision) == false) {
083      throw new DataFormatException("Invalid date/time string (datatype " + getClass().getSimpleName() + " does not support " + thePrecision + " precision): " + theDate);
084    }
085  }
086
087  /**
088   * Constructor
089   */
090  public BaseDateTimeType(Date theDate, TemporalPrecisionEnum thePrecision, TimeZone theTimeZone) {
091    this(theDate, thePrecision);
092    setTimeZone(theTimeZone);
093  }
094
095  /**
096   * Constructor
097   *
098   * @throws DataFormatException
099   *            If the specified precision is not allowed for this type
100   */
101  public BaseDateTimeType(String theString) {
102    setValueAsString(theString);
103    if (isPrecisionAllowed(getPrecision()) == false) {
104      throw new DataFormatException("Invalid date/time string (datatype " + getClass().getSimpleName() + " does not support " + getPrecision() + " precision): " + theString);
105    }
106  }
107
108  private void clearTimeZone() {
109    myTimeZone = null;
110    myTimeZoneZulu = false;
111  }
112
113  @Override
114  protected String encode(Date theValue) {
115    if (theValue == null) {
116      return null;
117    } else {
118      GregorianCalendar cal;
119      if (myTimeZoneZulu) {
120        cal = new GregorianCalendar(getTimeZone("GMT"));
121      } else if (myTimeZone != null) {
122        cal = new GregorianCalendar(myTimeZone);
123      } else {
124        cal = new GregorianCalendar();
125      }
126      cal.setTime(theValue);
127
128      StringBuilder b = new StringBuilder();
129      leftPadWithZeros(cal.get(Calendar.YEAR), 4, b);
130      if (myPrecision.ordinal() > TemporalPrecisionEnum.YEAR.ordinal()) {
131        b.append('-');
132        leftPadWithZeros(cal.get(Calendar.MONTH) + 1, 2, b);
133        if (myPrecision.ordinal() > TemporalPrecisionEnum.MONTH.ordinal()) {
134          b.append('-');
135          leftPadWithZeros(cal.get(Calendar.DATE), 2, b);
136          if (myPrecision.ordinal() > TemporalPrecisionEnum.DAY.ordinal()) {
137            b.append('T');
138            leftPadWithZeros(cal.get(Calendar.HOUR_OF_DAY), 2, b);
139            b.append(':');
140            leftPadWithZeros(cal.get(Calendar.MINUTE), 2, b);
141            if (myPrecision.ordinal() > TemporalPrecisionEnum.MINUTE.ordinal()) {
142              b.append(':');
143              leftPadWithZeros(cal.get(Calendar.SECOND), 2, b);
144              if (myPrecision.ordinal() > TemporalPrecisionEnum.SECOND.ordinal()) {
145                b.append('.');
146                b.append(myFractionalSeconds);
147                for (int i = myFractionalSeconds.length(); i < 3; i++) {
148                  b.append('0');
149                }
150              }
151            }
152
153            if (myTimeZoneZulu) {
154              b.append('Z');
155            } else if (myTimeZone != null) {
156              int offset = myTimeZone.getOffset(theValue.getTime());
157              if (offset >= 0) {
158                b.append('+');
159              } else {
160                b.append('-');
161                offset = Math.abs(offset);
162              }
163
164              int hoursOffset = (int) (offset / DateUtils.MILLIS_PER_HOUR);
165              leftPadWithZeros(hoursOffset, 2, b);
166              b.append(':');
167              int minutesOffset = (int) (offset % DateUtils.MILLIS_PER_HOUR);
168              minutesOffset = (int) (minutesOffset / DateUtils.MILLIS_PER_MINUTE);
169              leftPadWithZeros(minutesOffset, 2, b);
170            }
171          }
172        }
173      }
174      return b.toString();
175    }
176  }
177
178  /**
179   * Returns the default precision for the given datatype
180   */
181  protected abstract TemporalPrecisionEnum getDefaultPrecisionForDatatype();
182
183  private int getOffsetIndex(String theValueString) {
184    int plusIndex = theValueString.indexOf('+', 16);
185    int minusIndex = theValueString.indexOf('-', 16);
186    int zIndex = theValueString.indexOf('Z', 16);
187    int retVal = Math.max(Math.max(plusIndex, minusIndex), zIndex);
188    if (retVal == -1) {
189      return -1;
190    }
191    if ((retVal - 2) != (plusIndex + minusIndex + zIndex)) {
192      throwBadDateFormat(theValueString);
193    }
194    return retVal;
195  }
196
197  /**
198   * Gets the precision for this datatype (using the default for the given type if not set)
199   *
200   * @see #setPrecision(TemporalPrecisionEnum)
201   */
202  public TemporalPrecisionEnum getPrecision() {
203    if (myPrecision == null) {
204      return getDefaultPrecisionForDatatype();
205    }
206    return myPrecision;
207  }
208
209  /**
210   * Returns the TimeZone associated with this dateTime's value. May return <code>null</code> if no timezone was
211   * supplied.
212   */
213  public TimeZone getTimeZone() {
214    if (myTimeZoneZulu) {
215      return getTimeZone("GMT");
216    }
217    return myTimeZone;
218  }
219
220  /**
221   * Returns the value of this object as a {@link GregorianCalendar}
222   */
223  public GregorianCalendar getValueAsCalendar() {
224    if (getValue() == null) {
225      return null;
226    }
227    GregorianCalendar cal;
228    if (getTimeZone() != null) {
229      cal = new GregorianCalendar(getTimeZone());
230    } else {
231      cal = new GregorianCalendar();
232    }
233    cal.setTime(getValue());
234    return cal;
235  }
236
237  /**
238   * To be implemented by subclasses to indicate whether the given precision is allowed by this type
239   */
240  abstract boolean isPrecisionAllowed(TemporalPrecisionEnum thePrecision);
241
242  /**
243   * Returns true if the timezone is set to GMT-0:00 (Z)
244   */
245  public boolean isTimeZoneZulu() {
246    return myTimeZoneZulu;
247  }
248
249  /**
250   * Returns <code>true</code> if this object represents a date that is today's date
251   *
252   * @throws NullPointerException
253   *            if {@link #getValue()} returns <code>null</code>
254   */
255  public boolean isToday() {
256    Validate.notNull(getValue(), getClass().getSimpleName() + " contains null value");
257    return DateUtils.isSameDay(new Date(), getValue());
258  }
259
260  private void leftPadWithZeros(int theInteger, int theLength, StringBuilder theTarget) {
261    String string = Integer.toString(theInteger);
262    for (int i = string.length(); i < theLength; i++) {
263      theTarget.append('0');
264    }
265    theTarget.append(string);
266  }
267
268  @Override
269  protected Date parse(String theValue) throws DataFormatException {
270    Calendar cal = new GregorianCalendar(0, 0, 0);
271    cal.setTimeZone(TimeZone.getDefault());
272    String value = theValue;
273    boolean fractionalSecondsSet = false;
274
275    if (value.length() > 0 && (value.charAt(0) == ' ' || value.charAt(value.length() - 1) == ' ')) {
276      value = value.trim();
277    }
278
279    int length = value.length();
280    if (length == 0) {
281      return null;
282    }
283
284    if (length < 4) {
285      throwBadDateFormat(value);
286    }
287
288    TemporalPrecisionEnum precision = null;
289    cal.set(Calendar.YEAR, parseInt(value, value.substring(0, 4), 0, 9999));
290    precision = TemporalPrecisionEnum.YEAR;
291    if (length > 4) {
292      validateCharAtIndexIs(value, 4, '-');
293      validateLengthIsAtLeast(value, 7);
294      int monthVal = parseInt(value, value.substring(5, 7), 1, 12) - 1;
295      cal.set(Calendar.MONTH, monthVal);
296      precision = TemporalPrecisionEnum.MONTH;
297      if (length > 7) {
298        validateCharAtIndexIs(value, 7, '-');
299        validateLengthIsAtLeast(value, 10);
300        cal.set(Calendar.DATE, 1); // for some reason getActualMaximum works incorrectly if date isn't set
301        int actualMaximum = cal.getActualMaximum(Calendar.DAY_OF_MONTH);
302        cal.set(Calendar.DAY_OF_MONTH, parseInt(value, value.substring(8, 10), 1, actualMaximum));
303        precision = TemporalPrecisionEnum.DAY;
304        if (length > 10) {
305          validateLengthIsAtLeast(value, 17);
306          validateCharAtIndexIs(value, 10, 'T'); // yyyy-mm-ddThh:mm:ss
307          int offsetIdx = getOffsetIndex(value);
308          String time;
309          if (offsetIdx == -1) {
310            //throwBadDateFormat(theValue);
311            // No offset - should this be an error?
312            time = value.substring(11);
313          } else {
314            time = value.substring(11, offsetIdx);
315            String offsetString = value.substring(offsetIdx);
316            setTimeZone(value, offsetString);
317            cal.setTimeZone(getTimeZone());
318          }
319          int timeLength = time.length();
320
321          validateCharAtIndexIs(value, 13, ':');
322          cal.set(Calendar.HOUR_OF_DAY, parseInt(value, value.substring(11, 13), 0, 23));
323          cal.set(Calendar.MINUTE, parseInt(value, value.substring(14, 16), 0, 59));
324          precision = TemporalPrecisionEnum.MINUTE;
325          if (timeLength > 5) {
326            validateLengthIsAtLeast(value, 19);
327            validateCharAtIndexIs(value, 16, ':'); // yyyy-mm-ddThh:mm:ss
328            cal.set(Calendar.SECOND, parseInt(value, value.substring(17, 19), 0, 59));
329            precision = TemporalPrecisionEnum.SECOND;
330            if (timeLength > 8) {
331              validateCharAtIndexIs(value, 19, '.'); // yyyy-mm-ddThh:mm:ss.SSSS
332              validateLengthIsAtLeast(value, 20);
333              int endIndex = getOffsetIndex(value);
334              if (endIndex == -1) {
335                endIndex = value.length();
336              }
337              int millis;
338              String millisString;
339              if (endIndex > 23) {
340                myFractionalSeconds = value.substring(20, endIndex);
341                fractionalSecondsSet = true;
342                endIndex = 23;
343                millisString = value.substring(20, endIndex);
344                millis = parseInt(value, millisString, 0, 999);
345              } else {
346                millisString = value.substring(20, endIndex);
347                millis = parseInt(value, millisString, 0, 999);
348                myFractionalSeconds = millisString;
349                fractionalSecondsSet = true;
350              }
351              if (millisString.length() == 1) {
352                millis = millis * 100;
353              } else if (millisString.length() == 2) {
354                millis = millis * 10;
355              }
356              cal.set(Calendar.MILLISECOND, millis);
357              precision = TemporalPrecisionEnum.MILLI;
358            }
359          }
360        }
361      } else {
362        cal.set(Calendar.DATE, 1);
363      }
364    } else {
365      cal.set(Calendar.DATE, 1);
366    }
367
368    if (fractionalSecondsSet == false) {
369      myFractionalSeconds = "";
370    }
371
372    myPrecision  = precision;
373    return cal.getTime();
374
375  }
376
377  private int parseInt(String theValue, String theSubstring, int theLowerBound, int theUpperBound) {
378    int retVal = 0;
379    try {
380      retVal = Integer.parseInt(theSubstring);
381    } catch (NumberFormatException e) {
382      throwBadDateFormat(theValue);
383    }
384
385    if (retVal < theLowerBound || retVal > theUpperBound) {
386      throwBadDateFormat(theValue);
387    }
388
389    return retVal;
390  }
391
392  /**
393   * Sets the precision for this datatype
394   *
395   * @throws DataFormatException
396   */
397  public void setPrecision(TemporalPrecisionEnum thePrecision) throws DataFormatException {
398    if (thePrecision == null) {
399      throw new NullPointerException("Precision may not be null");
400    }
401    myPrecision = thePrecision;
402    updateStringValue();
403  }
404
405  private BaseDateTimeType setTimeZone(String theWholeValue, String theValue) {
406
407    if (isBlank(theValue)) {
408      throwBadDateFormat(theWholeValue);
409    } else if (theValue.charAt(0) == 'Z') {
410      myTimeZone = null;
411      myTimeZoneZulu = true;
412    } else if (theValue.length() != 6) {
413      throwBadDateFormat(theWholeValue, "Timezone offset must be in the form \"Z\", \"-HH:mm\", or \"+HH:mm\"");
414    } else if (theValue.charAt(3) != ':' || !(theValue.charAt(0) == '+' || theValue.charAt(0) == '-')) {
415      throwBadDateFormat(theWholeValue, "Timezone offset must be in the form \"Z\", \"-HH:mm\", or \"+HH:mm\"");
416    } else {
417      parseInt(theWholeValue, theValue.substring(1, 3), 0, 23);
418      parseInt(theWholeValue, theValue.substring(4, 6), 0, 59);
419      myTimeZoneZulu = false;
420      myTimeZone = getTimeZone("GMT" + theValue);
421    }
422
423    return this;
424  }
425
426  public BaseDateTimeType setTimeZone(TimeZone theTimeZone) {
427    myTimeZone = theTimeZone;
428    myTimeZoneZulu = false;
429    updateStringValue();
430    return this;
431  }
432
433  public BaseDateTimeType setTimeZoneZulu(boolean theTimeZoneZulu) {
434    myTimeZoneZulu = theTimeZoneZulu;
435    myTimeZone = null;
436    updateStringValue();
437    return this;
438  }
439
440  /**
441   * Sets the value for this type using the given Java Date object as the time, and using the default precision for
442   * this datatype (unless the precision is already set), as well as the local timezone as determined by the local operating
443   * system. Both of these properties may be modified in subsequent calls if neccesary.
444   */
445  @Override
446  public BaseDateTimeType setValue(Date theValue) {
447    setValue(theValue, getPrecision());
448    return this;
449  }
450
451  /**
452   * Sets the value for this type using the given Java Date object as the time, and using the specified precision, as
453   * well as the local timezone as determined by the local operating system. Both of
454   * these properties may be modified in subsequent calls if neccesary.
455   *
456   * @param theValue
457   *           The date value
458   * @param thePrecision
459   *           The precision
460   * @throws DataFormatException
461   */
462  public void setValue(Date theValue, TemporalPrecisionEnum thePrecision) throws DataFormatException {
463    if (getTimeZone() == null) {
464      setTimeZone(TimeZone.getDefault());
465    }
466    myPrecision = thePrecision;
467    myFractionalSeconds = "";
468    if (theValue != null) {
469      long millis = theValue.getTime() % 1000;
470      if (millis < 0) {
471        // This is for times before 1970 (see bug #444)
472        millis = 1000 + millis;
473      }
474      String fractionalSeconds = Integer.toString((int) millis);
475      myFractionalSeconds = StringUtils.leftPad(fractionalSeconds, 3, '0');
476    }
477    super.setValue(theValue);
478  }
479
480  @Override
481  public void setValueAsString(String theValue) throws DataFormatException {
482    clearTimeZone();
483    super.setValueAsString(theValue);
484  }
485
486  private void throwBadDateFormat(String theValue) {
487    throw new DataFormatException("Invalid date/time format: \"" + theValue + "\"");
488  }
489
490  private void throwBadDateFormat(String theValue, String theMesssage) {
491    throw new DataFormatException("Invalid date/time format: \"" + theValue + "\": " + theMesssage);
492  }
493
494  /**
495   * Returns a human readable version of this date/time using the system local format.
496   * <p>
497   * <b>Note on time zones:</b> This method renders the value using the time zone that is contained within the value.
498   * For example, if this date object contains the value "2012-01-05T12:00:00-08:00",
499   * the human display will be rendered as "12:00:00" even if the application is being executed on a system in a
500   * different time zone. If this behaviour is not what you want, use
501   * {@link #toHumanDisplayLocalTimezone()} instead.
502   * </p>
503   */
504  public String toHumanDisplay() {
505    return DateTimeUtil.toHumanDisplay(getTimeZone(), getPrecision(), getValue(), getValueAsString());
506  }
507
508  /**
509   * Returns a human readable version of this date/time using the system local format, converted to the local timezone
510   * if neccesary.
511   *
512   * @see #toHumanDisplay() for a method which does not convert the time to the local timezone before rendering it.
513   */
514  public String toHumanDisplayLocalTimezone() {
515    return DateTimeUtil.toHumanDisplayLocalTimezone(getPrecision(), getValue(), getValueAsString());
516  }
517
518  private void validateCharAtIndexIs(String theValue, int theIndex, char theChar) {
519    if (theValue.charAt(theIndex) != theChar) {
520      throwBadDateFormat(theValue, "Expected character '" + theChar + "' at index " + theIndex + " but found " + theValue.charAt(theIndex));
521    }
522  }
523
524  private void validateLengthIsAtLeast(String theValue, int theLength) {
525    if (theValue.length() < theLength) {
526      throwBadDateFormat(theValue);
527    }
528  }
529
530  /**
531   * Returns the year, e.g. 2015
532   */
533  public Integer getYear() {
534    return getFieldValue(Calendar.YEAR);
535  }
536
537  /**
538   * Returns the month with 0-index, e.g. 0=January
539   */
540  public Integer getMonth() {
541    return getFieldValue(Calendar.MONTH);
542  }
543
544  /**
545   * Returns the month with 1-index, e.g. 1=the first day of the month
546   */
547  public Integer getDay() {
548    return getFieldValue(Calendar.DAY_OF_MONTH);
549  }
550
551  /**
552   * Returns the hour of the day in a 24h clock, e.g. 13=1pm
553   */
554  public Integer getHour() {
555    return getFieldValue(Calendar.HOUR_OF_DAY);
556  }
557
558  /**
559   * Returns the minute of the hour in the range 0-59
560   */
561  public Integer getMinute() {
562    return getFieldValue(Calendar.MINUTE);
563  }
564
565  /**
566   * Returns the second of the minute in the range 0-59
567   */
568  public Integer getSecond() {
569    return getFieldValue(Calendar.SECOND);
570  }
571
572  /**
573   * Returns the milliseconds within the current second.
574   * <p>
575   * Note that this method returns the
576   * same value as {@link #getNanos()} but with less precision.
577   * </p>
578   */
579  public Integer getMillis() {
580    return getFieldValue(Calendar.MILLISECOND);
581  }
582
583  /**
584   * Returns the nanoseconds within the current second
585   * <p>
586   * Note that this method returns the
587   * same value as {@link #getMillis()} but with more precision.
588   * </p>
589   */
590  public Long getNanos() {
591    if (isBlank(myFractionalSeconds)) {
592      return null;
593    }
594    String retVal = StringUtils.rightPad(myFractionalSeconds, 9, '0');
595    retVal = retVal.substring(0, 9);
596    return Long.parseLong(retVal);
597  }
598
599  /**
600   * Sets the year, e.g. 2015
601   */
602  public BaseDateTimeType setYear(int theYear) {
603    setFieldValue(Calendar.YEAR, theYear, null, 0, 9999);
604    return this;
605  }
606
607  /**
608   * Sets the month with 0-index, e.g. 0=January
609   */
610  public BaseDateTimeType setMonth(int theMonth) {
611    setFieldValue(Calendar.MONTH, theMonth, null, 0, 11);
612    return this;
613  }
614
615  /**
616   * Sets the month with 1-index, e.g. 1=the first day of the month
617   */
618  public BaseDateTimeType setDay(int theDay) {
619    setFieldValue(Calendar.DAY_OF_MONTH, theDay, null, 0, 31);
620    return this;
621  }
622
623  /**
624   * Sets the hour of the day in a 24h clock, e.g. 13=1pm
625   */
626  public BaseDateTimeType setHour(int theHour) {
627    setFieldValue(Calendar.HOUR_OF_DAY, theHour, null, 0, 23);
628    return this;
629  }
630
631  /**
632   * Sets the minute of the hour in the range 0-59
633   */
634  public BaseDateTimeType setMinute(int theMinute) {
635    setFieldValue(Calendar.MINUTE, theMinute, null, 0, 59);
636    return this;
637  }
638
639  /**
640   * Sets the second of the minute in the range 0-59
641   */
642  public BaseDateTimeType setSecond(int theSecond) {
643    setFieldValue(Calendar.SECOND, theSecond, null, 0, 59);
644    return this;
645  }
646
647  /**
648   * Sets the milliseconds within the current second.
649   * <p>
650   * Note that this method sets the
651   * same value as {@link #setNanos(long)} but with less precision.
652   * </p>
653   */
654  public BaseDateTimeType setMillis(int theMillis) {
655    setFieldValue(Calendar.MILLISECOND, theMillis, null, 0, 999);
656    return this;
657  }
658
659  /**
660   * Sets the nanoseconds within the current second
661   * <p>
662   * Note that this method sets the
663   * same value as {@link #setMillis(int)} but with more precision.
664   * </p>
665   */
666  public BaseDateTimeType setNanos(long theNanos) {
667    validateValueInRange(theNanos, 0, NANOS_PER_SECOND-1);
668    String fractionalSeconds = StringUtils.leftPad(Long.toString(theNanos), 9, '0');
669
670    // Strip trailing 0s
671    for (int i = fractionalSeconds.length(); i > 0; i--) {
672      if (fractionalSeconds.charAt(i-1) != '0') {
673        fractionalSeconds = fractionalSeconds.substring(0, i);
674        break;
675      }
676    }
677    int millis = (int)(theNanos / NANOS_PER_MILLIS);
678    setFieldValue(Calendar.MILLISECOND, millis, fractionalSeconds, 0, 999);
679    return this;
680  }
681
682  private void setFieldValue(int theField, int theValue, String theFractionalSeconds, int theMinimum, int theMaximum) {
683    validateValueInRange(theValue, theMinimum, theMaximum);
684    Calendar cal;
685    if (getValue() == null) {
686      cal = new GregorianCalendar(0, 0, 0);
687    } else {
688      cal = getValueAsCalendar();
689    }
690    if (theField != -1) {
691      cal.set(theField, theValue);
692    }
693    if (theFractionalSeconds != null) {
694      myFractionalSeconds = theFractionalSeconds;
695    } else if (theField == Calendar.MILLISECOND) {
696      myFractionalSeconds = StringUtils.leftPad(Integer.toString(theValue), 3, '0');
697    }
698    super.setValue(cal.getTime());
699  }
700
701  private void validateValueInRange(long theValue, long theMinimum, long theMaximum) {
702    if (theValue < theMinimum || theValue > theMaximum) {
703      throw new IllegalArgumentException("Value " + theValue + " is not between allowable range: " + theMinimum + " - " + theMaximum);
704    }
705  }
706
707  private Integer getFieldValue(int theField) {
708    if (getValue() == null) {
709      return null;
710    }
711    Calendar cal = getValueAsCalendar();
712    return cal.get(theField);
713  }
714
715  protected void setValueAsV3String(String theV3String) {
716    if (StringUtils.isBlank(theV3String)) {
717      setValue(null);
718    } else {
719      StringBuilder b = new StringBuilder();
720      String timeZone = null;
721      for (int i = 0; i < theV3String.length(); i++) {
722        char nextChar = theV3String.charAt(i);
723        if (nextChar == '+' || nextChar == '-' || nextChar == 'Z') {
724          timeZone = (theV3String.substring(i));
725          break;
726        }
727
728        // assertEquals("2013-02-02T20:13:03-05:00", DateAndTime.parseV3("20130202201303-0500").toString());
729        if (i == 4 || i == 6) {
730          b.append('-');
731        } else if (i == 8) {
732          b.append('T');
733        } else if (i == 10 || i == 12) {
734          b.append(':');
735        }
736
737        b.append(nextChar);
738      }
739
740      if (b.length() == 16)
741        b.append(":00"); // schema rule, must have seconds
742      if (timeZone != null && b.length() > 10) {
743        if (timeZone.length() ==5) {
744          b.append(timeZone.substring(0, 3));
745          b.append(':');
746          b.append(timeZone.substring(3));
747        }else {
748          b.append(timeZone);
749        }
750      }
751
752      setValueAsString(b.toString());
753    }
754  }
755
756  private TimeZone getTimeZone(String offset) {
757    return timezoneCache.computeIfAbsent(offset, TimeZone::getTimeZone);
758  }
759
760}