package nxm.sys.lib;

import java.io.Serializable;
import java.math.BigDecimal;
import java.math.MathContext;
import java.text.ParseException;
import java.util.Calendar;
import java.util.Date;
import java.util.GregorianCalendar;
import java.util.TimeZone;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import javax.xml.datatype.DatatypeConfigurationException;
import javax.xml.datatype.DatatypeFactory;
import javax.xml.datatype.XMLGregorianCalendar;

import nxm.sys.inc.InternalUseOnly;

/** A class to handle time related functions. Midas time is defined as "seconds since 01-JAN-1950."
    For historical purposes Midas time is stored as two doubles:
    <pre>
      wsec  - Whole seconds since 01-JAN-1950 (i.e. Midas epoch)
      fsec  - Fractional seconds.
    </pre>
    The total number of seconds elapsed is thus the sum of the two values ({@code sec = wsec + fsec}),
    however this computation may result in a loss of precision. <br>
    <br>
    Special handling for most time formats allows values less than one day to be represented simply
    in HMS format (i.e. if {@code sec < 86400} assume it is a time-of-day rather than a date+time
    value). The following textual time formats are supported:
    <pre>
      Format Name         Description                     Format                      Example
      -------------------------------------------------------------------------------------------------------------
      {@link #STD}      - STanDard time code format.      yyyy:MM:dd::HH:mm:ss[.SSS]  1993:02:25::14:42:57.123456
                                                          yyyy-MM-dd::HH:mm:ss[.SSS]  (for input only)
                                                          T:yyyy:MM:dd::HH:mm:ss[.SSS](for input only)
                                                          HH:mm:ss.SSS                (output when time is 1 day or less)
                                                          DDD::HH:mm:ss.SSS           (output when time is 1 year or less)
      {@link #ACQ}      - ACQisition time code format.    [yy]yy.DDD:HH:mm:ss[.SSS]   93.056:14:42:57.123456
                                                          [:]HH:mm:ss[.SSS]           (for input only)
                                                          [yy]yy.DDD[:]               (for input only)
                                                          [yy]yyMMdd:HH:mm:ss[.SSS]   (for input only)
                                                          [yy]yyMMdd[:]               (for input only)
                                                          HH:mm:ss.SSS                (output when time is 1 day or less)
                                                          DDD:HH:mm:ss.SSS           (output when time is 1 year or less)
      {@link #EPOCH}    - EPOCH quadwords for time code.  yyyy:sec_in_year            1993:4804977.123456
                                                          sec_in_year                 (output when time is 1 year or less)
      {@link #FILENAME} - Filename time format.           yyyyMMdd_HHmmss[SSS]        19930225_144257123456
      {@link #NORAD}    - NORAD timecode format [1].      yyDDD.frac_of_day           93056.61316115
      {@link #TCR}      - TimeCode Reader format.         D:HH:mm:ss.SSS              56:14:42:57.123456
      {@link #VAX}      - VAX time format.                dd-MMM-yyyy:HH:mm:ss.SSS    25-FEB-1993:14:42:57.123456
                                                          HH:mm:ss.SSS                (output when time is 1 day or less)
      {@link #HMS}      - Hour-Min-Sec time format.       HH:mm:ss.SSS                14:42:57.123456
      {@link #YMD}      - Year-Month-Day time format.     yyyy:MM:dd                  1993:02:25
      {@link #ISO8601}  - ISO 8601 time format.           yyyy-MM-ddTHH:mm:ss.SSSZ    1993-02-25T14:42:57.123456Z
      {@link #FULL_STD} - Full STanDard time code format. yyyy:MM:dd::HH:mm:ss.SSS    1993:02:25::14:42:57.123456
                          (year, month, and day always included, even when time is 1 day or less)
      {@link #ACQDATE}  - ACQDATE time code format.       yy.DDD                      93.056
                                                          [yy]yyMMdd                  (for input only)
      {@link #ACQTIME}  - ACQTIME time code format.       HH:mm:ss                    14:42:57

      Or they can be any of the patterns accepted by {@link java.text.SimpleDateFormat}.
      Here is a mini guide to some of the above format pattern letters:
        D=Day in year (1-366),  d=Day in month (1-31),   M=Month in year (1-12),
        H=Hour in day (0-23),   h=Hour in am/pm (1-12),  m=Minute in hour (0-59),
        s=whole seconds in minute (0-59),
        S=fractional seconds (up {@value #MAX_PRECISION} decimal places)
        (NOTE: above doc difference vs S for milliseconds in SimpleDateFormat).
    </pre>
    <br>
    [1] Note : For {@link #NORAD} time, the <code>frac_of_day</code> is always rounded to
               8 decimal places. This may result in some loss of precision when going
               {@code Time -> String -> Time}.
               <br>
    [2] Warning: Input time is not supported at/past {@link #MAX_INPUT_WSEC} (~1.855E14) which is in
                 the year 5,879,489.
                 <br>
    [3] Warning: Various methods like {@link #ymd2mjd(int, int, int)} will <b>not work</b> up
                 to/past 2100 when the leap year exception rule comes into play (i.e. years that
                 are integer multiples of 100 are not leap years, unless they are also integer
                 multiples of 400). E.g. 1900, 2100, 2200, 2300, etc. are not leap years.
                 <br>
    [4] Note : Since the fractional second is stored as a 64-bit double precision floating point value,
               the smallest spacing between fractions is 2^(-52) ~ 1.11E-16, hence it can
               represent values up to a femtosecond (1E-15) but in the general case, it is only guaranteed to
               picosecond precision ({@link #MAX_PRECISION} decimal places.)
               <br>
    <br>
    For more control over time/date formatting, please see the {@link MFormat} class.
    @author Jeff Schoen
    @author J. Czechowski IV
 */
public class Time extends Object implements Comparable<Object>, Serializable {
  /** NOTE: this should be changed if the structure/fields of this class changes. */
  private static final long serialVersionUID = 202210141030L; // for Serializable interface

  /** stores the last tag sent to any caller.  This variable should only
      be accessed inside a synchronized region.  */
  private static double lastTag = 0;

  /** string version of last tag used by previous caller.  This variable
      should only be accessed inside a synchronized region. */
  private static String strTag;

  /** Default precision (decimal places) . This is final as of NXM 2.5.0 since the set and get methods for it were
      private and never called by anyone.
   */
  private static final int defPrecision = 3;

  /** maximum precision allowed for time format (picoseconds) */
  public static final int MAX_PRECISION = 12;
  /** minimum precision allowed for time format (seconds) */
  public static final int MIN_PRECISION = 0;

  /** Time Format: STanDard (yyyy:MM:dd::HH:mm:ss.SSS) */
  public static final int STD      = 1;
  /** Time Format: ACQisition ([yy]yy.DDD:HH:mm:ss[.SSS], see class description for other forms) */
  public static final int ACQ      = 2;
  /** Time Format: EPOCH quadwords (yyyy:sec_in_year) */
  public static final int EPOCH    = 3;
  /** Time Format: NORAD (yyDDD.frac_of_day) */
  public static final int NORAD    = 4;
  /** Time Format: TimeCode Reader (D:HH:mm:ss.SSS) */
  public static final int TCR      = 5;
  /** Time Format: VAX (dd-MMM-yyyy:HH:mm:ss.SSS) */
  public static final int VAX      = 6;
  /** Time Format: FILENAME (yyyyMMdd_HHmmssS[SS]) */
  public static final int FILENAME = 7;
  /** Time Format: Hours-Mins-Secs (HH:mm:ss.SSS) */
  public static final int HMS      = 8;
  /** Time Format: Year-Month-Day (yyyy:MM:dd) */
  public static final int YMD      = 9;
  /** Time Format: ISO 8601:2004 (yyyy-MM-ddTHH:mm:ssZ). Note that ISO 8601:2004
      specifically disallows two-digit years (which were permissible under ISO
      8601:2000, but caused ambiguity). An intentional deviation from ISO
      8601:2004 is that "-00:00" is permitted as a valid time-zone (this matches
      RFC 3339, whereas strict adherence to ISO 8601:2004 requires "+00:00" or "Z")
      -- note that this only affects parsing as date strings formed by this class
      always use "Z" as the time-zone specifier. <br>
      <br>
      <i>This identifier is not named "RFC3339" since (with the exception noted
      above) RFC 3339 is a subset of ISO 8601. All strings formed by
      <code>toString(ISO8601,dp)</code> are compatible with *both* ISO 8601:2004 and
      RFC 3339, while <code>fromString(str,ISO8601)</code> accepts any string that
      is compatible with *either* RFC 3339 or ISO 8601:2004.</i>
      @since NeXtMidas 3.3.0.
   */
  public static final int ISO8601  = 10;

  /** Time Format: Full STanDard (yyyy:MM:dd::HH:mm:ss.SSS) even when Time less than 1 day. Since 3.5.2 Bug 2864. */
  public static final int FULL_STD = 11;

  /** Time Format: ACQDATE (yy.ddd or yyyymmdd)  */
  public static final int ACQDATE = 12;// These values matching the first 3 letters of ACQ should be ok as they check in order

  /** Time Format: ACQTIME (HH:MM:SS) */
  public static final int ACQTIME = 13;// These values matching the first 3 letters of ACQ should be ok as they check in order

  /** The list of time formats. */
  public static final String formatList = "Std,Acq,Epoch,Norad,TCR,Vax,Filename,HMS,YMD,ISO8601,Full_Std,AcqDate,AcqTime";
  // Note: It is best to avoid adding new formats that have a similar beginning to existing Time formats,
  // especially formats that contain an existing format as its prefix.
  // 1) Since a case sensitive to find a match/partial match is done first, in some cases the wrong format could be
  //    chosen (ex: if a STD_FULL were added and the search was for "STD", "STD_FULL" would be chosen over "Std")
  // 2) Warnings are issued if there is no exact match and multiple partial matches (ex: if a Std_Full were added
  //    and the search was for "ST", a warning would be issued and the 1st partial match would "Std" would be chosen.)

  /** The list of months (3-letter trigraphs).
      @see #monthNameList
   */
  public static final String monthList  = "JAN,FEB,MAR,APR,MAY,JUN,JUL,AUG,SEP,OCT,NOV,DEC";

  /** The list of months (full names).
      @since NeXtMidas 2.5.0
      @see #monthList
   */
  public static final String monthNameList
          = "January,February,March,April,May,June,July,August,September,October,November,December";

  /** The list of days (3-letter trigraphs). This follows the {@link java.util.Calendar} class in
      numbering the days 1..7 starting with Sunday.
      @see #monthNameList
   */
  public static final String dayList = "SUN,MON,TUE,WED,THU,FRI,SAT";

  /** The list of days (full names). This follows the {@link java.util.Calendar} class in
      numbering the days 1..7 starting with Sunday.
      @since NeXtMidas 2.5.0
      @see #monthList
   */
  public static final String dayNameList
          = "Sunday,Monday,Tuesday,Wednesday,Thursday,Friday,Saturday";

  /** Day In Month map. Maps the day-of-year to the month that day falls in.
      <pre>
        NON-LEAP-YEARS:
          (<i>day</i> &gt;= dim[0]) &amp;&amp; (<i>day</i> &lt; dim[1])  --&gt; JAN
          (<i>day</i> &gt;= dim[8]) &amp;&amp; (<i>day</i> &lt; dim[9])  --&gt; SEP

        LEAP-YEARS:
          (<i>day</i> &gt;= dim[0+12]) &amp;&amp; (<i>day</i> &lt; dim[1+12])  --&gt; JAN
          (<i>day</i> &gt;= dim[8+12]) &amp;&amp; (<i>day</i> &lt; dim[9+12])  --&gt; SEP
      </pre>
   */
  public static final int dim[] = {
    0,31,59,90,120,151,181,212,243,273,304,334,
    0,31,60,91,121,152,182,213,244,274,305,335 };

  /** Seconds added when converting from a J1950 time to a J1970 time (<code>J1950TOJ1970={@value}</code>). */
  public static final int J1950TOJ1970 = -631152000;
  /** Seconds added when converting from a J1970 time to a J1950 time (<code>J1970TOJ1950={@value}</code>). */
  public static final int J1970TOJ1950 = 631152000;
  /** Seconds added when converting from a J1950 time to a J2000 time (<code>J1950TOJ2000={@value}</code>). */
  public static final int J1950TOJ2000 = -1577836800;
  /** Seconds added when converting from a J2000 time to a J1950 time (<code>J2000TOJ1950={@value}</code>). */
  public static final int J2000TOJ1950 = 1577836800;
  /** Seconds added when converting from a J1970 time to a J2000 time (<code>J1970TOJ2000={@value}</code>). */
  public static final int J1970TOJ2000 = -946684800;
  /** Seconds added when converting from a J2000 time to a J1970 time (<code>J2000TOJ1970={@value}</code>). */
  public static final int J2000TOJ1970 = 946684800;
  /** Days added when converting from a Midas Julian Day (MJD) in seconds-since-1950 to a Julian
      Day (JD) (<code>MJDTOJD={@value}</code>). The fractional part (<code>0.5</code>) accounts for the
      fact that a Julian Day begins at noon rather than midnight.
   */
  public static final double MJDTOJD = 2433282.5;

  /** Seconds Per Minute = 60 seconds in a minute */
  public static final int SECONDS_PER_MINUTE = 60;
  /** Seconds Per Hour = 60 minutes * 60 seconds in an hour */
  public static final int SECONDS_PER_HOUR = 3600;
  /** Seconds Per Day = 24 hours * 60 minutes * 60 seconds in a day */
  public static final int SECONDS_PER_DAY  = 86400;
  /** Seconds Per Week (7 days) = 7 days * 24 hours * 60 minutes * 60 seconds in a year */
  public static final int SECONDS_PER_WEEK  =   7 * SECONDS_PER_DAY;
  /** Seconds Per Month (31 days) = 31 days * 24 hours * 60 minutes * 60 seconds in a year */
  public static final int SECONDS_PER_MONTH =  31 * SECONDS_PER_DAY;
  /** Seconds Per Year (non-leap year) = 365 days * 24 hours * 60 minutes * 60 seconds in a year */
  public static final int SECONDS_PER_YEAR  = 365 * SECONDS_PER_DAY;
  /** Seconds Per Leap Year = 366 days * 24 hours * 60 minutes * 60 seconds in a year */
  public static final int SECONDS_PER_LEAP_YEAR  = SECONDS_PER_YEAR + SECONDS_PER_DAY;

  /** Maximum allowable input seconds given that {@link #getYear()} calculates days as a 32-bit integer. Since 3.1.0 For BUG 2126 */
  public static final double MAX_INPUT_WSEC = Integer.MAX_VALUE * 86400.0;

  // String describing typical year,month, day format and ranges (1-12 months, 1-31 days)
  private static final String YMD_REGEX = "[0-9]{4}[:-](([0][1-9])|([1][0-2])|[1-9])[:-](([0][1-9])|([1-2][0-9])|([3][0-1])|[1-9])";
  // String describing typical hour, minute, seconds, and optional fractional seconds
  // format and ranges (0-23 hours, 0-59 minutes, 0-60 seconds)
  private static final String HMS_REGEX = "(([0-1][0-9])|([2][0-3])|[0-9]):(([0-5][0-9])|[0-9]):(([0-5][0-9])|(60)|[0-9])(\\.[0-9]*+)?";

  /** Pattern for STD Format Time String. <br>

      Since this is used to check if a string is a STD Time string, only the
      typical forms and ranges are allowed (i.e. just 12 months in a year and
      no more than 31 days in a month). This is a much more restrictive list of
      what can be considered a STD format than can be handled if Time.STD
      is specified as the format. <br><br>

      Forms considered a match: <br>
        yyyy:[M]M:[d]d::[h]h:[m]m:[s]s[.SSS] or <br>
        yyyy-[M]M-[d]d::[h]h:[m]m:[s]s[.SSS] <br>
      (if a decimal point is included, then there must be 1 or more fractional seconds)
      @since NeXtMidas 3.5.3  Bug 2876 2883
   */
  public static final Pattern STD_FORMAT_PATTERN =
      Pattern.compile(YMD_REGEX + "::" + HMS_REGEX);

  // Array index for toYMDHMS(..) and toYMDFOD(..) methods
  private static final int YMDHMS_LEN  = 14; // Length of YMDHMS array
  private static final int YMDHMS_YEAR =  0; // Index into YMDHMS array: year (e.g. 2008)
  private static final int YMDHMS_MON  =  1; // Index into YMDHMS array: mon  (1-12)
  private static final int YMDHMS_DAY  =  2; // Index into YMDHMS array: day  (1-31)
  private static final int YMDHMS_HOUR =  3; // Index into YMDHMS array: hour (0-23)
  private static final int YMDHMS_MIN  =  4; // Index into YMDHMS array: min  (0-60)
  private static final int YMDHMS_SEC  =  5; // Index into YMDHMS array: sec  (0-60)
  private static final int YMDHMS_SOY  =  6; // Index into YMDHMS array: sec-of-year
  private static final int YMDHMS_DOY  =  7; // Index into YMDHMS array: day-of-year (1-365 or 1-366)
  private static final int YMDHMS_SOD  =  8; // Index into YMDHMS array: sec-of-day  (0-86399)
  private static final int YMDHMS_SECW =  9; // Index into YMDHMS array: sec as whole seconds based on dp
  private static final int YMDHMS_SECF = 10; // Index into YMDHMS array: sec as fract seconds based on dp
  private static final int YMDHMS_FOD  = 11; // Index into YMDHMS array: fract-of-day based on dp
  private static final int YMDHMS_SOYW = 12; // Index into YMDHMS array: sec-of-year as whole based on dp
  private static final int YMDHMS_SOYF = 13; // Index into YMDHMS array: sec-of-year as fract based on dp

  /** The UTC time zone, used with conversions to {@link GregorianCalendar}. */
  private static final TimeZone UTC_TIMEZONE = TimeZone.getTimeZone("UTC");

  /** The factory used when creating a {@link XMLGregorianCalendar}. */
  private static DatatypeFactory DATATYPE_FACTORY = null;

  /** whole seconds, fractions-of-seconds */
  private double wsec,fsec;

  /** Creates a new instance that starts at time <code>0.0</code> (1 Jan 1950). */
  public Time () {
    wsec = 0.0;
    fsec = 0.0;
  }

  /** Creates a new instance that starts at the given time.
      @param seconds The number of J1950 seconds.
      @see #fromJ1950(double)
   */
  public Time (double seconds) {
    if (seconds > MAX_INPUT_WSEC) {
      Shell.warning("Invalid input time of " + seconds + " seconds. Max supported input value is " + MAX_INPUT_WSEC); // since 3.1.0 BUG 2126
    }
    fromJ1950(seconds);
  }

  /** Creates a new instance that starts at the given time.
      @param wsec The number of whole seconds since 1 Jan 1950.
      @param fsec The number of fractional seconds.
      @see #fromJ1950(double,double)
   */
  public Time (double wsec, double fsec) {
    if (wsec > MAX_INPUT_WSEC) {
      Shell.warning("Invalid input time of " + wsec + " wsec. Max supported input value is " + MAX_INPUT_WSEC); // since 3.1.0 BUG 2126
    }
    fromJ1950(wsec,fsec);
  }

  /** Creates a new instance that clones the time from the given Time object.
      @param time Time object to copy from
      @since NeXtMidas 2.8.0
   */
  public Time (Time time) {
    wsec = time.wsec;
    fsec = time.fsec;
    if (wsec > MAX_INPUT_WSEC) { // TODO: do we need to warn twice on a second
      Shell.warning("Invalid input Time of " + wsec + ". Max supported input value is " + MAX_INPUT_WSEC); // since 3.1.0 BUG 2126
    }
  }

  /** Creates a new instance that starts at the given time.
      @param str The start time as a string (<code>fmt=STD</code>).
      @see #fromString(String)
   */
  public Time (String str) {
    this(str,STD);
  }

  /** Creates a new instance that starts at the given time.
      @param str The start time as a string.
      @param fmt The time format.
      @see #fromString(String,int)
   */
  public Time (String str, int fmt) {
    fromString(str,fmt);
  }

  /** Creates a new instance based on a number of time ticks, a time delta, and a time epoch.
      This format is popular with fields in a Platinum file.
      @param timeTicks The number of time ticks.
      @param timeEpoch The time epoch.
      @param timeDelta The time delta (seconds/tick) (cannot be zero! SHOULD be {@code > 2^-53 ~ 1.11e-16})
      @see #toTimeTicks(Time, double)
      @since NeXtMidas 3.3.0
   */
  public Time (long timeTicks, Time timeEpoch, double timeDelta) {
    this(timeEpoch);
    // NOTE: Below calculations are done to keep highest precision possible!
    //       Change with care and run the TimeTest unit test cases to verify nothing breaks.
    double _wsec = Math.floor(timeTicks * timeDelta); // since timeEpoch
    long fsecTicks = timeTicks - ((long)_wsec * ticksPerSecond(timeDelta));
    double _fsec = fsecTicks * timeDelta;             // since timeEpoch
    addTime(_wsec, _fsec);
  }

  /** Creates a new instance that starts at the given time given the specified Time/simpleDate format. <br>

      Supports format from the Time.formatList, {@link java.text.SimpleDateFormat},
      and extended precision SimpleDateFomats (explained below). <br><br>

      Extended precision SimpleDateFormat interprets
      SSSS to SSSSSSSSSSSSSSS (femtoseconds) as digits of fractional seconds
      - S to SSS are still processed as standard SimpleDateFormats
      - fractional seconds are truncated to the number of digits specified
        (or a maximum of 15 for femtosecond precision). <br><br>

      Note: Formats in the Time.formatList will round and SimpleDateFormats will truncate
      (The truncation is important for the enhanced simpleDateFormat case, where fractional
      seconds and the rest of the Date are handles separately.)

      @param str           The start time as a string.
      @param simpleDateFmt Time format specified as SimpleDateFormat
      @see #fromString(String,String)
      @since NeXtMidas 3.5.4
   */
   public Time (String str, String simpleDateFmt) {
     fromString(str,simpleDateFmt);
   }

  /** Converts the time value to a number of time ticks in a given time epoch.
      This is the high precision version vs {@link #toTimeTicks(double, Time, double)}.
      <br>
      <pre>
      NOTE: Make sure timeDelta does NOT cause overflow of max long value ({@link Long#MAX_VALUE})
            for the number of ticks from the specified time epoch. See chart below
            for highest possible resolution for timeDelta.

      Max 64-bit signed long integer = {@link Long#MAX_VALUE} = 9.223372036854775807E18 (maxLong)
      seconds/day = 86400 = 8.64E4 (sod)
      timeDelta (sec/tick)
      [n] - See notes

      EPOCH         | highest resolution time delta (seconds/tick)
      --------------+-------------------------------------------------------------
      Start of Hour | 3600 / maxLong (9.2E18) (femto-sec precision)  ~ 3.9E-16  (1E-15) [1]
      --------------+-------------------------------------------------------------
      Start of Day  | 8.64E4 / maxLong (9.2E18) (pico-sec precision) ~ 9.4E-15  (1E-14) [1]
      --------------+-------------------------------------------------------------
      Start of Year | (8.64E4 * 365.25 ~ 3.16E7) / maxLong (9.2E18)  ~ 3.5E-12  (1E-11) [1]
      --------------+-------------------------------------------------------------
      J1994         | (3.16E7 * 100 yrs ~ 3.16E9) / maxLong (9.2E18) ~ 3.5E-10  (1E-8)  [1][2]
      --------------+-------------------------------------------------------------
      J1950         | (3.16E7 * 144 yrs ~ 4.55E9 / maxLong (9.22E18) ~ 5.0E-10  (1E-8)  [1][2]
      --------------+-------------------------------------------------------------
      J1950         | (3.16E7 * 292 yrs ~ 9.22E9 / maxLong (9.22E18) ~ 1.0E-9   (1E-8)  [1][2]
      --------------+-------------------------------------------------------------
      Notes:
        [1] WARN: DO NOT convert to ticks {@code > 2^53 (~ 9.007E15)} to double since it cannot be
                  exactly represented in 64-bit double-precision floating-point number (double)
                  vs a 64-bit fixed-point number (long).
        [2] Since some calculations MUST be made with doubles, there are some loss in precision
            so the theoretical highest resolution is NOT possible.


      Max number of years/days/hours = (timeDelta * maxLong) / (sod * 365.25).
      Max number of days  = (timeDelta * maxLong) / secondsPerDay.
      Max number of hours = (timeDelta * maxLong) / secondsperHour.
      Desired resolution (seconds/tick) | max years (before overflowing maxLong)
      ----------------------------------+-------------------------------
      1e-15   (1 femtosecond)           | ~2.5 hours
      ----------------------------------+-------------------------------
      1e-12   (1 picosecond)            | 106 days ~ 0.29 year
      ----------------------------------+-------------------------------
      1e-11   (10 picoseconds)          | 2.92 years
      ----------------------------------+-------------------------------
      1e-10   (100 picoseconds)         | 29.22 years
      ----------------------------------+-------------------------------
      2.5e-10 (250 picoseconds)         | 73.06 years
      ----------------------------------+-------------------------------
      1e-9    (1 nanosecond)            | 292.27 years
      ----------------------------------+-------------------------------
      1e-6    (1 microsecond)           | 292,271.02 years
      ----------------------------------+-------------------------------
      </pre>
      @param timeEpoch The time epoch.
      @param timeDelta The time delta (seconds/tick) (cannot be zero! SHOULD be {@code > 2^-53 ~ 1.11e-16})
      @return time ticks
      @since NeXtMidas 3.3.0
   */
  public long toTimeTicks (Time timeEpoch, double timeDelta) {
    // This is not implemented as 'return toTimeTicks(getSec(), timeEpoch, timeDelta)'
    // to prevent a loss of precision (keeping all calculations using long).
    // NOTE: Below calculations are done to keep highest precision possible!
    //       Change with care and run the TimeTest unit test cases to verify nothing breaks.
    long ticksPerSecond = ticksPerSecond(timeDelta);
    long wTicks = (long)(wsec - timeEpoch.wsec) * ticksPerSecond;
    long fTicks = Math.round((fsec - timeEpoch.fsec) * ticksPerSecond);
    return wTicks + fTicks;
  }

  /** Converts a time value to a number of time ticks in a given time epoch. <br>
      <br>
      This is a less-precise alternative to {@link #toTimeTicks(Time,double)}
      that can be used in situations where high-precision is not a concern, but
      where performance is. A typical use-case for this would be converting
      Midas time values already represented as a J1950 double to time ticks
      for a Platinum file.
      @param sec       The midas time (J1950 seconds).
      @param timeEpoch The time epoch.
      @param timeDelta The time delta (seconds/tick) (cannot be zero! SHOULD be {@code >  2^-53 ~ 1.11e-16})
      @return time ticks
      @see #toTimeTicks(Time, double)
      @since NeXtMidas 3.3.0
   */
  public static long toTimeTicks (double sec, Time timeEpoch, double timeDelta) {
    double tdiff = sec - timeEpoch.getSec();
    return (long)(tdiff / timeDelta);
  }

  /** Returns time as a string.
      @return The time in standard format ({@link #STD}) with default precision.
   */
  @Override
  public final String toString () {
    // NOTE: lots of macros depend on the STD and -1 decimal places output format
    return toStringBuffer(null,wsec,fsec,STD,-1).toString();
  }

  /** Returns time as a string.
      @param dp  The precision to use (number of decimal places).
      @return The time in standard format ({@link #STD}) with given precision.
   */
  public final String toString (int dp) {
    return toStringBuffer(null,wsec,fsec,STD,dp).toString();
  }

  /** Returns time as a string.
      @param fmt The time format to use (see {@link #formatList}).
      @param dp  The precision to use (number of decimal places).
      @return The time in the given format with the given precision.
   */
  public final String toString (int fmt, int dp) {
    return toStringBuffer(null,wsec,fsec,fmt,dp).toString();
  }

  /** Returns time as a string.
      @param fmtStr The time format (String) to use (see {@link #formatList}).
      @param dp  The precision to use (number of decimal places).
      @return The time in the given format with the given precision.
   */
  public final String toString (String fmtStr, int dp) {
    int fmt = Parser.find(formatList,fmtStr,-1,0,Parser.E_WARNING); // since 2.9.0: warning added for Bug 760
    return toStringBuffer(null,wsec,fsec,fmt,dp).toString();
  }

  /** Returns time as a string in specified Time/simpleDate format. <br>

      Supports format from the Time.formatList, simpleDateFormats, and extended precision
      simpleDateFomats (explained below). <br><br>

      Extended precision simpleDateFormat interprets
      SSSS to SSSSSSSSSSSSSSS (femtoseconds) as digits of fractional seconds
      - S to SSS are still processed as standard SimpleDateFormat
      - fractional seconds are truncated to the number of digits specified (or a maximum of 15 for femtosecond precision)
      <br><br>
      Note: Formats in the Time.formatList will round and simpleDateFormats will truncate
      (The truncation is important for the enhanced simpleDateFormat case, where fractional
      seconds and the rest of the Date are handles separately.)

      @param enhancedSimpleDateFormat desired Time format specified as SimpleDateFormat
      @return Time String in the format specified
      @since NeXtMidas 3.5.2 for Bug 2864
   */
  public final String toString (String enhancedSimpleDateFormat) {
    if (enhancedSimpleDateFormat.contains("SSSS")) {
      enhancedSimpleDateFormat = createHighPrecisionSimpleDateFormat(enhancedSimpleDateFormat);
    }
    return MFormat.getDateFormatFor(enhancedSimpleDateFormat).format(wsec + fsec);
  }

  /**
     Adapt simpleDateFormat String to support fractional seconds up to a femtosecond. <br>

     1) Formats matching the Time.formatList are not altered. <br>
     2) SimpleDateFormats with millisecond (SSS) or less precision are also unchanged. <br>
     3) enhancedSimpleDateFormats with 4 - 15 digits of fraction seconds specified
        will have the format altered with the fractional seconds directly inserted
        between single quotes (ex: SSSSS might become '67891'). <br>
     5) If greater than 15 digits are requested (ex: SSSSSSSSSSSSSSSSSS), only 15
        digits are returned (ex: '123456789012345'). <br>
     6) The return value has fractional seconds truncated to the specified number of digits. <br>
     7) More than one fractional second section can be specified  in the format String
        (ex: "HH:mm:ss.SSSS HH.mm.ss.SSSS")

     @param enhancedSimpleDateFormat simpleDateFormat, Time.listFormat, or high precision simpleDateFormat
     @return format String modified for high precision if necessary
     @since NeXtMidas 3.5.2 for Bug 2864
   */
  public String createHighPrecisionSimpleDateFormat (String enhancedSimpleDateFormat) {
    if (enhancedSimpleDateFormat == null) return null;

    int fracSecLocation = enhancedSimpleDateFormat.indexOf("SSSS");
    int formatStringLength = enhancedSimpleDateFormat.length();
    // Check for multiple occurrences of high precision fractional seconds
    while (fracSecLocation >= 0) {
      // get # digits fractional seconds
      int numFracSec = 4;
      int nextCharCheck = fracSecLocation + numFracSec;
      // determine how many digits of fractional seconds are specified
      while ((nextCharCheck < formatStringLength) && (enhancedSimpleDateFormat.charAt(nextCharCheck) == 'S')){
        numFracSec++;
        nextCharCheck++;
      }
      // Get substitute section for format String with fractional seconds between single quotes
      String precisionFSecAsString = getPrecisionFSecAsString(numFracSec);
      // replace the original SSS...S with this new format String
      enhancedSimpleDateFormat = enhancedSimpleDateFormat.substring(0, fracSecLocation) + "'" +
          precisionFSecAsString + "'" +enhancedSimpleDateFormat.substring(fracSecLocation + numFracSec);
      // check for more occurrences of high precision fractional seconds
      fracSecLocation = enhancedSimpleDateFormat.indexOf("SSSS");
    }
    return enhancedSimpleDateFormat;
  }

  /**
     Gets high precision string of the fractional seconds, supporting up to a femtosecond. <br>

     Note: Fractional seconds are truncated to the desired number of digits or to a maximum of 15 digits.

     @param numDigitsSpecified number of characters/digits to be in String
     @return String representation for fractional digits up to SSSSSSSSSSSSSSS
     @since NeXtMidas 3.5.2 for Bug 2864
   */
  private String getPrecisionFSecAsString (int numDigitsSpecified) {
    int numDigitsInString = Math.min(numDigitsSpecified, 15);
    long precisionMultiplier = (long)getPrecisionMult(numDigitsInString);
    // force a temporary high order digit (that will be stripped from the toString), in order to
    // generate left padding of zeros
    long toForceZeroPadding = precisionMultiplier;
    Long fSecStringPlusForceZeroFactor = (long)(fsec * precisionMultiplier) + toForceZeroPadding;
    // Convert value to String and strip off digit used to force zero padding
    String precisionFSecAsString = fSecStringPlusForceZeroFactor.toString().substring(1);
    return precisionFSecAsString;
  }

  /** Returns time as a {@link StringBuffer}.
      @param buf  The string buffer to append to.
      @param wsec Whole seconds.
      @param fsec Fractional seconds.
      @param fmt  The time format (int constant) to use (see {@link #formatList}).
      @param dp   The precision to use (number of decimal places).
      @return The time in the given format with the given precision.
      @throws IllegalArgumentException if specified time is beyond max supported value of
              {@link #MAX_INPUT_WSEC} ({@value #MAX_INPUT_WSEC}) (Since NeXtMidas 3.1.0)
   */
  private static StringBuffer toStringBuffer (StringBuffer buf, double wsec, double fsec, int fmt, int dp) {
    if (buf == null) buf = new StringBuffer(32); // 32 is just an estimate
    if (dp<0) { dp = defPrecision; }

    // handle negative time
    if (wsec <0.0) { buf.append('-'); wsec = -wsec-1; fsec = 1-fsec; }
    if (fsec>=1.0) { wsec+=1; fsec-=1; }
    if (fsec <0.0) { wsec-=1; fsec+=1; }

    if (wsec > MAX_INPUT_WSEC)
      throw new IllegalArgumentException("Invalid input date.  Input date of " + wsec + " is beyond the max value of " + MAX_INPUT_WSEC);  // since 3.1.0 BUG 2126

    int day = (int)(wsec/SECONDS_PER_DAY);
    boolean date = (day > 365) || (wsec < 0);
    double[] ymdhms = toYMDHMS(null, wsec, fsec, dp, fmt);

    switch (fmt) {
      case STD:
        if (date) getYMD(buf,ymdhms,fmt).append(':').append(':');
        else if (wsec>SECONDS_PER_DAY) buf.append(day).append(':').append(':');
        getHMS(buf,ymdhms,dp,fmt);
        break;
      case ISO8601: // since 3.3.0 for BUG 2673
        // if (date) getYMD(buf,ymdhms,fmt).append('T'); // from 3.3.0 to 3.5.3
        getYMD(buf,ymdhms,fmt).append('T'); // since 3.5.4 for BUG 2912 to always output date & time
        getHMS(buf,ymdhms,dp,fmt);
        break;
      case FILENAME:
        getYMD(buf,ymdhms,fmt).append('_');
        getHMS(buf,ymdhms,dp,fmt);
        break;
      case YMD:
        getYMD(buf,ymdhms,fmt);
        break;
      case HMS:
        getHMS(buf,ymdhms,dp,fmt);
        break;
      case VAX:
        if (date) getYMD(buf,ymdhms,fmt).append(':');
        else if (wsec>SECONDS_PER_DAY) buf.append(day).append('-');
        getHMS(buf,ymdhms,dp,fmt);
        break;
      case ACQ:
        if (date) getYYDDD(buf,ymdhms,fmt).append(':');
        else if (wsec>SECONDS_PER_DAY) buf.append(day).append(':');
        getHMS(buf,ymdhms,dp,fmt);
        break;
      case EPOCH:
        int    year = (int)ymdhms[YMDHMS_YEAR];
        String wsoy = Long.toString((long)ymdhms[YMDHMS_SOYW]);
        String fsoy = Long.toString((long)ymdhms[YMDHMS_SOYF]);
        if (date) {
          buf.append(year).append(':');
          buf.append(wsoy);
        }
        else {
          buf.append((int)wsec);
        }
        if (dp != 0) {
          buf.append('.');
          for (int i = fsoy.length(); i < dp; i++) buf.append('0');
          buf.append(fsoy);
        }
        break;
      case NORAD:
        String fod = Long.toString((long)ymdhms[YMDHMS_FOD]);
        getYYDDD(buf,ymdhms,fmt).append('.');
        for (int i = fod.length(); i < 8; i++) buf.append('0'); // special case, always use 8 places
        buf.append(fod);
        break;
      case TCR:
        getYYDDD(buf,ymdhms,fmt).append(':');
        getHMS(buf,ymdhms,dp,fmt);
        break;
      case FULL_STD:                                           // Since 3.5.2  Bug 2864
        getYMD(buf,ymdhms,fmt).append(':').append(':');
        getHMS(buf,ymdhms,dp,fmt);
        break;
      case ACQDATE:  /// ACQDATE and ACQTIME added in 3.9.3 for NXM-3139
        if (date) getYYDDD(buf,ymdhms,fmt);
        break;
      case ACQTIME:
        getHMS(buf,ymdhms,0,fmt);
        break;
      default:
        throw new MidasException("Time: Unknown time format fmt="+fmt+".");
    }
    return buf;
  }

  /** Gets the time as an YMD string, only used by toStringBuffer(..). */
  private static StringBuffer getYMD (StringBuffer buf, double[] ymdhms, int fmt) {
    int y = (int)ymdhms[YMDHMS_YEAR];
    int m = (int)ymdhms[YMDHMS_MON];
    int d = (int)ymdhms[YMDHMS_DAY];

    if (fmt==FILENAME) {
      buf.append(y).append(m/10).append(m%10).append(d/10).append(d%10);
    }
    else if (fmt==VAX) {
      String mStr = Parser.get(monthList,m);
      buf.append(d/10).append(d%10).append('-').append(mStr).append('-').append(y);
    }
    else if (fmt==ISO8601) {
      buf.append(y).append('-').append(m/10).append(m%10).append('-').append(d/10).append(d%10);
    }
    else {
      buf.append(y).append(':').append(m/10).append(m%10).append(':').append(d/10).append(d%10);
    }
    return buf;
  }

  /** Gets the time as an YMD string, only used by toStringBuffer(..). */
  private static StringBuffer getYYDDD (StringBuffer buf, double[] ymdhms, int fmt) {
    int year = (int)ymdhms[YMDHMS_YEAR];
    int yr   = year % 100;
    int doy  = (int)ymdhms[YMDHMS_DOY];

    if (fmt==ACQ) {
      buf.append(yr/10).append(yr%10).append('.').append(doy);
    } else if (fmt==ACQDATE) {
      buf.append(yr/10).append(yr%10).append('.').append(doy/100).append((doy%100)/10).append(doy%10);
    } else if (fmt==NORAD) {
      buf.append(yr/10).append(yr%10).append(doy/100).append((doy%100)/10).append(doy%10);
    } else { // fmt==TCR
      buf.append(doy);
    }
    return buf;
  }

  /** Gets the time as an HMS string, only used by toStringBuffer(..).
      @param buf StringBuffer to put result into
      @param ymdhms double[] of time usually created by {@link #toYMDHMS(double[], double, double, int, int)}
      @param dp  number of decimal places to display (for fractional seconds)
      @param fmt Time format constant (e.g. STD, HMS, etc.)
   */
  private static StringBuffer getHMS (StringBuffer buf, double[] ymdhms, int dp, int fmt) {
    int     h    = (int)ymdhms[YMDHMS_HOUR];
    int     m    = (int)ymdhms[YMDHMS_MIN];
    String  wStr = Long.toString((long)ymdhms[YMDHMS_SECW]);
    String  fStr = Long.toString((long)ymdhms[YMDHMS_SECF]);
    boolean dot  = (fmt!=FILENAME) && (dp != 0);
    int     wp   = 2;

    // hour and minute
    if (fmt==FILENAME) {
      buf.append(h/10).append(h%10);
      buf.append(m/10).append(m%10);
    } else {
      buf.append(h/10).append(h%10).append(':');
      buf.append(m/10).append(m%10).append(':');
    }

    // seconds
    if (wp != 0) {
      for (int i = wStr.length(); i < wp; i++) buf.append('0');
      buf.append(wStr);
    }

    // fractional seconds
    if (dot) buf.append('.');
    if (fStr.length() > dp) {
      fStr = fStr.substring(0,dp);
    }
    for (int i = fStr.length(); i < dp; i++) buf.append('0');
    buf.append(fStr);

    if (fmt == ISO8601) {
      buf.append('Z'); // Time-zone specifier
    }

    return buf;
  }

  /** Converts wsec, fsec to values used by toStringBuffer(..).
      These values are designed for a human readable format
      <pre>
        Basic YMDHMS values:
          ymdhms[YMDHMS_YEAR] = year (e.g. 2008)
          ymdhms[YMDHMS_MON ] = mon  (1-12)
          ymdhms[YMDHMS_DAY ] = day  (1-31)
          ymdhms[YMDHMS_HOUR] = hour (0-23)
          ymdhms[YMDHMS_MIN ] = min  (0-60)
          ymdhms[YMDHMS_SEC ] = sec  (0-60)

        Extra values that are "free" as part of YMDHMS computation:
          ymdhms[YMDHMS_SOY ] = sec-of-year (0-31535999.9999 or 0-31622399.9999)
          ymdhms[YMDHMS_DOY ] = day-of-year (1-365 or 1-366)
          ymdhms[YMDHMS_SOD ] = sec-of-day  (0-86399.9999999)

        Special values computed when (fmt!=0), otherwise NaN:
          ymdhms[YMDHMS_WSOD] = sec as whole seconds based on dp   (NaN if fmt==NORAD or fmt==EPOCH)
          ymdhms[YMDHMS_FSOD] = sec as fract seconds based on dp   (NaN if fmt==NORAD or fmt==EPOCH)
          ymdhms[YMDHMS_FOD ] = fract-of-day based on dp           (NaN if fmt!=NORAD)
          ymdhms[YMDHMS_WSOY] = sec-of-year as whole based on dp   (NaN if fmt!=EPOCH)
          ymdhms[YMDHMS_WSOY] = sec-of-year as fract based on dp   (NaN if fmt!=EPOCH)

        Note: The hour will be set to 24 if wsec+fsec==86400.0 and fmt is not TCR, FILENAME or FULL_STD.
              This is a special case that matches the behavior of NeXtMidas 2.4.2 and earlier.
      </pre>
      @param ymdhms Output array of length YMDHMS_LEN (null=allocate new array).
      @param wsec   Whole seconds. (MUST be normalized - i.e. no fractional seconds)
      @param fsec   Fractional seconds. (MUST be normalized - i.e. no whole seconds)
      @param dp     The precision to use (number of decimal places).
      @param fmt    Format to use.
      @return The output array (same as ymdhms, when applicable).
   */
  private static double[] toYMDHMS (double[] ymdhms, double wsec, double fsec, int dp, int fmt) {
    if (ymdhms == null) ymdhms = new double[YMDHMS_LEN];

    // This partially duplicates getHour(), getMinute() and getSecond() but is done this
    // way for performance reasons.
    int     yr   = getYear(wsec);
    boolean leap = isLeapYear(yr);                                     // leap year?
    double  sodx = (wsec % SECONDS_PER_DAY);                           // sec-of-day (no fsec)
    double  sod  = sodx + fsec;                                        // sec-of-day
    double  soyx = (wsec-SECONDS_PER_DAY*(double)ymd2mjd(yr,1,1));     // sec-of-year (no fsec)
    double  doy  = (int)(soyx/SECONDS_PER_DAY)+1;                      // day-of-year
    double  mult = getPrecisionMult(dp);                               // multiplier based on dp
    double  secx = (sodx % 60);                                        // Seconds (no fsec)

    int m = (leap)? 23: 11; while (dim[m] >= doy) m--; // index in month array

    // special handling for 24:00:00 (Bug 1250), exclude FULL_STD in 3.5.2 for Bug 2864, exclude ISO8601 in 3.5.4 for Bug 2912
    boolean sp24 = (wsec == SECONDS_PER_DAY) && (fsec == 0.0) && (fmt!=TCR) && (fmt!=FILENAME) && (fmt!=FULL_STD) && (fmt!=ISO8601);

    ymdhms[YMDHMS_YEAR] = yr;
    ymdhms[YMDHMS_MON ] = (m <= 11)? m+1: m-11;
    ymdhms[YMDHMS_DAY ] = doy - dim[m];
    ymdhms[YMDHMS_HOUR] = (sp24)? 24 : (int)(sodx / 3600); // since 3.5.1 using sodx instead of sod for BUG 2849
    ymdhms[YMDHMS_MIN ] = (int)((sodx % 3600) / 60);
    ymdhms[YMDHMS_SEC ] = (int)secx;
    ymdhms[YMDHMS_SOY ] = soyx + fsec;
    ymdhms[YMDHMS_DOY ] = doy;
    ymdhms[YMDHMS_SOD ] = sod;
    ymdhms[YMDHMS_SECW] = Double.NaN; // set below, if applicable
    ymdhms[YMDHMS_SECF] = Double.NaN; // set below, if applicable
    ymdhms[YMDHMS_FOD ] = Double.NaN; // set below, if applicable
    ymdhms[YMDHMS_SOYW] = Double.NaN; // set below, if applicable
    ymdhms[YMDHMS_SOYF] = Double.NaN; // set below, if applicable

    if (fmt == 0) {
      // skip extra computations
    }
    else if (fmt==NORAD) { // special case, always use 1e8 as multiplier
      long fod  = Math.round(sod / SECONDS_PER_DAY * 1e8);
      if (fod >= 1e8) return toYMDHMS(ymdhms, wsec+1, 0, dp, fmt);  // after rounding, fod >= 1.0
      ymdhms[YMDHMS_FOD] = fod;
    }
    else {
      long fract = Math.round(fsec * mult);
      if (fract >= mult) return toYMDHMS(ymdhms, wsec+1, 0, dp, fmt); // after rounding, fract >= 1.0

      if (fmt==EPOCH) {
        ymdhms[YMDHMS_SOYW] = soyx;
        ymdhms[YMDHMS_SOYF] = fract;
      } else {
        ymdhms[YMDHMS_SECW] = secx;
        ymdhms[YMDHMS_SECF] = fract;
      }
    }
    return ymdhms;
  }

  /** Returns time contents in standard filename format.
      @return The time in standard filename format.
   */
  public String toFileName() {
    return format(wsec,fsec,FILENAME,0);
  }

  /** Rounds a value to the nearest microsecond. This function replaced various
      stand-alone rounding implementations prior to NeXtMidas 3.3.0 when it was
      found that they were not rounding properly (as could be seen when given an
      input value like 0.2000009). Although non-intuitive a computation involving
      "x / 1e6" is NOT the same as a "x * 1e-6" since 1e-6 is not a multiple of
      1/2^n and therefore introduces rounding error into the result. This
      implementation takes care to avoid those issues.
      @param fsec The value to round.
      @return The rounded value, note that this may roll over to the next integer
              value (e.g. 1.0 in the case of 0.9999999).
      @since NeXtMidas 3.3.0
   */
  public static double roundToMicro (double fsec) {
    double fsec6  = fsec * 1e6;
    double fsec6r = Math.round(fsec6);
    return fsec6r / 1e6;
  }

  /** Set from J1950 seconds (rounded to nearest microsecond [1e-6]).
      @param sec The number of seconds since 1 Jan 1950.
   */
  public void fromJ1950 (double sec) {
    // This naive rounding gets into issues when given a value like 0.9999999
    //wsec = Math.floor(sec);
    //fsec = roundToMicro(sec - wsec);
    wsec = Math.floor(sec);
    fsec = 0.0;
    addTime(0.0, roundToMicro(sec - wsec));
  }

  /** Set from J1950 whole and fractional seconds (no rounding).
      If fsec has an integer portion, it will be added to wsec.
      If wsec has a fraction portion, it will be added to fsec.
      @param wsec The number of whole seconds since 1 Jan 1950.
      @param fsec The number of fractional seconds.
   */
  public void fromJ1950 (double wsec, double fsec) {
    double wwsec = Math.floor(wsec); // whole part of wsec
    double fwsec = wsec - wwsec;     // frac  part of wsec
    double wfsec = Math.floor(fsec); // whole part of fsec
    double ffsec = fsec - wfsec;     // frac  part of fsec
    wsec = wwsec+wfsec;              // combined wsec
    fsec = fwsec+ffsec;              // combined fsec
    if (fsec>=1.0) { wsec++; fsec--; }
    this.wsec = wsec;
    this.fsec = fsec;
  }

  /** <b>Internal use only:</b> Set directly from specified J1950 whole and fractional seconds.
     @param wsec The number of whole seconds since 1 Jan 1950 (MUST be normalized, aka no fractional seconds)
     @param fsec The number of fractional seconds. (MUST be normalized, aka no whole seconds)
     @return reference to this Time instance to allows chaining
     @since NeXtMidas 3.3.0
   */
  @InternalUseOnly("Since added")
  Time fromJ1950x (double wsec, double fsec) { // package-private
    assert wsec == Math.floor(wsec) : "wsec ["+wsec+"] MUST not contain any fractional seconds";
    assert fsec == Math.floor(fsec) : "fsec ["+fsec+"] MUST not contain any whole seconds";
    this.wsec = wsec;
    this.fsec = fsec;
    return this;
  }

  /** Get the number of seconds (whole + fractional) since 1 Jan 1950.
      @return The number of seconds since 1 Jan 1950.
   */
  public double toJ1950() {
    return wsec+fsec;
  }

  /** Convert the seconds since J1950 (Midas epoch) to seconds since J1970 (UTC epoch)
      @param sec number of seconds since 1 Jan 1950 (Midas epoch)
      @return The number of seconds since 1 Jan 1970 (UTC epoch)
      @since NeXtMidas 3.3.0
   */
  public static double toJ1970 (double sec) {
    return sec + J1950TOJ1970;
  }

  /** Get the number of seconds (whole + fractional) since 1 Jan 1970 (UTC epoch).
      @return The number of seconds since 1 Jan 1970.
      @since NeXtMidas 3.3.0
   */
  public double toJ1970 () {
    // this order of operations (minus large #, then add small fsec) maximizes the output precision
    return (wsec - J1970TOJ1950) + fsec ;
  }

  /** Set from J1970 (UTC) seconds (rounded to nearest usec).
      @param sec The number of seconds since 1 Jan 1970.
   */
  public void fromJ1970 (double sec) {
    fromJ1950(sec+J1970TOJ1950);
  }

  /** Set from J1970 (UTC) miliseconds.
      @param ms The number of miliseconds since UTC epoch (1 Jan 1970)
      @since NeXtMidas 3.3.0
   */
  public void fromJ1970ms (long ms) {
    double j1970sec = 0.001 * ms;         // convert miliseconds to seconds
//  fromJ1950(j1970sec+J1970TOJ1950);     // <-- this is NOT as efficient as below (since no uSec rounding is needed)
    wsec = (long)j1970sec + J1970TOJ1950; // convert to J1950 whole seconds (truncating fractional seconds)
    fsec = (j1970sec - (long)j1970sec);   // get fractional seconds
  }

  /** Set from J2000 seconds (rounded to nearest usec).
      @param sec The number of seconds since 1 Jan 2000.
   */
  public void fromJ2000 (double sec) {
    fromJ1950(sec+J2000TOJ1950);
  }

  /** Set the time based on the current CPU time. */
  public void fromCurrent() {
    fromJ1950 (current());
  }

  /** Set the time to match the timecode field of a {@link DataFile}.
      @param hcb The data file.
   */
  public void fromTimeCode (DataFile hcb) {
    fromTime(hcb.getTime());
  }

  /** Set the time to match the <code>ACQDATE</code> keyword of a {@link DataFile}.
      @param hcb The data file.
   */
  public void fromAcqDate (DataFile hcb) {
    Object tDate = hcb.getKeywordsObject().get("ACQDATE");
    Object tTime = hcb.getKeywordsObject().get("ACQTIME");
    if (tDate == null || tTime == null) {
      Shell.warning("Unable to set time from ACQDATE + ACQTIME with ACQDATE="+tDate+" and ACQTIME="+tTime);
      return;
    }
    // create the 2 time objects then add them together.
    Time acqDate = new Time(tDate.toString(), ACQDATE);
    Time acqTime = new Time(tTime.toString(), ACQTIME);
    fromTime(acqDate.addTime(acqTime));
  }

  /** Set the time to match the Type5000 Quadwords of a {@link DataFile}.
      @param hcb The data file.
   */
  public void fromQuadwords (DataFile hcb) {
    double year = hcb.getQuadword(DataFile.Q_YEAR);
    double soy  = hcb.getQuadword(DataFile.Q_SEC);
    fromQuadwords(year, soy);
  }

  private void fromQuadwords (double year, double soy) {
    int yr = (int)year;
    wsec = Math.floor(soy);
    fsec = soy-wsec;
    wsec += SECONDS_PER_DAY*ymd2mjd(yr,1,1);
  }

  /** Check GHA in quadwords as a result of Bug 1601. */
  static void checkQuadwords (Midas M, DataFile hcb) { // package-private only used by DataFile
    final double TOL = 0.0001;

    double year = hcb.getQuadword(DataFile.Q_YEAR);
    double soy  = hcb.getQuadword(DataFile.Q_SEC);
    double gha  = hcb.getQuadword(DataFile.Q_GHA);

    if ((year == 0) && (soy == 0)) return; // quadwords not set

    Time t = new Time();
    t.fromQuadwords(year, soy);
    double diff = t.getGHA() - gha;

    if ((diff > TOL) || (diff < -TOL)) {
      M.warning("DataFile: "+hcb.getName()+" appears to have incorrect GHA in quadwords, due to a "
              + "bug in NeXtMidas 2.6.0 and earlier. This can be fixed by running "
              + "'TIMEX "+t+" QUAD="+hcb.getName()+"'.");
    }
  }

  /** Creates a new instance that starts at the given time. This is the same as
      <code>fromString(str,STD)</code>.
      @param str The start time as a string.
   */
  public final void fromString (String str) {
    fromString(str,STD);
  }

  /** Set the time to match that of another {@link Time} instance.
      @param time The time to match.
      @return reference to this Time instance so it can be chained (since NeXtMidas 3.3.0)
   */
  public Time fromTime (Time time) {
    if (time==null) return null; // since 3.9.4
    if (time.wsec >= MAX_INPUT_WSEC)
      Shell.warning("Invalid input from Time of " + time.wsec + ". Max supported input value is " + MAX_INPUT_WSEC); // since 3.1.0 BUG 2126

    wsec = time.wsec;
    fsec = time.fsec;
    return this;
  }

  /** Static method for converting a text rendition of time into a double. This is identical to
      <code>tod2sec(timestring)</code>.
      @param str The time string (using {@link #STD} format).
      @return The current time (seconds-since-1950).
   */
  public static double parseTime (String str) {
    return new Time(str).getSec();
  }

  /** Static method for converting a text rendition of time into a double. This is identical to
      <code>tod2sec(timestring)</code>.
      @param str The time string (using {@link #STD} format).
      @param fmt The time format.
      @return The current time (seconds-since-1950).
      @since NeXtMidas 2.5.0
   */
  public static double parseTime (String str, int fmt) {
    return new Time(str,fmt).getSec();
  }

  /** Static method for converting a text rendition of time into a Time.

      @param fmt The time format.
      @param str The time string
      @return The current time (seconds-since-1950).
      @since NeXtMidas 3.5.4 (moved from Timex)
 */
  private static Time parseTime (MFormat fmt, String str) {
    try {
      Time t = Time.toTime(fmt.parseObject(str));

      if (t == null) {
        throw new MidasException("Unable to parse '"+str+"' using "+fmt+".");
      }
      return t;

    } catch (ParseException e) {
      throw new MidasException("Unable to parse '"+str+"': "+e.getMessage(), e);
    }
  }

  /** Set the timecode field of a {@link DataFile} to match this time.
      @param hcb The data file.
   */
  public void toTimeCode (DataFile hcb) {
    hcb.setTime(wsec,fsec);
  }

  /** Set the <code>ACQDATE</code> keyword of a {@link DataFile} to match this time.
      @param hcb The data file.
   */
  public void toAcqDate (DataFile hcb) {
    hcb.getKeywordsObject().put("ACQDATE", toString(ACQDATE,0));
    hcb.getKeywordsObject().put("ACQTIME", toString(ACQTIME,0));
  }

  /** Converts an arbitrary time object to a Time object. This method is
      identical to: <code>toTime(obj,STD)</code>.
      @param obj The object to convert to a time object.
      @return The Time object.
   */
  public static Time toTime (Object obj) {
    return toTime(obj,STD);
  }

  /** Converts an arbitrary time object to a Time object. This method takes in the following
      time definitions:
      <pre>
        null               - Returns null.
        {@link Time}               - Just returns the instance passed in.
        {@link Date}               - Converts to Midas time using {@link #fromDateObject(Date)}.
        {@link Data}               - If the object is a Scalar (e.g. "SD" or "1D") it is assumed to be
                             the number of seconds since 01-JAN-1950. If it is Complex (e.g. "CD"
                             or "2D") it is assumed to be "{@code (<wsec>,<fsec>)}" similar to
                             {@link #getWSec} and {@link #getFSec}. ASCII formats are all converted to
                             Strings and handled separately. Other data formats are not supported.
        {@link Number}            - Converts Number object to double and creates a new time instance.
        "{@code  <sec>}"          - Converts to a number (sec since 01-JAN-1950). <i>[see notes]</i>
        "{@code (<sec>)}"         - Converts to a number (sec since 01-JAN-1950).
        "{@code (<wsec>,<fsec>)}" - Converts to a number (whole+fractional sec since 01-JAN-1950).
        "{@code <time string>}"   - Converts object to a String and parses the string.

        Notes:
          1. When the fmt={@link #ISO8601} the "{@code <sec>}" is explicitly skipped over
             in favor of parsing the string as an ISO 8601 time. This is done
             because values like "20121107" (2012-11-07) and "123456" (12:34:56)
             are valid ISO 8601 date/time strings and do not represent a number of
             J1950 seconds. Note that this exception does not apply to situations
             where the number is surrounded by parenthesis (e.g. "{@code (<sec>)}").
          2. Prior to NeXtMidas 2.7.0, all Data and String formats had to be scalars
             without parenthesis.
      </pre>
      Note that the legacy X-Midas <code>M$TPICK</code> format "WSEC|FSEC" is *not* supported here. It
      is only supported in the {@link Args#getTime(String,Time)} method and produces deprecation
      warnings when used. All NeXtMidas applications should use the "{@code (<wsec>,<fsec>)}"
      syntax when specifying whole and fractional seconds separately. Also note that the X-Midas
      "T:" syntax is *not* supported since it conflicts with NeXtMidas tables.
      @param obj The object to convert to a time object.
      @param fmt The format to use for string conversions (e.g. {@link #STD}).
      @return The Time object.
      @since NeXtMidas 3.3.0
   */
  public static Time toTime (Object obj, int fmt) {
    if (obj == null          )                      return null;
    if (obj instanceof Time  )                      return (Time)obj;
    if (obj instanceof Calendar  )                  return new Time(fromCalendar((Calendar)obj));
    if (obj instanceof XMLGregorianCalendar  )      return new Time(fromXMLGregorianCalendar((XMLGregorianCalendar)obj));
    if (obj instanceof Date  )                      return new Time(fromDateObject((Date)obj));
    if (obj instanceof Number)                      return new Time(((Number)obj).doubleValue());

    // ASCII DATA OBJECTS (convert to String -- must be before numeric string check)
    if ((obj instanceof Data) && ((Data)obj).isString()) { // 256 ASCII 'Z' is unlikely here, but still handling NXM-3570 3.9.4
      obj = obj.toString();
    }

    // TRY CONVERTING NUMERIC STRINGS TO DATA OBJECTS
    if (!(obj instanceof Data)) {
      String str = Convert.o2s(obj);
      if ((fmt != ISO8601) && StringUtil.isNumber(str)) {
        return new Time(Convert.s2d(str)); // basic numeric string
      }
      if (str.startsWith("(") && str.endsWith(")")) {
        // Try to convert "(<sec>)" and "(<wsec>,<fsec>)" to Data (handled below)
        try { obj = Convert.o2o(str, "CD", null); } catch (Exception e) { /* ignore */ }
      }
    }

    // DATA OBJECTS
    if (obj instanceof Data) {
      Data data = (Data)obj;
      if ((data.mode == 'S') || (data.mode == '1')) {
        return new Time(data.toD());
      }
      else if ((data.mode == 'C') || (data.mode == '2')) {
        double[] vals = new double[2];
        data.toArray(vals);
        return new Time(vals[0],vals[1]);
      }
      else {
        // this part is for backwards-compatibility with 2.6.0
        Shell.deprecate("Since NeXtMidas 2.7.0: Conversion of data format "+data.getFormat()+" to Time is deprecated.");
        return new Time(Convert.o2d(obj));
      }
    }

    // ASSUME A TIME STRING
    return new Time(obj.toString(), fmt);
  }

  /** Converts from Midas time to a Java {@link Date} object.
      <br><br>Note: to convert {@code Date -> Time}, use <code>toTime(...)</code> method<br>
      @return The {@link Date} object in ms since 1970.
      @since NeXtMidas 2.5.0
   */
  public Date toDateObject () {
    return toDateObject(wsec+fsec);
  }

  /** Converts from Midas time to a Java {@link Date} object, truncating times less than 1 millisecond.
      @param sec Midas time (sec since 1970).
      @return The {@link Date} object in ms since 1970.
      @since NeXtMidas 2.5.0
   */
  public static Date toDateObject (double sec) {
    long ms = (long)(sec * 1000) - (J1970TOJ1950 * 1000L);  // Since 3.5.2 - always truncate
    return new Date(ms);
  }

  /** Converts from a Java {@link Date} object to Midas time.
      <br><br>Note: to convert {@code Date -> Time}, use <code>toTime(...)</code> method<br>

      @param date The {@link Date} object in ms since 1970.
      @return Midas time (sec since 1970).
      @since NeXtMidas 2.5.0
   */
  public static double fromDateObject (Date date) {
    return (date.getTime() / 1000.0) + J1970TOJ1950;
  }

  /** Converts this time object to a {@link Calendar} object.
   * @return The {@link Calendar} object. The implementation will always return an instance of
   * {@link GregorianCalendar} as {@link Calendar} is abstract.
   * @since NeXtMidas 3.5.0
   */
  public GregorianCalendar toCalendar () {
    GregorianCalendar cal = new GregorianCalendar(UTC_TIMEZONE);
    cal.setTime(this.toDateObject());
    return cal;
  }

  /** Converts a {@link Calendar} object to a Time object.
   *  @param cal The Calendar object.
   *  @return The new Time object.
   *  @since NeXtMidas 3.5.0
   */
  public static Time fromCalendar (Calendar cal) {
    return new Time(Time.fromDateObject(cal.getTime()));
  }

    /** Converts this time object to a {@link XMLGregorianCalendar} object.
   * @return The {@link XMLGregorianCalendar} object.
   * @since NeXtMidas 3.5.0
   */
  public XMLGregorianCalendar toXMLGregorianCalendar () {
    if (DATATYPE_FACTORY == null) {
      //Create a new factory only on first use.
      try {
        DATATYPE_FACTORY = DatatypeFactory.newInstance();
      }
      catch (DatatypeConfigurationException e) {
        throw new RuntimeException("Could not create new DatatypeFactory", e);
      }
    }
    GregorianCalendar cal = toCalendar();
    return DATATYPE_FACTORY.newXMLGregorianCalendar(cal);
  }

  /** Converts a {@link XMLGregorianCalendar} object to a Time object.
   *  @param cal The XMLGregorianCalendar object.
   *  @return The new Time object.
   *  @since NeXtMidas 3.5.0
   */
  public static Time fromXMLGregorianCalendar (XMLGregorianCalendar cal) {
    return fromCalendar(cal.toGregorianCalendar());
  }

  /** Set the Type5000 Quadwords of a {@link DataFile} to match this time.
      @param hcb The data file.
   */
  public void toQuadwords (DataFile hcb) {
    hcb.setQuadword(DataFile.Q_YEAR, getYear());
    hcb.setQuadword(DataFile.Q_SEC,  getSoY());
    hcb.setQuadword(DataFile.Q_GHA,  getGHA());
  }

  /** Get Midas Julian Day (since 1950).
      @return The number of days since 1 Jan 1950.
   */
  public int getMJD() { return (int)(wsec/SECONDS_PER_DAY); }

  /** Get the year.
      @return The year.
   */
  public int getYear() { return getYear(wsec); }

  /** Converts whole seconds to year. */
  private static int getYear (double wsec) {
    // This equation taken from X-Midas's utilities.for.
    int days = (int)(wsec/SECONDS_PER_DAY);
    return (int)((days+712237.5)/365.25 + .002);
  }

  /** Gets the month of the year.
      @return The month (1..12)
      @since NeXtMidas 2.5.0
      @see #getMonthName()
   */
  public int getMonth () {
    int mon = getMonthIndex() + 1;
    return (mon <= 12)? mon: mon-12;
  }

  /** Gets the name of the month.
      @return The month (see {@link #monthNameList})
      @since NeXtMidas 2.5.0
      @see #getMonth()
   */
  public final String getMonthName () {
    return Parser.get(monthNameList, getMonth());
  }

  /** Gets the name of the month.
      @return The month (see {@link #monthList})
      @since NeXtMidas 2.5.0
      @see #getMonth()
   */
  public final String getMonthTrigraph () {
    return Parser.get(monthList, getMonth());
  }

  /** Gets the day of the month.
      @return The day (1..28/29/30/31)
      @since NeXtMidas 2.5.0
   */
  public int getDay() {
    return getDoY() - dim[getMonthIndex()] + 1;
  }

  /** Gets the day of the week. This follows the {@link java.util.Calendar} class in numbering
      the days 1..7 starting with Sunday.
      @return The day of the week (1=SUN,2=MON,...,7=SAT).
      @since NeXtMidas 2.5.0
   */
  public int getDayOfWeek() {
    int dow = ((getMJD()+1) % 7);
    return (dow == 0)? 7 : dow;
  }

  /** Gets the day of the week. This follows the {@link java.util.Calendar} class in numbering
      the days 1..7 starting with Sunday.
      @return The day of the week (1=SUN,2=MON,...,7=SAT).
      @since NeXtMidas 3.3.0
   */
  private static int getDayOfWeek (int year, int mon, int day) {
    int dow = ((ymd2mjd(year,mon,day)+1) % 7);
    return (dow == 0)? 7 : dow;
  }

  /** Gets the day of the week.
      @return The day of the week (see {@link #dayNameList})
      @since NeXtMidas 2.5.0
   */
  public String getDayOfWeekName() {
    return Parser.get(dayNameList, getDayOfWeek());
  }

  /** Gets the day of the week.
      @return The day of the week (see {@link #dayList})
      @since NeXtMidas 2.5.0
   */
  public String getDayOfWeekTrigraph() {
    return Parser.get(dayList, getDayOfWeek());
  }

  /** Index into the dim (day-in-month) array. */
  private int getMonthIndex() {
    int doy = getDoY();
    int mon = (isLeapYear())? 23: 11;
    while (dim[mon] > doy) mon--;
    return mon;
  }

  /** Gets the hour of the day.
      @return The hour (0..23)
      @since NeXtMidas 2.5.0
   */
  public int getHour() { return (int)(getSoD() / 3600); }

  /** Gets the minute of the hour.
      @return The hour (0..59)
      @since NeXtMidas 2.5.0
   */
  public int getMinute() { return (int)((getSoD() % 3600) / 60); }

  /** Gets the second of the minute. <b>This is not in MJS, see {@link #getSec()}!</b>
      @return The second (0..59)
      @since NeXtMidas 2.5.0
   */
  public int getSecond() { return (int)(getSoD() % 60); }

  /** Indicates if the given year is a leap year. The code for this method was copied from RFC 3339.
      <pre>
        Source: Klyne, et.al. "RFC 3339 / Date and Time on the Internet: Timestamps."
                ITEF, July 2002. http://tools.ietf.org/html/rfc3339 (Retrieved 2007-06-15)
      </pre>
      @param year The year number.
      @return true if it is a leap year, false otherwise.
      @since NeXtMidas 2.5.0
   */
  public static boolean isLeapYear(int year) {
    return (year % 4 == 0 && (year % 100 != 0 || year % 400 == 0));
  }

  /** Indicates if the current year is a leap year.
      @return true if it is a leap year, false otherwise.
      @see #isLeapYear(int)
      @since NeXtMidas 2.5.0
   */
  public boolean isLeapYear() { return isLeapYear(getYear()); }

  /** Gets the current year. */
  private static int getCurrentYear () {
    double wsec = Math.floor(Time.current());
    return getYear(wsec);
  }

  /** Get the Day of Year (zero based).
      @return The day of year (where 0 = 1 Jan).
   */
  public int getDoY() { return (int)(getSoY()/SECONDS_PER_DAY); }

  /** Get Midas Julian Seconds (since 1950).
      @return The number of seconds since 1 Jan 1950.
   */
  public double getMJS() { return toJ1950(); }

  /** Get the Seconds of Year.
      (Note: Since 3.5.1, truncation (to 8 decimal places) is used rather than rounding so the
      different public get methods (getDoY, getSoD, etc.) are consistent  with each other.)
      @return The seconds since the start of the year.
   */
  public double getSoY() {
    double wsecInYear = wsec-SECONDS_PER_DAY*(double)ymd2mjd(getYear(),1,1);
    // truncate fractional seconds so they will fit without an overall round.
    // a double is guaranteed to have 15 digits of base 10 precision and a maximum of 8 of them are
    // used for whole seconds of the day, so that leaves a guaranteed 7 decimal places.
    // The maximum guaranteed decimal places was calculated as follows:
    //   max seconds in year (31622399.9999999) is approx 2^(24.9)
    //   for 2^n to 2^(n+1) the guaranteed decimal places is 2^(n-52) base 2
    //   2^(24-52) = 2^(-18) =~ 3.7e-9, so 8 base 10 decimal places are guaranteed
    double truncFSec = ((long)(fsec*1e8))/1e8; // for BUG 2849
    return (wsecInYear + truncFSec);
  }

  /** Get the Seconds of Day.
      (Note: Since 3.5.1, truncation (to 10 decimal places) is used rather than rounding so the
      different public get methods (getHour, getFoD, etc.) are consistent with each other.)
      @return The seconds since the start of the day.
   */
  public double getSoD() {
    // keep precision & also not have rounding when wsec of day are added to fsec. BUG 2849
    // a double has a guaranteed accuracy of 15 base 10 digits and a maximum of 5 of them
    // are used for whole seconds of the day, so that leaves a guaranteed 10 decimal places
    return ((wsec %  SECONDS_PER_DAY) + ((long)(fsec*1e10))/1e10);
  }

  /** Get the Fraction of Day (0 to 1).
      @return The percentage of the day (0.0 = 00:00, 0.5 = 12:00, 1.0 = 24:00).
   */
  public double getFoD() { return getSoD()/SECONDS_PER_DAY; }

  /** Get the Greenwich Hour Angle (in radians).
      @return The Greenwich Hour Angle (in radians).
   */
  public double getGHA() { return RAGW(getSec()/SECONDS_PER_DAY); }

  /** Get the Julian Day (since 1950).
      @return The Julian Day.
   */
  public double getJD() { return (double)getMJD()+MJDTOJD; }

  /** Get Year in seconds.
      @return The number of seconds 1 Jan 1950 and 1 Jan of the year for this time.
   */
  public double getYiS() { return SECONDS_PER_DAY*(double)ymd2mjd(getYear(),1,1); }

  /** Get whole seconds since 1 Jan 1950.
      @return The whole seconds since 1 Jan 1950.
      @see #getFSec()
      @see #getSec()
   */
  public double getWSec() { return wsec; }

  /** Get fractional seconds. These are the number of seconds that when added to {@link #getWSec()}
      is equal to the number of seconds (whole+fractional) since 1 Jan 1950.
      @return The fractional seconds.
      @see #getWSec()
      @see #getSec()
   */
  public double getFSec() { return fsec; }

  /** Get seconds since 1 Jan 1950.
      This is the same as <code>getWSec()+getFSec()</code> and may result in a loss of precision.
      @return The whole seconds since 1 Jan 1950.
      @see #getWSec()
      @see #getFSec()
   */
  public double getSec() { return wsec+fsec; }

  /** Truncate to desired resolution in seconds.
      @param resolution The resolution.
   */
  public void floor (double resolution) {
    wsec = resolution*Math.floor(wsec/resolution);
    // 2011-01-24 added below to truncate fsec for BUG 2321
    if (resolution>1) { fsec = 0; }
    else              { fsec = resolution*Math.floor(fsec/resolution); }
  }

  /** Modulo of the desired resolution in seconds.
      @param resolution The resolution.
   */
  public void modulo (double resolution) {
    wsec -= resolution*Math.floor(wsec/resolution);
  }

  /** Round to desired resolution in seconds.
      @param resolution The resolution.
   */
  public void round (double resolution) {
    double sec = wsec+fsec;
    sec = resolution*Math.round(sec/resolution);
    fromJ1950 (sec);
  }

  /** Calculate difference from another {@link Time} instance in seconds.
      E.g. to convert to seconds in a different time epoch.
      <pre>
        Time epoch = Time.toTime("1994:01:01::00:00:00");
        double secondsSinceJ1994 = time.diff(epoch);
      </pre>
      @param time The time to diff against.
      @return The time difference (effectively <code>this - time</code>).
   */
  public double diff (Time time) {
    double diff = (wsec - time.wsec);
    return diff + (fsec - time.fsec);
  }

  /** Add an amount of time (in seconds) to this time instance.
      @param sec The number of seconds to add.
      @return reference to this Time instance so it can be chained (since NeXtMidas 3.3.0)
   */
  public Time addSec (double sec) {
    double _wsec = Math.rint(sec);  // uses rint(..) not floor(..) to support sec<0
    double _fsec = sec - _wsec;
    return addTime(_wsec, _fsec);
  }

  /** Add an amount of time to this time instance.
      @param t The amount of time to add.
      @return reference to this Time instance so it can be chained.
      @since NeXtMidas 3.3.0
   */
  public Time addTime (Time t) {
    return addTime(t.wsec, t.fsec);
  }

  /** Internal use only: Requires that wsec and fsec be normalized first, otherwise
      a loss of precision can result (see Bug 2674).
   */
  private Time addTime (double _wsec, double _fsec) {
    wsec += _wsec;
    fsec += _fsec;

    if (fsec<0 || fsec>=1) {
      double sec = Math.floor(fsec);
      wsec += sec;
      fsec -= sec;
    }
    return this;
  }

  /** Puts the current thread to sleep for a specified number of seconds. This method guarantees
      that the thread will sleep for (at least) the given number of seconds. In practice the thread
      (due to unavoidable system overhead) tends to sleep for approximately 9ms more than requested
      (though this value is entirely platform-dependent). For this reason, the following code:
      <pre>
        pause(10.0);
      </pre>
      which (in practice) waits for ~10.09 seconds, is ten times more accurate than the following
      code which (in practice) waits for ~10.90 seconds:
      <pre>
        for (int i = 0; i &lt; 10; i++) {
          pause(1.0);
        }
      </pre>
      @param sec The number of seconds for current thread to sleep for (any value less than 1ms is
                 ignored).
      @see #sleep(double)
      @since NeXtMidas 2.1.0
   */
  public static void pause (double sec) {
    final long start = System.currentTimeMillis(); // in ms
    final long end   = start + (long)(sec*1000);   // in ms

    long now = start;
    while(now < end) {
      try {
        Thread.sleep(end - now);
      } catch(InterruptedException e) {
        /* IGNORE */
      }
      now = System.currentTimeMillis();
    }
    //System.out.println("requested wait " + ((long)sec*1000) + "ms actual " + (now-start) + "ms");
  }

  /** Puts the current thread to sleep for a specified number of seconds. This method will return
      immediately (without an {@link InterruptedException}) if the thread is interrupted during the
      sleep period. Due to this behavior, this method does not guarantee that the thread will sleep
      for the given number of seconds, only that it will try to. <br>
      <br>
      Unlike this method, the {@link #pause(double)} method will NOT return immediately if the thread
      is interrupted. The {@link #pause(double)} method waits for the entire duration specified.
      @param seconds The number of seconds for current thread to (try to) sleep.
      @see #pause(double)
   */
  public static void sleep (double seconds) {
    try {
      int msec = (int)(seconds * 1000.0);
      if (msec>10) Thread.sleep(msec);
      else Thread.sleep(msec,(int)((seconds*1e3-msec)*1e6));
    }
    catch (InterruptedException e) { }
  }

  /** Puts the current thread to sleep for a specified number of seconds. This method will return
      immediately (without an {@link InterruptedException}) if the thread is interrupted during the
      sleep period. Due to this behavior, this method does not guarantee that the thread will sleep
      for the given number of seconds, only that it will try to.
      @param usec The number of microseconds for current thread to (try to) sleep.
  */
  public static void usleep (int usec) {
    int msec = usec/1000;
    int nsec = (usec-msec*1000) * 1000;
    try { Thread.sleep(msec,nsec); }
    catch (InterruptedException e) { }
  }

  /** Get the current time in J1950 seconds.
      @return The current CPU time in seconds since 1 Jan 1950.
   */
  public static double current () {
    double time = 0.001*(double)System.currentTimeMillis();
    return time+J1970TOJ1950;
  }

  /** Get the current time.
      @return The current time in a new instance
   */
  public static Time currentTime () {
    return new Time(current());
  }

  /** Formats the time into the last known default format (each format request changes the default format).
      @param time time to be formatted using currently active format
      @return string containing the formatted time
  */
  public static String toString (double time) {
    return format(time,STD,0);
  }

  /** Formats the time into the requested format.
      @param time The time to be formatted (in seconds since 1 Jan 1950).
      @param fmt  The time format to use.
      @return The string containing the formatted time
  */
  public static String toString (double time, int fmt) {
    return format(time,fmt,-1);
  }

  /** Formats the time into the requested format and precision.
      @param time The time to be formatted.
      @param fmt  The time format to use.
      @param dp   The precision (decimal places) to use.
      @return The formatted time string, or {@link #STD} format if invalid format is given.
  */
  public static String toString (double time, int fmt, int dp) {
    // return format(time,0.0,fmt,dp);  // JWB: Changed to line below for BUG 2308 (in 2.9.1)
    return format(time,fmt,dp);
  }

  /** Formats the time into the requested format and precision.
      @param time   The time to be formatted.
      @param format The time format to use in String form.
      @param dp     The precision (decimal places) to use.
      @return The formatted time string, or {@link #STD} format if invalid format is given.
  */
  public static String toString (double time, String format, int dp) {
    int fmt = Parser.find(formatList,format,-1,0,Parser.E_WARNING);  // since 2.9.0 warning added for Bug 760
    //return format(time,0.0,fmt,dp);  // JWB: Changed to line below for BUG 2308 (in 2.9.1)
    return format(time,fmt,dp);
  }

  /** Returns the current time as a {@link String} for fast message tagging.
      @return The current time in message tag format.
   */
  public static String tag() {
    double tag = current();
    if (tag-lastTag>.1) setTag (tag);
    return strTag;
  }

  /** Updates the last tag.
      @param tag The tag value.
   */
  public static synchronized void setTag (double tag) {
    strTag = Time.format (tag,STD,1);
    lastTag = tag;
  }

  /** Gets the precision multiplier.
      @param dp The display precision.
      @return The multiplier (<code>10^dp</code>).
   */
  private static double getPrecisionMult (int dp) {
    switch (dp) {
      case  3: return 1e3;  // most common (millisecond)
      case  6: return 1e6;  // very common (microsecond)
      case  9: return 1e9;  // common (nanosecond)
      case 12: return 1e12; // common (picosecond)
      case  0: return 1e0;
      case  1: return 1e1;
      case  2: return 1e2;
      case  4: return 1e4;
      case  5: return 1e5;
      case  7: return 1e7;
      case  8: return 1e8;
      case 10: return 1e10;
      case 11: return 1e11;
      case 15: return 1e15; // since 3.5.1 femtosecond
      default: return Math.pow(10.0,dp);
    }
  }

  /** Get number of decimal places (precision) that the specified seconds
      should be accurate to (6 for microseconds to 12 for picoseconds).<br>
      Seconds since J1950 have precision up to 6 decimal places (microseconds).<br>
      Seconds since start of year  have precision up to  8 decimal places.<br>
      Seconds since start of month have precision up to  9 decimal places.<br>
      Seconds since start of week  have precision up to 10 decimal places.<br>
      Seconds since start of day   have precision up to 10 decimal places.<br>
      Seconds since start of hour  have precision up to 12 decimal places (picoseconds).
      @param secs seconds since J1950
      @return number of decimal places needed to display the fractional seconds precisely.
      @since NeXtMidas 2.8.0
   */
  static int getDecimalPrecision (double secs) {
    if (secs > SECONDS_PER_LEAP_YEAR) return  6;            // most common case for absolute time since J1950 (microsecond)
    if (secs == 0)                    return defPrecision;
    if (secs <= SECONDS_PER_HOUR)     return MAX_PRECISION; // max precision is down to picosecond
    if (secs > SECONDS_PER_MONTH)     return  8;            // for soy
    if (secs > SECONDS_PER_WEEK)      return  9;            // for som
    if (secs > SECONDS_PER_HOUR)      return 10;            // for sow
    return defPrecision;  // default to (should not reach here in most cases)
  }

  /** Returns the time in standard format.
      @param time The time in seconds since 1 Jan 1950.
      @return The time formatted in standard (STD) format.
      @see #format(double, int, int)
  */
  public static String format (double time) {
    return Time.format(time,STD,0);
  }

  /** Returns the time in the specified format.
      @param time   The time in seconds since 1 Jan 1950.
      @param format The time format to use.
      @return The string containing formatted time
      @see #format(double, int, int)
   */
  public static String format (double time, int format) {
    return Time.format(time,format,-1);
  }

  /** Returns the time (rounded to microseconds) in the specified format and
      precision. Since NeXtMidas 3.3.0 this is identical to using
      <code>(new Time(time)).toString(fmt,dp)</code> which ensures that the rounding
      to microsecond precision is handled properly.
      @param time The time in seconds since 1 Jan 1950.
      @param fmt  The time format to use.
      @param dp   The display precision to use when formatting the string.
      @return The string containing formatted time.
   */
  public static String format (double time, int fmt, int dp) {
//return (new Time(time)).toString(fmt,dp);
    double sec = time+5e-7;
    double wsec = Math.floor(sec);
    double fsec = 1e-6*Math.floor(1e6*(sec-wsec));
    return toStringBuffer(null,wsec,fsec,fmt,dp).toString();
  }

  /** Return the time as a formatted string (if an invalid format is requested,
      the time will be returned in {@link #STD} format).
      @param wsec The whole seconds since 1 Jan 1950.
      @param fsec The fraction seconds.
      @param fmt  The time format to use.
      @param dp   The display precision to use when formatting the string.
      @return The formatted time string.
   */
  public static String format (double wsec, double fsec, int fmt, int dp) {
    return toStringBuffer(null,wsec,fsec,fmt,dp).toString();
  }

  /** Appends the time as a formatted string (if an invalid format is requested, the time will be
      returned in {@link #STD} format) to a given {@link StringBuffer}.
      @param wsec The whole seconds since 1 Jan 1950.
      @param fsec The fraction seconds.
      @param sbuf The string buffer to append the time to.
      @param fmt  The time format to use ({@link #STD}, {@link #ACQ}, {@link #EPOCH},
                  {@link #NORAD}, {@link #TCR}, {@link #VAX}, {@link #FILENAME}, {@link #HMS}).
      @param dp   The display precision to use when formatting the string.
      @return The length of the string buffer.
   */
  public static int format (double wsec, double fsec, StringBuffer sbuf, int fmt, int dp) {
    return toStringBuffer(sbuf,wsec,fsec,fmt,dp).length();
  }

  /** Given a time format, return the text string associated with that format type.
      @param fmt The code for the time format.
      @return String containing the name of the format.
   */
  public static String getFormat (int fmt) {
    return Parser.get(formatList,fmt);
  }

  /** Static method for converting a text rendition of time-of-day into a double.
      @param str The time string (using {@link #STD} format).
      @return The current time (seconds-since-1950).
   */
  public static double tod2sec (String str) {
    return new Time(str).getSec();
  }

  /** Static method for converting a text rendition of time-of-day into a double.
      @param buffer The buffer containing the time string (using {@link #STD} format).
      @param i1     Start index into the buffer.
      @param i2     End index into the buffer.
      @return The current time (seconds-since-1950).
   */
  public static double tod2sec (byte[] buffer,int i1,int i2) {
    return tod2sec(Convert.unpackS(buffer,i1,i2-i1));
  }

  /** Static method for converting a time string into a {@link Time} object.
      @param str The time string (using {@link #STD} format).
      @return The current time.
   */
  public static Time parse (String str) {
    return new Time(str);
  }

  /** Static method for converting a time string into a {@link Time} object.
      @param buffer The buffer containing the time string (using {@link #STD} format).
      @param i1     Start index into the buffer.
      @param i2     End index into the buffer.
      @return The current time.
   */
  public static Time parse (byte[] buffer,int i1,int i2) {
    return parse(Convert.unpackS(buffer,i1,i2-i1));
  }

  /** Sets the value based on a time string.
      @param buffer The buffer containing the time string (using {@link #STD} format).
      @param i1     Start index into the buffer.
      @param i2     End index into the buffer.
   */
  public void fromBytes (byte[] buffer,int i1,int i2) {
    fromString(Convert.unpackS(buffer,i1,i2-i1),STD);
  }

  /** Parses an ISO 8601 time zone specifier (other than 'Z'), returning seconds. */
  private static double parseTimeZone (String str) {
    if (str.charAt(0) == '-') {
      switch (str.length()) {
        case 3: return -(Integer.parseInt(str.substring( 1 )) * 3600);  // '+hh'
        case 5: return -(Integer.parseInt(str.substring(1,3)) * 3600    // '+hhmm'
                        +Integer.parseInt(str.substring( 3 )) *   60);
        case 6: return -(Integer.parseInt(str.substring(1,3)) * 3600    // '+hh:mm'
                        +Integer.parseInt(str.substring( 4 )) *   60);
      }
    }
    else if (str.charAt(0) == '+') {
      switch (str.length()) {
        case 3: return +(Integer.parseInt(str.substring( 1 )) * 3600);  // '+hh'
        case 5: return +(Integer.parseInt(str.substring(1,3)) * 3600    // '+hhmm'
                        +Integer.parseInt(str.substring( 3 )) *   60);
        case 6: return +(Integer.parseInt(str.substring(1,3)) * 3600    // '+hh:mm'
                        +Integer.parseInt(str.substring( 4 )) *   60);
      }
    }
    throw new MidasException("Invalid timezone specifier '"+str+"'");
  }

  /** Parses an ISO 8601 'yyyy-Www-d' or 'yyyyWwwd' specifier, returns day-of-year. */
  private static int parseYearWeekDay (int year, String str) {
    int week;  // week number 1..N
    int dow;   // day of week (1=MON, ... 7=SUN)

    switch (str.length()) {
      case 7: // 'yyyyWww'
        week = Integer.parseInt(str.substring( 5 ));
        dow  = 1;
        break;
      case 8: // 'yyyyWwwd' or 'yyyy-Www'
        if (str.charAt(4) == 'W') {
          week = Integer.parseInt(str.substring(5,7));
          dow  = Integer.parseInt(str.substring( 7 ));
        }
        else {
          week = Integer.parseInt(str.substring( 6 ));
          dow  = 1;
        }
        break;
      case 10: // 'yyyy-Www-d'
        week = Integer.parseInt(str.substring(6,8));
        dow  = Integer.parseInt(str.substring( 9 ));
        break;
      default:
        throw new MidasException("Invalid date specifier '"+str+"'");
    }

    int correction;
    switch (getDayOfWeek(year,1,1)) {
      case 1: correction =  -6; break; // SUN
      case 2: correction =  -7; break; // MON
      case 3: correction =  -8; break; // TUE
      case 4: correction =  -9; break; // WED
      case 5: correction = -10; break; // THR
      case 6: correction =  -4; break; // FRI
      case 7: correction =  -5; break; // SAT
      default: throw new AssertionError("Invalid day of week");
    }

    return week*7 + dow + correction;
  }

  /** Parses a Time from a time string based on the specified Time/simpleDate format

      Supports format from the Time.formatList, {@link java.text.SimpleDateFormat},
      and extended precision SimpleDateFomats (explained below)

      Extended precision SimpleDateFormat interprets
      SSSS to SSSSSSSSSSSSSSS (femtoseconds) as digits of fractional seconds
      - S to SSS are still processed as standard simpleDateFormats
      - fractional seconds are truncated to the number of digits specified
        (or a maximum of 15 for femtosecond precision)

      Note: Formats in the Time.formatList will round and SimpleDateFormats will truncate
      (The truncation is important for the enhanced simpleDateFormat case, where fractional
      seconds and the rest of the Date are handles separately.)

      @param str           The start time as a string.
      @param simpleDateFmt Time format specified as SimpleDateFormat
      @see #fromString(String,String)
      @since NeXtMidas 3.5.4 (moved logic here from Timex.getInputTime)
   */
  public void fromString (String str, String simpleDateFmt) {
    MFormat inFormat = null;

    wsec = 0.0;
    fsec = 0.0;
    double strippedFSec = 0;
    if (str == null) return;

    if (str.startsWith("\"") && str.endsWith("\"")) { // Remove quotes from a quoted string.
      str = str.substring(1,str.length()-1);
    }

    // Trim Time String (accept Strings with beginning and trailing spaces)
    str = str.trim();

    if (simpleDateFmt != null) {
      int indexHighPrecFSec = simpleDateFmt.indexOf("SSSS");
      if (indexHighPrecFSec >= 0)        {
        // get the number of fractional second digits
        int indexBeyondFSec = getIndexBeyondFSec (simpleDateFmt, indexHighPrecFSec);
        int numSSSS = indexBeyondFSec - indexHighPrecFSec;
        int strStartSSSS = indexHighPrecFSec;
        int strBeyondSSSS = indexBeyondFSec;
        // check if SSSS... is at end. If so, back up from end
        if (indexBeyondFSec == simpleDateFmt.length()) {
          strBeyondSSSS = str.length();
          strStartSSSS = strBeyondSSSS - numSSSS;
        }
        else {
          int firstDot = str.indexOf('.');
          if ((firstDot >= indexHighPrecFSec) && (firstDot == str.lastIndexOf('.'))) {
            // probably where the fractional seconds are in str
            strStartSSSS = firstDot + 1;
            strBeyondSSSS = strStartSSSS + numSSSS;
          }
        }
        String fSecSubstring = str.substring(strStartSSSS, strBeyondSSSS);
        if (fSecSubstring.matches("[0-9]+")) {
          strippedFSec = Double.valueOf("0." + fSecSubstring);
          // get rid of the "SSS...S" and the leading "." if there
          simpleDateFmt = simpleDateFmt.substring(0, indexHighPrecFSec) + simpleDateFmt.substring(indexBeyondFSec);
          str = str.substring(0, strStartSSSS) + str.substring(strBeyondSSSS);
        }
        else {
          throw new IllegalArgumentException("input string "+str
              + " and/or SimpleDateFormat:"+simpleDateFmt+" not supported");
        }
      }
      inFormat =MFormat.getDateFormatFor(simpleDateFmt);
    }

    // Use inFormat to parse Time
    if (inFormat==null)                { fromString(str,STD); }
    else                               {
      fromTime(parseTime(inFormat, str));
      addSec(strippedFSec);
    }
  }

  /** Get the index of the first character beyond the "SSS...S" in the format String
      @param inputFormat expanded simpleDateFormat with "SSSS" as part of format
      @param indexOfFracSec valid index of "SSSS" in format String
      @return index of first character beyond "SSS.S" in format String
      @since NeXtMidas 3.5.4 (moved over from Timex)
   */
  private int getIndexBeyondFSec (String inputFormat, int indexOfFracSec){
    int nextCharCheck;
    int formatStringLength = inputFormat.length();

    // get # digits fractional seconds
    nextCharCheck = indexOfFracSec + 4; // add length of SSSS to starting index
    // determine how many digits of fractional seconds are specified
    while ((nextCharCheck < formatStringLength) && (inputFormat.charAt(nextCharCheck) == 'S')){
      nextCharCheck++;
    }
    return nextCharCheck; // location after SSS...S
  }

  /** Parses a number from a time string.
      Since 3.3.0, if time format is {@link #STD} but input time string looks to be in ISO 8601 format,
      then format is switched to {@link #ISO8601}.
      @param str The time string.
      @param fmt The time format (see {@link #formatList}).
      @since NeXtMidas 2.5.0
   */
  @SuppressWarnings("fallthrough")
  public void fromString (String str, int fmt) {
    // This method replaces the old parse(..) and fromBytes(..) methods in NXM 2.4.0 it is
    // approximately 40% faster than the old methods and handles all time formats, not just STD,
    boolean isACQorACQDATE = (fmt == ACQDATE) || (fmt == ACQ);
    wsec = 0.0;
    fsec = 0.0;
    if (str == null) return;

    if (str.startsWith("\"") && str.endsWith("\"")) { // Remove quotes from a quoted string.
      str = str.substring(1,str.length()-1);
    }

    // Trim Time String (accept Strings with beginning and trailing spaces)
    str = str.trim();

    // allow T:... time as STD time Since 3.5.3 Bug 2872 and Bug 2883
    if ((fmt==STD || fmt == FULL_STD) && str.startsWith("T:")) {
      str = str.substring(2);
    }

    // FULL_STD added Since 3.5.2  Bug 2864
    if ((fmt==STD || fmt == FULL_STD) && StringUtil.isNumber(str)) { // Special case, time is a simple numeric value.
      fromJ1950(Convert.o2d(str));
      return;
    }
    if (fmt==STD || fmt == FULL_STD) {
      if ((str.indexOf('T') > 0) || (str.indexOf('Z') > 0) || ((str.indexOf('-') > 0) && (str.indexOf("::") < 0))) {
        // since 3.3.0 when Given an ISO 8601 time string (switch format from STD to ISO8601) as part of BUG 2673
        fmt = ISO8601;
      }
    }

    /////////////////////////////////////////////////////////////////
    // WARNING: This method is highly optimized, modify with care. //
    /////////////////////////////////////////////////////////////////
    char[]  chars = str.toCharArray(); // characters
    int     yrLen = 0;                 // length of year string (for handling two-digit years)
    int     year  = 1950;              // year
    int     mon   = 1;                 // month in year
    int     day   = 1;                 // day in month
    int     doy   = 1;                 // day of year (+1 = use 'mon'/'day')
    boolean neg   = chars[0] == '-';   // negative time?
    int     first = (neg)? 1: 0;       // first character
    boolean error = false;             // any errors?
    Exception causeOfError = null;

    if (fmt == NORAD) { // NORAD is different enough that it belongs on its own.
      // Need to use Double.parseDouble(..) for performance reasons.
      try {
        double sec = (Double.parseDouble(str.substring(first+2))-1) * SECONDS_PER_DAY; // day of year (with fraction for current day)
        year  = (chars[first+0]-'0')*10 + (chars[first+1]-'0');
        yrLen = 2;
        wsec  = Math.floor(sec);
        fsec  = sec - wsec;
      }
      catch (NumberFormatException nfe) {
        error = true;
        causeOfError = nfe;
      }
    } // end if NORAD format
    else if (fmt == FILENAME) { // FILENAME is different enough that it belongs on its own.
      int    sep  = str.indexOf('_');
      try {
        year = Integer.parseInt(str.substring(first, sep-4));         // year
        mon  = Integer.parseInt(str.substring(sep-4, sep-2));         // month
        day  = Integer.parseInt(str.substring(sep-2, sep));           // day
        wsec = Integer.parseInt(str.substring(sep+1, sep+3)) * 3600.0 // hours
             + Integer.parseInt(str.substring(sep+3, sep+5)) *   60.0 // minutes
             + Integer.parseInt(str.substring(sep+5, sep+7));         // whole seconds
        fsec = Double.parseDouble("0." + str.substring(sep+7));       // partial second - corrected in 3.5.2 Bug 2862

        // If fractional seconds rounded to 1.0, move whole second over to wsec
        if (fsec >= 1.0) {
          wsec++;
          fsec--;
        }
      }
      catch (NumberFormatException nfe) {
          error = true;
          causeOfError = nfe;
      }
    } // end else if FILENAME format
    else if (fmt == ISO8601) {
      String s  = (neg)? str.substring(1) : str;
      double tz = 0;                 // time zone 0=GMT

      int t     = s.indexOf('T');
      int w     = s.indexOf('W');
      int z     = s.lastIndexOf('Z');
      int dot   = s.lastIndexOf('.');
      int comma = s.lastIndexOf(',');
      int dash  = s.lastIndexOf('-');
      int plus  = s.lastIndexOf('+');
      int colon = s.lastIndexOf(':');
      int len   = s.length();

      // Allow alternate format with space instead of 'T'
      // example: "2014:05:04 11:12:13.345"
      t = (t > 0)? t : s.indexOf(' ');

      if (t < 0) {
        String prefix = (neg)? "-" : "";

        if ((colon >   0) ||  // <h>:<m>:<s>
            (dot   >   0) ||  // <h><m><s>.<fract>
            (comma >   0) ||  // <h><m><s>,<fract>
            (z     >   0)) {  // <time>Z
          fromString(prefix + "1950-01-01T" + s, fmt);
        }
        else if ((len ==  8) ||               // <year>-<doy> or <year><mon><day>
                 (s.indexOf('-') !=  dash)) { // <year>-<mon>-<day>
          fromString(prefix + s + "T00:00:00Z",   fmt);
        }
        else {
          fromString(prefix + "1950-01-01T" + s, fmt);
        }
        return;
      }

      // YEAR-MON-DAY ==========================================================
      yrLen = 4; // 4-digit year required

      try {
        if (w > 0) {     // <year>-W<wk>[-<d>] or <year>W<wk>[<d>]
          year = Integer.parseInt(s.substring(0,4));
          doy  = parseYearWeekDay(year, s.substring(0,t));
        }
        else if (t == 8) {
          if (chars[first+4] == '-') {     // <year>-<doy>
            year = Integer.parseInt(s.substring(0,4));
            doy  = Integer.parseInt(s.substring(5,t));
          }
          else {                           // <year><mon><day>
            year = Integer.parseInt(s.substring(0,4));
            mon  = Integer.parseInt(s.substring(4,6));
            day  = Integer.parseInt(s.substring(6,t));
          }
        }
        else if (t == 10) {                //  <year>-<mon>-<day>
          year = Integer.parseInt(s.substring(0,4));
          mon  = Integer.parseInt(s.substring(5,7));
          day  = Integer.parseInt(s.substring(8,t));
        }
        else {
          error = true;
        }

        // TIME ZONE =============================================================
        if (z == len-1) { tz = 0.0;                              len = z;    }
        else if (dash > t  ) { tz = parseTimeZone(s.substring(dash)); len = dash; }
        else if (plus > t  ) { tz = parseTimeZone(s.substring(plus)); len = plus; }

        // FRACTIONAL SEC ========================================================
        if (dot > 0) {
          fsec = Double.parseDouble("0" + s.substring(dot,len)); // corrected in 3.5.2 Bug 2862
          // If fractional seconds rounded to 1.0, move whole second over to wsec
          if (fsec >= 1.0) {
            wsec++;
            fsec--;
          }
          len  = dot;
        }
        else if (comma > 0) {
          fsec = Double.parseDouble("0."+s.substring(comma+1,len)); // corrected in 3.5.2 Bug 2862
          // If fractional seconds rounded to 1.0, move whole second over to wsec
          if (fsec >= 1.0) {
            wsec++;
            fsec--;
          }
          len  = comma;
        }

        // HOUR:MIN:SEC ==========================================================
        switch (len-t) {
          case 3: // hh
            wsec += Integer.parseInt(s.substring(t+1,t+3)) * 3600
                 + tz;
            break;
          case 5: // hhmm
            wsec += Integer.parseInt(s.substring(t+1,t+3)) * 3600
                 + Integer.parseInt(s.substring(t+3,t+5)) *   60
                 + tz;
            break;
          case 6: // hh:mm
            wsec += Integer.parseInt(s.substring(t+1,t+3)) * 3600
                 + Integer.parseInt(s.substring(t+4,t+6)) *   60
                 + tz;
            break;
          case 7: // hhmmss
            wsec += Integer.parseInt(s.substring(t+1,t+3)) * 3600
                 + Integer.parseInt(s.substring(t+3,t+5)) *   60
                 + Integer.parseInt(s.substring(t+5,t+7))
                 + tz;
            break;
          case 9: // hh:mm:ss
            wsec += Integer.parseInt(s.substring(t+1,t+3)) * 3600
                 + Integer.parseInt(s.substring(t+4,t+6)) *   60
                 + Integer.parseInt(s.substring(t+7,t+9))
                 + tz;
            break;
          default:
            error = true;
        }
      }
      catch (NumberFormatException nfe) {
        error = true;
        causeOfError = nfe;
      }
    } // end else if ISO8601
    else if (fmt==ACQTIME) { // ACQTIME added in 3.9.3 for NXM-3139
      if ((str.length() != 8) || (str.charAt(2) != ':') || (str.charAt(5) != ':')) {
        error = true;
        causeOfError = new NumberFormatException("Time String length or placement of ':' is incorrect - format should be strict HH:mm:ss");
      }
      else {
        try {
          wsec = Integer.parseInt(str.substring(0, 2)) * 3600.0 // hours
               + Integer.parseInt(str.substring(3, 5)) *   60.0 // minutes
               + Integer.parseInt(str.substring(6, 8));         // whole seconds
        }
        catch (NumberFormatException nfe) {
          error = true;
          causeOfError = nfe;
        }
      }
    }
    else {  // one of the other Time formats
      int     field = 1;             // field number 1..N (working backwards)
      int     wmult = 1;             // numeric multiplier for whole seconds
      int     end   = chars.length;  // current field start
      int     wval  = 0;             // whole part for numerics
      int firstColon = str.indexOf(':');

      //
      // Perform pre-processing on ACQ times that are different than the
      // traditional [[YY]YY.DDD:HH:MM:SS[.SSS]] in order to support all
      // forms supported by X-Midas
      // [YY]YY.DDD:HH:MM:SS[.SSS]  <-- original format - no adjustments needed
      // [YY]YY.DDD[:]              <-- adjust to start with day "field" (4)
      // [:]HH:MM:SS[.SSS]          <-- only adjustment is stripping any leading colon
      // [YY]YYMMDD:HH:MM:SS[.SSS]  <-- grab out year, month, & day & leave HH:MM:SS to process
      // [YY]YYMMDD[:]              <-- grab out year, month, & day
      //
      if (isACQorACQDATE){ // ACQDATE added in 3.9.3

        boolean endsWithColon = (str.lastIndexOf(':') == (end - 1));
        boolean isYYYYMMDDFormat = false; //default to assume regular [YY]YY.DDD:HH:MM:SS format
        int afterDayLoc = firstColon; //colon is usually 1st char after day specification

        if (firstColon < 0 || endsWithColon || (fmt == ACQDATE)) {  //no HMS
          if (endsWithColon) {   //strip off colon at end
                end -= 1;
          }
          if (str.indexOf('.') < 0) { //is [YY]YYMMDD format
            afterDayLoc = end; //day is at end of String
            isYYYYMMDDFormat = true;
          }
          else {  //is [YY]YY.DDD format
            field = 4;  //the first field to be processed (right-most) is a day field
          }
        }//end if no HMS
        else if (firstColon < 4) {  // is [:]HH:MM:SS format
          if (firstColon == 0) { //strip off any leading colon
            first +=1;
          }
        }
        else { // full Time String [YY]YY.DDD:HH:MM:SS[.SSS] or [YY]YYMMDD:HH:MM:SS[.SSS]
          int firstDot = str.indexOf('.');
          if (firstDot < 0 || firstDot > firstColon) { //is [YY]YYMMDD:HH:MM:SS[.SSS] format
            isYYYYMMDDFormat = true;
          }
        }
        //If [YY]YYMMDD[:HH:MM:SS[.SSS]] format grab off day, month, and year
        // and reduce remaining String to be processed to the optional HH:MM:SS[.SSS] part
        if (isYYYYMMDDFormat) {
          day = Integer.parseInt(str.substring(afterDayLoc-2, afterDayLoc));
          mon = Integer.parseInt(str.substring(afterDayLoc-4, afterDayLoc-2));
          year = Integer.parseInt(str.substring(first, afterDayLoc-4));
          yrLen = (afterDayLoc-4) - first; //figure out if 4-digit or 2-digit year
          first = afterDayLoc + 1; //loc of 1st char of HMS time info (if it exists)
        }
        //At this point the remaining ACQ String to process is either:
        //1) an untouched [[YY]YY.DDD:]HH:MM:SS[.SSS] String or
        //2) an HH:MM:SS[.SSS] string with year, month, and day already set or
        //3) a [YY]YY.DDD string with the field set so it knows to start with day
      }// end special ACQ pre-processing

      int dot = (str.lastIndexOf(".")); //location of last decimal - will error out a second is found in this field

      // set fsec and initial value of wsec using the fractional value
      // (if not successful, set error flag)
      if (dot > 4 || (!isACQorACQDATE && dot > 0)){
      //Make sure this is the fractional seconds separator rather than the yyyy.ddd separator of ACQ formatted strings.
        try {
          fsec = Double.parseDouble("0" + str.substring(dot)); // corrected in 3.5.2 Bug 2862
          if (fsec >= 1.0) {
            wsec++;
            fsec--;
          }
          end = dot;
          if (fmt==EPOCH && firstColon <0) {
            String beforeDot = str.substring(0,dot);
            if (!beforeDot.contains(":")) {
              wsec += Double.valueOf(beforeDot);
            }
            end = 0;
          }
        }
        catch (NumberFormatException nfe){
          error = true;
          causeOfError = nfe;
        }
      }

      for (int start = end; start >= first; start--) {
        char c = (start == first)? ' ': chars[start-1];

        boolean sep = (c == ':') || (c == '-');

        if (isACQorACQDATE && (field != 1)) sep = sep || (c == '.'); // Special separator for ACQ

        if (sep || (start == first)) {

          // a few of the cases have intentional fallthroughs if the test is true
          switch (fmt) {
            case STD:
            case FULL_STD:                                        // Since 3.5.2  Bug 2864
              switch (field) {
                case 1:  wsec += wval;                     break; // sec
                case 2:  wsec += wval *    60;             break; // min
                case 3:  wsec += wval *  3600;             break; // hour
                case 4:  if (end-start != 0) field++; else break; // time/date separator (FALLTHROUGH)
                case 5:  day   = wval;                     break; // day
                case 6:  mon   = wval;                     break; // mon
                case 7:  year  = wval; yrLen = end-start;  break; // year
                default: error = true;                     break;
              }
              break;
            case HMS:
              switch (field) {
                case 1:  wsec += wval;                     break; // sec
                case 2:  wsec += wval *    60;             break; // min
                case 3:  wsec += wval *  3600;             break; // hour
                default: error = true;                     break;
              }
              break;
            case YMD:
              switch (field) {
                case 1:  day   = wval;                     break; // day
                case 2:  mon   = wval;                     break; // mon
                case 3:  year  = wval; yrLen = end-start;  break; // year
                default: error = true;                     break;
              }
              break;
            case ACQ:
              switch (field) {
                case 1:  wsec += wval;                     break; // sec
                case 2:  wsec += wval *    60;             break; // min
                case 3:  wsec += wval *  3600;             break; // hour
                case 4:  day   = wval;                     break; // day of year
                case 5:  year  = wval; yrLen = end-start;  break; // year
                default: error = true;                     break;
              }
              break;
            case TCR:
              switch (field) {
                case 1:  wsec += wval;                     break; // sec
                case 2:  wsec += wval *    60;             break; // min
                case 3:  wsec += wval *  3600;             break; // hour
                case 4:  day   = wval;                     break; // day of year
                default: error = true;                     break;
              }
              break;
            case VAX:
              switch (field) {
                case 1:  wsec += wval;                     break; // sec
                case 2:  wsec += wval *    60;             break; // min
                case 3:  wsec += wval *  3600;             break; // hour
                case 4:  if (end-start != 0) field++; else break; // time/date separator (FALLTHROUGH)
                case 5:  year  = wval; yrLen = end-start;  break; // year
                case 6:  mon   = Parser.find(monthList,str.substring(start,end),-1); break; // mon (string)
                case 7:  day   = wval;                     break; // day
                default: error = true;                     break;
              }
              break;
            case EPOCH:
              switch (field) {
                case 1:  wsec += wval;                     break; // sec in year
                case 2:  year  = wval; yrLen = end-start;  break; // year
                default: error = true;                     break;
              }
              break;
            case ACQDATE: // added in 3.9.3 for NXM_3193
              switch (field) {
                case 4:  day   = wval;                     break; // day of year
                case 5:  year  = wval; yrLen = end-start;  break; // year
                default: error = true;                     break;
              }
              break;
            default:
              error = true;
              break;
          }
          field = field + 1;
          end   = start - 1;
          wval  = 0;
          wmult = 1;

        } else if ((c >= '0') && (c <= '9')) {
          wval  += (c - '0') * wmult;
          wmult *= 10;
        } else if ((field != 6) || fmt != VAX){
            error = true;
        }
      } // end for loop
      if (((fmt==STD) && (field==6)) || (fmt==ACQ) && (field==5)) {
        day++;  // relative format with no year
      }
    } // end else one of the other time formats
    if (error == true) {
      throw new MidasException("Time: Illegal time expression: "+str+" (fmt="+Parser.get(formatList,fmt)+")\n", causeOfError);
    }

    if (fmt == TCR) {
      year = getCurrentYear();
    }
    else if (fmt == HMS) {
      year = 1950;
    }
    else if (yrLen == 4) {
      // do nothing, year is correct
    }
    else if (yrLen == 2) {
      year += (year < 50)? 2000: 1900;
      if ((fmt != NORAD) && (fmt != ACQ)) { // NORAD and ACQ always have two-digit years
        Shell.getSharedMidasContext().deprecate("Time: Use of 2-digit years for time is deprecated, given '"+str+"'.");
      }
    }

    wsec += ymd2mjd(year,mon,day) * (double)SECONDS_PER_DAY; // Must compute as double to prevent integer overflow around 2019
    wsec += (doy - 1) * (double)SECONDS_PER_DAY;

    if (neg) wsec = -wsec; // negative time -- JC4: what about fsec? TODO !
    //if (wsec >= WSEC_MAX)
    //  throw new IllegalArgumentException("Invalid input date.  Input date of " + wsec + " is beyond the max value of " + WSEC_MAX);  // for BUG 2126
  }

  /** Calculate the Midas Julian Day (since 1950) for the supplied date.
      @param year  The year (e.g. 2002).
      @param month The month (1 = Jan)
      @param day   The day.
      @return The Midas Julian Day.
   */
  public static int ymd2mjd (int year, int month, int day) {
    // Note that this method will not work up past 2100 when the new rule about leap
    // years first become relevant (2100 is the first one that has the special case
    // making it a non-leap-year).
    int ly = 0; if (isLeapYear(year)) ly=12;
    int ndy = day + dim[month-1+ly];
    int mjd = (year-1950)*365 + (year+3)/4 - 488 + (ndy-1);
    return mjd;
  }

  /** Calculate the Right Ascension of Greenwich in radians. <br>
      This uses the Dec 1986 NORAD algorithm to compute the Right Ascen of Greenwich.
      @param DS50 The number of days since 1 Jan 1950 (i.e. seconds/86400).
      @return The Right Ascension of Greenwich in radians.

      @deprecated Since NeXtMidas 2.6.1, this method should be private, use {@link #getGHA()}.
   */ @Deprecated
  public static double RAGW (double DS50) {
    // This method is a direct port from type5000.for in X-Midas
    final double CR = 1.72027915249490159E-2;
    final double THGR70 = 1.73213060363866E0;
    final double TPI = 6.28318530717958648E0;
    final double FK4R = 5.06494548928754E-15;
    double TS70 = DS50 - 7304;
    double DS70 = Math.floor(TS70);
    double RAGW = THGR70 + CR*DS70 + (CR+TPI)*(TS70-DS70) + TS70*TS70*FK4R;
    RAGW = Math.IEEEremainder(RAGW,TPI);
    if (RAGW<-.5*TPI) RAGW += TPI;
    if (RAGW>+.5*TPI) RAGW -= TPI;
    return RAGW;
  }

   /** @since NeXtMidas 2.5.0 */
  @Override
  public boolean equals (Object obj) {
    Time t = Time.toTime(obj);
    return (t != null) && (t.wsec == wsec) && (t.fsec == fsec);
  }

  /** equals with absolute tolerance.
      @param t time to compare against
      @param tolerance absolute tolerance
      @return true if equal within specified absolute tolerance, otherwise false
      @since NeXtMidas 3.3.0
   */
  public boolean equals (Time t, double tolerance) {
    double diff = diff(t);
    return Math.abs(diff) <= tolerance;
  }

  /** @since NeXtMidas 2.5.0 */
  @Override
  public int hashCode () {
    // Must be overridden since we overrode equals(Object). This is loosely based on the Double.hashCode(..) function.
    long bits = Double.doubleToLongBits(wsec) ^ Double.doubleToLongBits(fsec);
    return (int)(bits ^ (bits >>> 32));
  }

  /** @since NeXtMidas 2.5.0 */
  @Override
  public int compareTo (Object obj) {
    Time t = Time.toTime(obj);
    int comp = Double.compare(wsec, t.wsec);
    return (comp != 0)? comp : Double.compare(fsec, t.fsec);
  }

  /** Is this time zero (check both whole and fractional seconds).
      @return true if time is zero
      @since NeXtMidas 2.8.0
   */
  public boolean isZero () {
    return (wsec==0) && (fsec==0);
  }

  /**
     Is the String provided a STD Format Time String?

     Forms considered a match:
       yyyy:[M]M:[d]d::[h]h:[m]m:[s]s[.SSS] or
       yyyy-[M]M-[d]d::[h]h:[m]m:[s]s[.SSS]
     (if a decimal point is included, then there must be 1 or more fractional seconds)

     @param timeString String to be checked
     @return true if it does match STD Format
     @since NeXtMidas 3.5.3 for Bug 2876 and Bug 2883
   */
  public static boolean isSTDTimeString(String timeString){
    Matcher matchSTD = STD_FORMAT_PATTERN.matcher(timeString);
    return matchSTD.matches();
  }

  /** Convert J1950 double seconds to time string. The number of decimal places
      for the output string is determined by {@link #getDecimalPrecision(double)}.
      @param secs     double of time since J1950
      @param sb       StringBuffer to receive string output of time (it will be emptied first) if not null
      @param prepend  String to prepend to sb before time string (this usually should not be null)
      @param postpend String to postpend to sb after time string (this usually should not be null)
      @return Time object representing this time without rounding
      @since NeXtMidas 2.8.0
   */
  static Time toTimeAndStringBuffer (double secs, StringBuffer sb, String prepend, String postpend) { // currently only used within SYS
    Time time = new Time(secs,0); // need to use this constructor so that it does NOT round to microseconds
    if (sb!=null) {
      sb.setLength(0); // first clear out sb
      int decimalPlaces = getDecimalPrecision(secs);
      sb.append(prepend).append(time.toString(decimalPlaces)).append(postpend);
    }
    return time;
  }

  /** Convert Time to Number
   *  @return Time as Number
   *  @since NeXtMidas 3.9.0 for NXM-3157
   */
  public Number toNumber() {
    BigDecimal w = new BigDecimal(wsec, MathContext.DECIMAL64);
    BigDecimal f = new BigDecimal(fsec, MathContext.DECIMAL64);
    BigDecimal s = w.add(f);
    if (s.precision() <= 15) {
      return s.doubleValue();
    }
    return s;
  }

  /** Get the ticks per second as a long for the specified timeDelta (seconds/tick time resolution).
      @param timeDelta The time delta (seconds/tick) (cannot be zero! SHOULD be > 2^-53 ~ 1.11e-16)
      @return the ticks per second as a long value for highest precision calculations
      @since NeXtMidas 3.3.0
   */
  static long ticksPerSecond (double timeDelta) {
    long ticksPerSecond = Math.round(1 / timeDelta); // MUST round otherwise will loose precision with division
    return ticksPerSecond;
  }

  /** Adds two time values (a + b) without modifying the input instance.
      @param a  The 1st time value.
      @param b  The 2nd time value (sec).
      @return The resulting value (i.e. "a + b").
      @since NeXtMidas 3.3.0
   */
  public static Time add (Time a, double b) {
    return (new Time(a)).addSec(b);
  }

  /** Adds two time values (a + b) without modifying the input instance.
      @param a  The 1st time value (sec).
      @param b  The 2nd time value.
      @return The resulting value (i.e. "a + b").
      @since NeXtMidas 3.3.0
   */
  public static Time add (double a, Time b) {
    return add(b,a);
  }

  /** Adds two time values (a + b) without modifying the input instance.
      @param a  The 1st time value (sec).
      @param b  The 2nd time value.
      @return The resulting value (i.e. "a + b").
      @since NeXtMidas 3.3.0
   */
  public static Time add (Time a, Time b) {
    return (new Time(a)).addTime(b);
  }

  /** Adds two time values (a + b) without modifying the input instance.
      @param a  The 1st time value (sec).
      @param b  The 2nd time value (sec).
      @return The resulting value (i.e. "a + b").
      @since NeXtMidas 3.3.0
   */
  public static Time add (double a, double b) {
    return (new Time()).addSec(a).addSec(b);
  }
}
