################################################################################
# julian.py - The Julian Library
#
# This is a set of routines for handing date and time conversions. It handles
# three time systems:
#   UTC = Universal Coordinates Time, similar to Grenwich Mean Time
#   TAI = International Atomic Time, which is the same as UTC except that it
#         ignores leap seconds. UTC and TAI always differ by a whole number of
#         seconds. The SPICE LS kernel is read at run time to get the lastest
#         list of leap seconds.
#   TDB = Terrestrial Barycentric Time, which is adjusted for the relativistic
#         effects that cause a clock on the Earth to vary in speed relative to
#         one at the solar system barycenter.
#
#   The library also handles calendar conversions and both parses and formats
#   strings that express time in UTC.
#
#   This library duplicates much of the functionality of python's built-in
#   datetime library, but is separate from them because the datetime library
#   cannot handle leap seconds.
#
#   Aside from the I/O routines, every argument to every function can be either
#   a scalar or something array-like, i.e, a numpy array, a tuple or a list.
#   Arguments other than scalars are converted to numpy arrays, the arrays are
#   broadcasted to the same shape if necessary, and the complete array(s) of
#   results is/are returned.
#
# Mark R. Showalter
# PDS Rings Node
# August 2011
#
# 12/31/11 (MRS) - Removed julian_isoparser based on indications that its
#   performance was unacceptably slow. New ISO routines parse the strings
#   without resorting to pyparsing, and also support array-like arguments. Also
#   added routine tai_from_iso().
#
# 1/11/12 (MRS) - Added the new leap second for July 1, 2012.
#
# 1/17/12 (BSW) - subtracted 12 hours from tdb on tdb_from_tai (and added 12
#   hours for the reverse in tai_from_tdb) to deal with J2000 starting from
#   noon on 1/1/2000, not midnight.
#
# 8/24/16 (MRS) - New leapsecond for 12/31/16.
#
# 3/29/18 (MRS) - Now compatible with both Python 2 and Python 3.
#
# 8/28/19 (MRS) - Eliminated returns of shapeless NumPy arrays; added minimal
#   support for SPICE TDT time.
################################################################################

from __future__ import print_function, division

import os
import datetime as dt
import numpy as np
import pyparsing
import unittest

import textkernel as tk
import julian_dateparser as jdp

# Handy utilities...

def INT(arg):
    if np.shape(arg):
        return arg.astype('int')
    else:
        return int(arg)

################################################################################
# Basic support for SPICE TDT times
################################################################################

JULIAN_TAI_MINUS_SPICE_TDT = 43167.816

def tai_from_tdt(tdt):
    return tdt + JULIAN_TAI_MINUS_SPICE_TDT

def tdt_from_tai(tai):
    return tai - JULIAN_TAI_MINUS_SPICE_TDT

################################################################################
# Initialization
#
# At load time, this file looks for an environment variable SPICE_LSK_FILEPATH.
# If found, this file is used to initialize the module. Otherwise, the text
# defined internally as SPICE_LSK_TEXT is used.
################################################################################

# Define the text from the latest LSK file, naif0009.tls
SPICE_LSK_DICT = {
    "DELTA_T_A": 32.184,
    "K": 1.657e-3,
    "EB": 1.671e-2,
    "M": (6.239996, 1.99096871e-7),
    "DELTA_AT": (10, dt.date(1972,1,1),
                 11, dt.date(1972,7,1),
                 12, dt.date(1973,1,1),
                 13, dt.date(1974,1,1),
                 14, dt.date(1975,1,1),
                 15, dt.date(1976,1,1),
                 16, dt.date(1977,1,1),
                 17, dt.date(1978,1,1),
                 18, dt.date(1979,1,1),
                 19, dt.date(1980,1,1),
                 20, dt.date(1981,7,1),
                 21, dt.date(1982,7,1),
                 22, dt.date(1983,7,1),
                 23, dt.date(1985,7,1),
                 24, dt.date(1988,1,1),
                 25, dt.date(1990,1,1),
                 26, dt.date(1991,1,1),
                 27, dt.date(1992,7,1),
                 28, dt.date(1993,7,1),
                 29, dt.date(1994,7,1),
                 30, dt.date(1996,1,1),
                 31, dt.date(1997,7,1),
                 32, dt.date(1999,1,1),
                 33, dt.date(2006,1,1),
                 34, dt.date(2009,1,1),
                 35, dt.date(2012,7,1),
                 36, dt.date(2015,7,1),
                 37, dt.date(2017,1,1))}

# Define the static variables needed for TAI-ET conversions
global DELTET_T_A, DELTET_K, DELTET_EB, DELTET_M0, DELTET_M1
DELTET_T_A = 0.     # indicates un-initialized data
DELTET_K   = 0.
DELTET_EB  = 0.
DELTET_M0  = 0.
DELTET_M1  = 0.

# Define the static variables needed for UTC-TAI conversions
global LS_YEAR0, LS_YEARS, LS_ARRAY1D, LS_ARRAY2D
LS_YEAR0 = 0        # indicates un-initialized data
LS_YEARS = 0
LS_ARRAY1D = None
LS_ARRAY2D = None

def load_from_dict(dict):
    """Loads the SPICE LSK parameters from the given dictionary. The dictionary
    is that returned by textkernel.from_file()["DELTET"].
    """

    global DELTET_T_A, DELTET_K, DELTET_EB, DELTET_M0, DELTET_M1
    global LS_YEAR0, LS_YEARS, LS_ARRAY1D, LS_ARRAY2D

    # Look up the needed variables and save them as globals
    DELTET_T_A = dict["DELTA_T_A"]
    DELTET_K   = dict["K"]
    DELTET_EB  = dict["EB"]

    (DELTET_M0, DELTET_M1) = dict["M"]

    # Construct a static array of (TAI minus UTC), the number of elapsed leap
    # seconds, and save them indexed by [year,halfyear]...

    # Get the list of leapseconds from the kernel
    delta_at = dict["DELTA_AT"]
    LS_YEAR0 = delta_at[1].year - 1     # subtract one so the first tabulated
                                        # year has zero leapseconds.
    LS_YEARS = delta_at[-1].year - LS_YEAR0 + 1
                                        # add one so years indexed is inclusive

    # Construct an array indexed by halfyear
    LS_ARRAY1D = np.zeros(2*LS_YEARS, dtype="int")

    for i in range(0, len(delta_at), 2):
        date = delta_at[i+1]
        indx = 2 * (date.year - LS_YEAR0) + (date.month - 1)//6
        LS_ARRAY1D[indx:] = delta_at[i]

    # Convert to an array indexed by [year,halfyear]
    # These arrays share the same data
    LS_ARRAY2D = LS_ARRAY1D.reshape((LS_YEARS,2))

def load_from_kernel(filespec):
    """Loads the SPICE LSK parameters from the given text kernel."""

    # Load the kernel as a dictionary
    load_from_dict(tk.from_file(filespec)["DELTET"])

# INITIALIZE PARAMETERS...

load_from_dict(SPICE_LSK_DICT)

try:
    filespec = os.environ["SPICE_LSK_FILEPATH"]
    load_from_kernel(filespec)
except KeyError: pass

########################################
# UNIT TESTS
########################################

class Test_Initialize(unittest.TestCase):

    def runTest(self):
        self.assertEqual(DELTET_T_A, 32.184,
            'DELTET_T_A value is wrong')
        self.assertEqual(DELTET_M0, 6.239996,
            'DELTET_M0 value is wrong')
        self.assertEqual(LS_YEAR0, 1971,
            'Leapseconds list does not start in 1971')
        self.assertEqual(LS_ARRAY2D[0,0], 0,
            'LS_ARRAY2D does not start at zero')
        self.assertEqual(LS_ARRAY2D[1999 - LS_YEAR0,0], 32,
            'LS_ARRAY2D returns the wrong value for early 1999')
        self.assertEqual(LS_ARRAY2D[1998 - LS_YEAR0,1], 31,
            'LS_ARRAY2D returns the wrong value for late 1998')

################################################################################
# Calendar conversions
#   Algorithms from http://alcor.concordia.ca/~gpkatch/gdate-algorithm.html
#
# day     = number of days elapsed since January 1, 2000
# month   = number of months elapsed since January 2000
# (y,m,d) = year, month (1-12), day (1-31)
# (y,d)   = year and day-of-year (1-366)
# (y,m)   = year and month number (1-12)
#
# All function operate on either scalars or arrays. If given scalars, they
# return scalars; if given anything array-like, they return arrays.
################################################################################

def day_from_ymd(y, m, d):
    """Day number from year, month and day. All must be integers. Supports
    scalar or array arguments."""

    m = (m + 9) % 12
    y = y - m//10
    return 365*y + y//4 - y//100 + y//400 + (m*306 + 5)//10 + d - 730426

########################################

def ymd_from_day(day):
    """Year, month and day from day number. Inputs must be integers."""

    # Execute the magic algorithm
    g = day + 730425
    y = (10000*g + 14780)//3652425
    ddd = g - (365*y + y//4 - y//100 + y//400)

    # Use scalar version of test...
    if np.shape(day):
        y[ddd < 0] -= 1
    elif ddd < 0:
        y -= 1

    ddd = g - (365*y + y//4 - y//100 + y//400)

    mi = (100*ddd + 52)//3060
    mm = (mi + 2)%12 + 1
    y = y + (mi + 2)//12
    dd = ddd - (mi*306 + 5)//10 + 1

    return (y, mm, dd)

########################################

def yd_from_day(day):
    """Year and day-of-year from day number."""

    (y,m,d) = ymd_from_day(day)
    return (y, day - day_from_ymd(y,1,1) + 1)

########################################

def day_from_yd(y, d):
    """Day number from year and day-of-year."""

    return day_from_ymd(y,1,1) + d - 1

########################################

def month_from_ym(y, m):
    """Number of elapsed months since January 2000. Supports scalar or array
    arguments."""

    return 12*(y - 2000) + (m - 1)

########################################

def ym_from_month(month):
    """Year and month from the number of elapsed months since January 2000."""

    y = INT(month//12)
    m = month - 12*y
    y += 2000
    m += 1

    return (y, m)

########################################

def days_in_month(month):
    """Number of days in month."""

    (y, m) = ym_from_month(month)
    day0 = day_from_ymd(y, m, 1)

    (y, m) = ym_from_month(month + 1)
    day1 = day_from_ymd(y, m, 1)

    return day1 - day0

########################################

def days_in_year(year):
    """Number of days in year."""

    return day_from_ymd(year+1, 1, 1) - day_from_ymd(year, 1, 1)

########################################
# UNIT TESTS
########################################

class Test_Calendar(unittest.TestCase):

    def runTest(self):

        # day_from_ymd()
        self.assertEqual(day_from_ymd(2000,1,1), 0)
        self.assertEqual(day_from_ymd(2000,2,np.array([27,28,29])).tolist(),    [57,58,59])
        self.assertEqual(day_from_ymd(2000,np.array([1,2,3]),1).tolist(),       [ 0,31,60])
        self.assertEqual(day_from_ymd(np.array([2000,2001,2002]),1,1).tolist(), [0,366,731])

        # ymd_from_day()
        self.assertEqual(ymd_from_day(0),   (2000,1,1))
        self.assertEqual(ymd_from_day(60),  (2000,3,1))
        self.assertEqual(ymd_from_day(365), (2000,12,31))
        self.assertEqual(ymd_from_day(366), (2001,1,1))

        # yd_from_day()
        self.assertEqual(yd_from_day(0), (2000,1))
        self.assertEqual(yd_from_day(365), (2000,366))

        # A large number of dates, spanning > 200 years
        daylist = np.arange(-40000,40000,83)

        # Convert to ymd and back
        (ylist, mlist, dlist) = ymd_from_day(daylist)
        test_daylist = day_from_ymd(ylist, mlist, dlist)

        self.assertTrue(np.all(test_daylist == daylist),
            "Large-scale conversion from day to YMD and back failed")

        # Make sure every month is in range
        self.assertTrue(np.all(mlist >= 1), "Month-of-year < 1 found")
        self.assertTrue(np.all(mlist <= 12), "Month-of-year > 12 found")

        # Make sure every day is in range
        self.assertTrue(np.all(dlist >= 1), "Day < 1 found")
        self.assertTrue(np.all(dlist <= 31), "Day > 31 found")

        # Another large number of dates, spanning > 200 years
        daylist = np.arange(-40001,40000,79)

        # Convert to yd and back
        (ylist, dlist) = yd_from_day(daylist)
        test_daylist = day_from_yd(ylist, dlist)

        self.assertTrue(np.all(test_daylist == daylist))

        # Make sure every day is in range
        self.assertTrue(np.all(dlist >= 1), "Day < 1 found")
        self.assertTrue(np.all(dlist <= 366), "Day > 366 found")

        # A large number of months, spanning > 200 years
        monthlist = np.arange(-15002,15000,19)

        # Convert to ym and back
        (ylist, mlist) = ym_from_month(monthlist)
        test_monthlist = month_from_ym(ylist, mlist)

        self.assertTrue(np.all(test_monthlist == monthlist))

        # Make sure every month is in range
        self.assertTrue(np.all(mlist >= 1), "Month-of-year < 1 found")
        self.assertTrue(np.all(mlist <= 12), "Month-of-year > 12 found")

        # Check the days in each January
        mlist = np.arange(month_from_ym(1980,1),month_from_ym(2220,1),12)
        self.assertTrue(np.all(days_in_month(mlist) == 31),
            "Not every January has 31 days")

        # Check the days in each April
        mlist = np.arange(month_from_ym(1980,4),month_from_ym(2220,4),12)
        self.assertTrue(np.all(days_in_month(mlist) == 30),
            "Not every April has 30 days")

        # Check the days in each year
        ylist = np.arange(1890, 2210)
        dlist = days_in_year(ylist)
        self.assertTrue(np.all((dlist == 365) | (dlist == 366)),
            "Not every year has 365 or 366 days")

        # Every leap year is a multiple of four
        select = np.where(dlist == 366)
        self.assertTrue(np.all(ylist[select]%4 == 0),
            "Not every leapyear is a multiple of four")

        # February always has 29 days in a leapyear
        self.assertTrue(np.all(days_in_month(month_from_ym(ylist[select],2))
            == 29), "Not every leap year February has 29 days")

        # February always has 28 days otherwise
        select = np.where(dlist == 365)
        self.assertTrue(np.all(days_in_month(month_from_ym(ylist[select],2))
            == 28), "Not every non-leap year February has 28 days")

################################################################################
# Leapsecond routines
################################################################################

def leapsecs_from_ym(y, m):
    """Returns the number of elapsed leapseconds for a given year and month."""

    # Scalar version...
    if np.shape(y) == () and np.shape(m) == ():
        index = 2*(y - LS_YEAR0) + (m-1)//6
        if index <= 0:               return LS_ARRAY1D[0]
        if index >= LS_ARRAY1D.size: return LS_ARRAY1D[-1]
        return LS_ARRAY1D[index]

    # Array version...
    index = 2*(y - LS_YEAR0) + (m-1)//6
    index[index < 0] = 0
    index[index >= LS_ARRAY1D.size] = LS_ARRAY1D.size - 1
    return LS_ARRAY1D[index]

########################################

def leapsecs_from_day(day):
    """Returns the number of elapsed leapseconds for a given number of days
    elapsed since January 1, 2000."""

    (y,m,d) = ymd_from_day(day)
    return leapsecs_from_ym(y,m)

########################################

def seconds_on_day(day, leapseconds=True):
    """Returns the number of seconds duration for the given day number since
    January 1, 2000."""

    if not leapseconds: return 86400

    shape = np.shape(day)
    if shape != (): day = np.asarray(day)

    return 86400 + leapsecs_from_day(day+1) - leapsecs_from_day(day)

########################################
# UNIT TESTS
########################################

class Test_Leapseconds(unittest.TestCase):

    def runTest(self):

        # A large number of dates, spanning > 200 years
        daylist = range(-40001,40000,83)

        # Check all seconds are within the range
        self.assertTrue(np.all(seconds_on_day(daylist) >= 86400))
        self.assertTrue(np.all(seconds_on_day(daylist) <= 86401))

        self.assertEqual(seconds_on_day(day_from_ymd(1998,12,31)), 86401)

        # Check case where leap seconds are ignored
        self.assertTrue(np.all(seconds_on_day(daylist,False) == 86400))

        self.assertEqual(seconds_on_day(day_from_ymd(1998,12,31), False), 86400)

################################################################################
# TAI - UTC conversions
################################################################################

def day_sec_from_tai(tai):
    """Returns a number of integer days and the number of elapsed seconds into
    that day, given the number of elapsed seconds since January 1, 2000 TAI."""

    # Make an initial guess at the day and seconds
    day = INT(tai//86400)
    leapsecs = leapsecs_from_day(day)
    sec = tai - 86400. * day - leapsecs

    # Update the day and seconds if necessary
    if np.shape(tai):
        mask = (sec < 0.)
        day[mask] -= 1
        sec[mask] += seconds_on_day(day[mask])
    elif sec < 0.:
        day -= 1
        sec += seconds_on_day(day)

    return (day, sec)

########################################

def tai_from_day(day):
    """Returns a number of elapsed seconds since January 1, 2000 TAI, at the
    beginning of the specified day since January 1, 2000 UTC."""

    (y,m,d) = ymd_from_day(day)
    leapsecs = leapsecs_from_ym(y,m)

    return 86400. * day + leapsecs

########################################
# UNIT TESTS
########################################

class Test_TAI_UTC(unittest.TestCase):

    def runTest(self):

        # Check tai_from_day
        self.assertEqual(tai_from_day(0), 32)
        self.assertEqual(tai_from_day(np.array([0,1]))[0],    32)
        self.assertEqual(tai_from_day(np.array([0,1]))[1], 86432)

        #Check day_sec_from_tai
        self.assertEqual(day_sec_from_tai(32.), (0, 0.))
        self.assertEqual(day_sec_from_tai(np.array([35.,86435.]))[0][0], 0)
        self.assertEqual(day_sec_from_tai(np.array([35.,86435.]))[0][1], 1)
        self.assertEqual(day_sec_from_tai(np.array([35.,86435.]))[1][0], 3.)
        self.assertEqual(day_sec_from_tai(np.array([35.,86435.]))[1][1], 3.)

        # A large number of dates, spanning > 200 years
        daylist = np.arange(-40000,40000,83)

        # Test as a loop
        for day in daylist:
            (test_day, test_sec) = day_sec_from_tai(tai_from_day(day))
            self.assertEqual(test_day, day, "Day mismatch at " + str(day))
            self.assertEqual(test_sec, 0,   "Sec mismatch at " + str(day))

        # Test as an array operation
        (test_day, test_sec) = day_sec_from_tai(tai_from_day(daylist))
        self.assertTrue(np.all(test_day == daylist))
        self.assertTrue(np.all(test_sec == 0))

################################################################################
# Time-of-day conversions
################################################################################

def hms_from_sec(sec):
    """Hour, minute and second from seconds into day. Supports scalar or array
    arguments. Input must be between 0 and 86410, where numbers above 86400 are
    treated as leap seconds."""

    # Test for valid range
    if (np.any(sec < 0.)):     raise ValueError("seconds < 0")
    if (np.any(sec > 86410.)): raise ValueError("seconds > 86410")

    h = np.minimum(INT(sec//3600), 23)
    t = sec - 3600*h

    m = np.minimum(INT(t//60), 59)
    t -= 60*m

    return (h, m, t)

########################################

def sec_from_hms(h, m, s):
    """Seconds into day from hour, minute and second. Supports scalar or array
    arguments."""

    return 3600*h + 60*m + s

########################################
# UNIT TESTS
########################################

class Test_Time_of_Day(unittest.TestCase):

    def runTest(self):

        #Check hms_from_sec
        self.assertEqual(hms_from_sec(0), (0, 0, 0),
                         "0 is not (0, 0, 0).")
        self.assertEqual(hms_from_sec(86400), (23, 59, 60),
                         "86400 is not (23, 59, 60).")
        self.assertEqual(hms_from_sec(86410), (23, 59, 70),
                         "86410 is not (23, 59, 70).")
        self.assertRaises(ValueError, hms_from_sec, 86410.0000001)
        self.assertRaises(ValueError, hms_from_sec, -1.e-300)

        # Check sec_from_hms
        self.assertEqual(sec_from_hms(0, 0, 0), 0,
                         "(0, 0, 0) is not 0 seconds.")
        self.assertEqual(sec_from_hms(23, 59, 60), 86400,
                         "(23, 59, 60) is not 86400 seconds.")

        # Array tests
        # This makes about 333,000 non-uniformly spaced transcendental numbers
        secs = 86410. * np.sqrt(np.arange(0., 1., 3.e-6))

        # Because HMS times carry extra precision, inversions should be exact
        (h,m,s) = hms_from_sec(secs)
        errors = (sec_from_hms(h,m,s) - secs)
        self.assertTrue(np.all(errors == 0.))

        # Test all seconds
        seclist = np.arange(0,86410)

        # Convert to hms and back
        (h, m, t) = hms_from_sec(seclist)
        test_seclist = sec_from_hms(h, m, t)

        self.assertTrue(np.all(test_seclist == seclist),
            'Large-scale conversion from sec to hms and back failed')

################################################################################
# TDB - TAI conversions
#
# Extracted from naif0009.tls...
#
# [4]       ET - TAI  =  DELTA_T_A  + K sin E
#
# where DELTA_T_A and K are constant, and E is the eccentric anomaly of the
# heliocentric orbit of the Earth-Moon barycenter. Equation [4], which ignores
# small-period fluctuations, is accurate to about 0.000030 seconds.
#
# The eccentric anomaly E is given by
#
# [5]       E = M + EB sin M
#
# where M is the mean anomaly, which in turn is given by
#
# [6]       M = M  +  M t
#                0     1
#
# where t is the number of ephemeris seconds past J2000.
#
# in the end, subtract 12 hours as J2000 starts at noon on 1/1/2000, not
# midnight
################################################################################

def tdb_from_tai(tai, iters=2):
    """Converts from TAI to TDB. Operates on either a single scalar or an
    arbitrary array of values. Accurate to about 30 microseconds but an exact
    inverse for function tai_from_tdb().

    The default value of 2 iterations appears to give full double-precision
    convergencec for every possible case."""

    # Solve:
    #   tdb = tai + DELTA + K sin(E)
    #   E = M + EB sin(M)
    #   M = M0 + M1 * tdb

    # Solve for
    #   x == DELTA + K sin(E)
    # by iteration. It's fast.

    x = DELTET_T_A
    for iter in range(iters):
        m = DELTET_M0 + DELTET_M1 * (x + tai)
        e = m + DELTET_EB * np.sin(m)
        x = DELTET_T_A + DELTET_K * np.sin(e)

    return x + tai - 43200.

########################################

def tai_from_tdb(tdb):
    """Converts from TDB to TAI. Operates on either a single scalar or an
    arbitrary array of values. An exact solution; no iteration required."""

    tdb = tdb + 43200.   # add 12 hours as tdb is respect to noon on 1/1/2000

    #   tai = tdb - DELTA - K sin(E)
    #   E = M + EB sin(M)
    #   M = M0 + M1 * tdb

    m = DELTET_M0 + DELTET_M1 * tdb
    return tdb - DELTET_T_A - DELTET_K * np.sin(m + DELTET_EB * np.sin(m))

########################################
# UNIT TESTS
########################################

class Test_TDB_TAI(unittest.TestCase):

    def runTest(self):

        # Check tdb_from_tai
        self.assertAlmostEqual(tdb_from_tai(tai_from_day(0)),
                               64.183927284731055-43200, places=15)

        # Check tai_from_tdb
        self.assertTrue(abs(tai_from_tdb(64.183927284731055)
                                         - tai_from_day(0)) < 1.e15)

        # Test inversions around tdb = 0.
        # A list of two million small numbers spanning 2 sec
        secs = 2.
        tdbs = np.arange(-secs, secs, 1.e-6 * secs)
        errors = tdb_from_tai(tai_from_tdb(tdbs)) - tdbs
        self.assertTrue(np.all(errors <  1.e-11 * secs))
        self.assertTrue(np.all(errors > -1.e-11 * secs))

        # Now make sure we get the exact same results when we replace arrays by
        # scalars
        for i in range(0, tdbs.size, 1000):
            self.assertEqual(errors[i],
                             tdb_from_tai(tai_from_tdb(tdbs[i])) - tdbs[i])

        # A list of two million bigger numbers spanning ~ 20 days
        secs = 20. * 86400.
        tdbs = np.arange(-secs, secs, 1.e-6 * secs)
        errors = tdb_from_tai(tai_from_tdb(tdbs)) - tdbs
        self.assertTrue(np.all(errors <  1.e-15 * secs))
        self.assertTrue(np.all(errors > -1.e-15 * secs))

        # A list of two million still bigger numbers spanning ~ 2000 years
        secs = 2000. * 365. * 86400.
        tdbs = np.arange(-secs, secs, 1.e-6 * secs)
        errors = tdb_from_tai(tai_from_tdb(tdbs)) - tdbs
        self.assertTrue(np.all(errors <  1.e-15 * secs))
        self.assertTrue(np.all(errors > -1.e-15 * secs))

################################################################################
# Julian Date conversions
################################################################################

MJD_OF_EPOCH_2000 = 51544
JD_OF_EPOCH_2000 = 2451544.5
JD_MINUS_MJD = JD_OF_EPOCH_2000 - MJD_OF_EPOCH_2000

# Integer versions

def mjd_from_day(day):
    """Returns the Modified Julian Date for a specified day number relative to
    January 1, 2000. Works for scalars or arrays, expected to be integers."""

    return day + MJD_OF_EPOCH_2000

def day_from_mjd(mjd):
    """Returns the day number relative to January 1, 2000 for the given Modified
    Julian Day. Works for scalars or arrays, expected to be integers."""

    return mjd - MJD_OF_EPOCH_2000

# Floating-point versions

def jd_from_time(time):
    """Returns the Julian Date for a specified number of seconds relative to
    midnight on January 1, 2000. Works for scalars or arrays. This definition of
    Julian Date assumes every day contains 86400 seconds; it ignores leap
    seconds."""

    return time/86400. + JD_OF_EPOCH_2000

def mjd_from_time(time):
    """Returns the Modified Julian Date for a specified number of seconds
    relative to midnight on January 1, 2000. Works for scalars or arrays. This
     definition of Julian Date assumes every day contains 86400 seconds; it
    ignores leap seconds."""

    return time/86400. + MJD_OF_EPOCH_2000

def time_from_jd(jd):
    """Returns the elapsed seconds relative to midnight on January 1, 2000 from
    the Julian Date. Works for scalars or arrays. This definition of Julian Date
    assumes every day contains 86400 seconds; it ignores leap seconds."""

    return (jd - JD_OF_EPOCH_2000) * 86400.

def time_from_mjd(mjd):
    """Returns the elapsed seconds relative to midnight on January 1, 2000 from
    the Modified Julian Date. Works for scalars or arrays. This definition of
    MJD assumes every day contains 86400 seconds; it ignores leap seconds."""

    return (mjd - MJD_OF_EPOCH_2000) * 86400.

# Floating-point UTC versions

def jd_from_day_sec(day, sec, leapseconds=True):
    """Returns the Julian Date for a given UTC day and seconds. Works for
    scalars or arrays. This definition of Julian Date includes leap seconds, so
    some days are longer than others."""

    # Add zero to force conversion to float
    return day + (sec + 0.)/seconds_on_day(day, leapseconds) + JD_OF_EPOCH_2000

def mjd_from_day_sec(day, sec, leapseconds=True):
    """Returns the Modified Julian Date for a given UTC day and seconds. Works
    for scalars or arrays. This definition of MJD includes leap seconds, so some
    days are longer than others."""

    return day + (sec + 0.)/seconds_on_day(day, leapseconds) + MJD_OF_EPOCH_2000

def day_sec_from_jd(jd, leapseconds=True):
    """Returns a UTC day number and seconds based on a Julian Date. Works for
    scalars or arrays. This definition of Julian Date allows for leap seconds,
    so some days are longer than others."""

    delta = jd - JD_OF_EPOCH_2000
    day = INT(delta//1)
    sec = seconds_on_day(day, leapseconds) * (delta - day)

    return (day, sec)

def day_sec_from_mjd(mjd, leapseconds=True):
    """Returns a UTC day number and seconds based on a Julian Date. Works for
    scalars or arrays. This definition of Julian Date allows for leap seconds,
    so some days are longer than others."""

    delta = mjd - MJD_OF_EPOCH_2000
    day = INT(delta//1)
    sec = seconds_on_day(day, leapseconds) * (delta - day)

    return (day, sec)

########################################
# UNIT TESTS
########################################

class Test_JD_MJD(unittest.TestCase):

    def runTest(self):

        # Test integer conversions...
        self.assertEqual(mjd_from_day(0), 51544)
        self.assertEqual(day_from_mjd(51545), 1)

        self.assertTrue(np.all(mjd_from_day(np.arange(10)) ==
                               np.arange(10) + 51544))

        self.assertTrue(np.all(day_from_mjd(np.arange(10)) ==
                               np.arange(10) - 51544))

        # Test MJD floating-point conversions spanning 1000 years
        span = 1000. * 365.25
        mjdlist = np.arange(-span, span, np.pi) + MJD_OF_EPOCH_2000

        test = mjd_from_time(time_from_mjd(mjdlist))
        error = np.max(np.abs(test - mjdlist))
        self.assertTrue(np.max(np.abs(test - mjdlist)) < span * 1.e-15)

        for mjd in mjdlist[:100]:
            error = abs(mjd_from_time(time_from_mjd(mjd)) - mjd)
            self.assertTrue(error < span * 1.e-15)

        (day,sec) = day_sec_from_mjd(mjdlist)
        test = mjd_from_day_sec(day,sec)
        error = np.abs(test - mjdlist)
        self.assertTrue(np.max(np.abs(test - mjdlist)) < span * 1.e-15)

        for mjd in mjdlist[:100]:
            (day,sec) = day_sec_from_mjd(mjd)
            error = abs(mjd_from_day_sec(day,sec) - mjd)
            self.assertTrue(error < span * 1.e-15)

        # Test JD floating-point conversions spanning 100 years
        span = 100. * 365.25
        jdlist = np.arange(-span, span, np.pi/10.) + JD_OF_EPOCH_2000

        test = jd_from_time(time_from_jd(jdlist))
        error = np.max(np.abs(test - jdlist))
        self.assertTrue(np.max(np.abs(test - jdlist)) < JD_OF_EPOCH_2000*1.e-15)

        for jd in jdlist[:100]:
            error = abs(jd_from_time(time_from_jd(jd)) - jd)
            self.assertTrue(error < span * 1.e-15)

        (day,sec) = day_sec_from_jd(jdlist)
        test = jd_from_day_sec(day,sec)
        error = np.abs(test - jdlist)
        self.assertTrue(np.max(np.abs(test - jdlist)) < JD_OF_EPOCH_2000*1.e-15)

        for jd in jdlist[:100]:
            (day,sec) = day_sec_from_jd(jd)
            error = abs(jd_from_day_sec(day,sec) - jd)
            self.assertTrue(error < span * 1.e-15)

################################################################################
# Time System conversions
#
# UTC day and second allow for leap seconds.
# TAI day and second does not allow for leapseconds. Every day has exactly 86400
#       seconds. TAI and UTC were equal prior to 1972.
# TDB is TAI plus an offset and allowance for relativistic effects.
################################################################################

def utc_from_day_sec_as_type(day, sec, time_type="UTC"):

    # Conversion UTC to UCT is easy
    if time_type == "UTC": return (day, sec)

    # Conversion from day and second to TAI ignores leap seconds
    if time_type == "TAI":
        tai = 86400. * day + sec
        return day_sec_from_tai(tai)

    # Conversion from TDB requires a relativistic correction to TAI
    if time_type == "TDB":
        tdb = 86400. * day + sec
        tai = tai_from_tdb(tdb)
        return day_sec_from_tai(tai)

    raise ValueError("Unrecognized time_type: " + time_type)

def day_sec_as_type_from_utc(day, sec, time_type="UTC"):

    # Conversion UTC to UCT is easy
    if time_type == "UTC": return (day, sec)

    # Conversion from TAI to day and second ignores leap seconds
    if time_type == "TAI":
        tai = tai_from_day(day) + sec
        day = tai // 86400.
        sec = tai - 86400. * day
        return (day, sec)

    # Conversion to TDB requires a relativistic correction to TAI
    if time_type == "TDB":
        tai = tai_from_day(day) + sec
        tdb = tdb_from_tai(tai)
        day = tdb // 86400.
        sec = tdb - 86400. * day
        return (day, sec)

    raise ValueError("Unrecognized time_type: " + time_type)

########################################
# UNIT TESTS
########################################

class Test_Conversions(unittest.TestCase):

    def runTest(self):

        # TAI tests...

        # TAI was 31-32 seconds ahead of UTC in 1999,2000,2001
        (day,sec) = day_sec_as_type_from_utc(np.array((-366,0,366)),0.,"TAI")
        self.assertTrue(np.all(day == (-366,0,366)))
        self.assertTrue(np.all(sec == (31.,32.,32.)))

        # Inverse of the above
        (day,sec) = utc_from_day_sec_as_type(np.array((-366,0,366)),32.,"TAI")
        self.assertTrue(np.all(day == (-366,0,366)))
        self.assertTrue(np.all(sec == (1.,0.,0.)))

        # TAI jumped 10 seconds ahead of UTC at the beginning of 1972
        (day,sec) = day_sec_as_type_from_utc(day_from_ymd(1972,1,1),0,"TAI")
        self.assertEqual(sec,10)
        (day,sec) = day_sec_as_type_from_utc(day_from_ymd(1971,12,31),0,"TAI")
        self.assertEqual(sec,0)

        # Inverses of the above
        (day,sec) = utc_from_day_sec_as_type(day_from_ymd(1972,1,1),10,"TAI")
        self.assertEqual(sec,0)
        (day,sec) = utc_from_day_sec_as_type(day_from_ymd(1971,12,31),0,"TAI")
        self.assertEqual(sec,0)

        # Now do a batch test 1971-2012. Conversions should be exact.
        daylist = np.arange(day_from_ymd(1971,1,1),day_from_ymd(2012,1,1))

        (day,sec) = day_sec_as_type_from_utc(daylist,43200.,"TAI")
        (dtest,stest) = utc_from_day_sec_as_type(day,sec,"TAI")
        self.assertTrue(np.all(dtest == daylist))
        self.assertTrue(np.all(stest == 43200.))

        (day,sec) = day_sec_as_type_from_utc(daylist,0.,"TAI")
        (dtest,stest) = utc_from_day_sec_as_type(day,sec,"TAI")
        self.assertTrue(np.all(dtest == daylist))
        self.assertTrue(np.all(stest == 0.))

        (day,sec) = utc_from_day_sec_as_type(daylist,0.,"TAI")
        (dtest,stest) = day_sec_as_type_from_utc(day,sec,"TAI")
        self.assertTrue(np.all(dtest == daylist))
        self.assertTrue(np.all(stest == 0.))

        # TDB tests...

        self.assertTrue(abs(day_sec_as_type_from_utc(0,0,"TDB")[1]
                            - 64.183927284731055) < 1.e15)
        self.assertTrue(abs(utc_from_day_sec_as_type(0,0,"TDB")[1]
                            + 64.183927284731055) < 1.e15)

        (day,sec) = day_sec_as_type_from_utc(daylist,43200.,"TDB")
        (dtest,stest) = utc_from_day_sec_as_type(day,sec,"TDB")
        self.assertTrue(np.all(dtest == daylist))
        self.assertTrue(np.all(np.abs(stest - 43200.) < 1.e-6))

        (day,sec) = utc_from_day_sec_as_type(daylist,43200.,"TDB")
        (dtest,stest) = day_sec_as_type_from_utc(day,sec,"TDB")
        self.assertTrue(np.all(dtest == daylist))
        self.assertTrue(np.all(np.abs(stest - 43200.) < 1.e-6))

################################################################################
# Formatting Routines
################################################################################

def ymd_format_from_day(day, buffer=None):
    """Returns a date in 'yyyy-mm-dd' format. Supports scalars or arrays.

    Input:
        day         integer or arbitrary array of integers defining day numbers
                    relative to January 1, 2000.
        buffer      an optional byte array into which to write the results.
                    Only used if day is an array. Must have sufficient length.
    """

    return _yxd_format_from_day(day, True, buffer)

def yd_format_from_day(day, buffer=None):
    """Returns a date in 'yyyy-ddd' format. Supports scalars or arrays.

    Input:
        day         integer or arbitrary array of integers defining day numbers
                    relative to January 1, 2000.
        buffer      an optional byte array into which to write the results.
                    Only used if day is an array. Must have sufficient length.
    """

    return _yxd_format_from_day(day, False, buffer)

def _yxd_format_from_day(day, ymd=True, buffer=None):
    """Support function for ymd and yd ISO date formats."""

    # Translate the days and set up the formatting parameters
    if ymd:
        tuple = ymd_from_day(day)
        fmt = "{:04d}-{:02d}-{:02d}"
        dtype = "|S10"
        lstring = 10
    else:
        tuple = yd_from_day(day)
        fmt = "{:04d}-{:03d}"
        dtype = "|S8"
        lstring = 8

    # Return a scalar
    if np.shape(day) == ():
        return fmt.format(*tuple)   # "*tuple" expands the tuple into its parts

    # Create or check the buffer
    if buffer is None:
        buffer = np.empty(np.shape(day), dtype=dtype)
    else:
        if buffer.shape != np.shape(day):
            raise ValueError("buffer shape does not match day array")
        if lstring > buffer.dtype.itemsize:
            raise ValueError("buffer is too small for the ISO date format")

    # Fill the buffer
    if ymd:
        (y,m,d) = tuple
        for i,value in np.ndenumerate(day):
            buffer[i] = fmt.format(y[i], m[i], d[i])
    else:
        (y,d) = tuple
        for i,value in np.ndenumerate(day):
            buffer[i] = fmt.format(y[i], d[i])

    return buffer

########################################

def hms_format_from_sec(sec, digits=None, suffix="", buffer=None):
    """Returns a time in 'hh:mm:ss[.mmm][Z]' format. Supports scalars or arrays.

    Input:
        day         integer or arbitrary array of integers defining day numbers
                    relative to January 1, 2000.
        sec         the number of seconds into a day, or an arbitrary array
                    thereof; should be less than 86410.
        digits      the number of digits to include after the decimal point; use
                    a negative value or None for for seconds to be rounded to
                    integer.
        suffix      "Z" to include the Zulu time zone indicator.
        buffer      an optional byte array into which to write the results.
                    Only used if day is an array. Must have sufficient length.
    """

    (h,m,s) = hms_from_sec(sec)

    if digits is None or digits < 0:
        secfmt = "{:02d}"
        lsec = 2
        s = INT((s + 0.5) // 1)
    else:
        secfmt = "{:0" + str(digits+3) + "." + str(digits) + "f}"
        lsec = 3 + digits

    fmt = ("{:02d}:{:02d}:" + secfmt + "{:s}")

    if suffix != "Z": suffix = ""
    lstring = 6 + lsec + len(suffix)

    if np.shape(sec) == ():
        return fmt.format(h,m,s,suffix)

    if buffer is None:
        buffer = np.empty(np.shape(sec), dtype="|S" + str(lstring))
    else:
        if buffer.shape != np.shape(sec):
            raise ValueError("buffer shape does not match time array")
        if lstring > buffer.dtype.itemsize:
            raise ValueError("buffer is too small for the ISO time format")

    for i,value in np.ndenumerate(sec):
        buffer[i] = fmt.format(h[i], m[i], s[i], suffix)

    return buffer

########################################

def ymdhms_format_from_day_sec(day, sec, sep="T", digits=None, suffix="",
                               buffer=None):
    """Returns a date and time in ISO format 'yyyy-mm-ddThh:mm:ss....'. Works
    for both scalars and arrays.

    Input:
        day         integer or arbitrary array of integers defining day numbers
                    relative to January 1, 2000.
        sec         the number of seconds into a day; should be less than the
                    number of seconds on the associated day. Note that day and
                    sec need not have the same shape, but must be broadcastable
                    to the same shape.
        sep         the character to separate the date from the time. Default is
                    "T" but " " is also allowed.
        digits      the number of digits to include after the decimal point; use
                    a negative value or None for for seconds to be rounded to
                    integer.
        suffix      "Z" to include the Zulu time zone indicator.
        buffer      an optional byte array into which to write the results.
                    Only used if day/sec are arrays. If the buffer is provided,
                    the elements must have sufficient length.
    """

    return _yxdhms_format_from_day_sec(day, sec, True, sep, digits, suffix,
                                       buffer)

def ydhms_format_from_day_sec(day, sec, sep="T", digits=None, suffix="",
                              buffer=None):
    """Returns a date and time in ISO format 'yyyy-dddThh:mm:ss....'. Works
    for both scalars and arrays.

    Input:
        day         integer or arbitrary array of integers defining day numbers
                    relative to January 1, 2000.
        sec         the number of seconds into a day; should be less than the
                    number of seconds on the associated day. Note that day and
                    sec need not have the same shape, but must be broadcastable
                    to the same shape.
        sep         the character to separate the date from the time. Default is
                    "T" but " " is also allowed.
        digits      the number of digits to include after the decimal point; use
                    a negative value or None for for seconds to be rounded to
                    integer.
        suffix      "Z" to include the Zulu time zone indicator.
        buffer      an optional byte array into which to write the results.
                    Only used if day/sec are arrays. If the buffer is provided,
                    the elements must have sufficient length.
    """

    return _yxdhms_format_from_day_sec(day, sec, False, sep, digits, suffix,
                                       buffer)

def _yxdhms_format_from_day_sec(day, sec, ymd=True, sep="T", digits=None,
                                suffix="", buffer=None):
    """Support function for ymd and yd ISO date-time formats."""

    # Validate the extra characters
    sep = str(sep)
    suffix = str(suffix)

    if sep not in ('T', ' ', ':'):
        raise ValueError('date/time separator must be "T", colon, or blank')

    if suffix not in ('Z', ''):
        raise ValueError('suffix character can only be "Z" or absent')

    # Round the seconds
    if digits is None or digits < 0:
        lsec = 2
        sec = (sec + 0.5)//1
    else:
        lsec = 3 + digits
        factor = 10.**digits
        sec = ((sec*factor + 0.5)//1) / factor

    # Return a scalar
    if np.shape(day) == () and np.shape(sec) == ():
        if sec >= seconds_on_day(day):
            sec -= seconds_on_day(day)
            day += 1

        return (_yxd_format_from_day(day, ymd) + sep +
                hms_format_from_sec(sec, digits, suffix))

    # Check day boundaries
    secs_on_day = seconds_on_day(day)
    crossovers = (sec >= secs_on_day)
    day[crossovers] += 1
    sec[crossovers] -= secs_on_day[crossovers]

    # Determine the sizes of the fields
    if ymd: lday = 10
    else:   lday = 8

    lstring = lday + 1 + 6 + lsec + len(suffix)

    # Create or check the buffer
    if buffer is None:
        buffer = np.empty(day.shape, "|S" + str(lstring))
    else:
        if buffer.shape != np.shape(day):
            raise ValueError("buffer shape does not match day/sec arrays")
        if lstring > buffer.dtype.itemsize:
            raise ValueError("buffer is too small for ISO date-time format")
        lstring = buffer.dtype.itemsize

    # Define an alternative dtype that separates the date and the time
    dtype_dict = {"day": ("|S" + str(lday), 0),
                  "sep": ("|S1", lday),
                  "sec": ("|S" + str(lstring - lday - 1), lday+1)}

    b = buffer.view(np.dtype(dtype_dict))

    # Fill in the date, separator and time
    _yxd_format_from_day(day, ymd, buffer=b["day"])
    b["sep"] = sep.encode('utf-8')
    hms_format_from_sec(sec, digits, suffix, buffer=b["sec"])

    return buffer

########################################

def ymdhms_format_from_tai(tai, sep="T", digits=None, suffix="",
                           buffer=None):
    """Returns a date and time in ISO format 'yyyy-mm-ddThh:mm:ss....' given
    seconds TAI. Works for both scalars and arrays.

    Input:
        tai         number of elapsed seconds from TAI January 1, 2000.
        sep         the character to separate the date from the time. Default is
                    "T" but " " is also allowed.
        digits      the number of digits to include after the decimal point; use
                    a negative value or None for for seconds to be rounded to
                    integer.
        suffix      "Z" to include the Zulu time zone indicator.
        buffer      an optional byte array into which to write the results.
                    Only used if day/sec are arrays. If the buffer is provided,
                    the elements must have sufficient length.
    """

    (day, sec) = day_sec_from_tai(tai)
    return _yxdhms_format_from_day_sec(day, sec, True, sep, digits, suffix,
                                       buffer)

def ydhms_format_from_tai(tai, sep="T", digits=None, suffix="",
                          buffer=None):
    """Returns a date and time in ISO format 'yyyy-dddThh:mm:ss....' given
    seconds TAI. Works for both scalars and arrays.

    Input:
        tai         number of elapsed seconds from TAI January 1, 2000.
        sep         the character to separate the date from the time. Default is
                    "T" but " " is also allowed.
        digits      the number of digits to include after the decimal point; use
                    a negative value or None for for seconds to be rounded to
                    integer.
        suffix      "Z" to include the Zulu time zone indicator.
        buffer      an optional byte array into which to write the results.
                    Only used if day/sec are arrays. If the buffer is provided,
                    the elements must have sufficient length.
    """

    (day, sec) = day_sec_from_tai(tai)
    return _yxdhms_format_from_day_sec(day, sec, False, sep, digits, suffix,
                                       buffer)

def iso_from_tai(tai, digits=None, ymd=True, suffix=""):
    """Return a date and time in ISO format given seconds tai.

    Input:
        tai         number of elapsed seconds from TAI January 1, 2000.
        digits      the number of digits to include after the decimal point; use
                    a negative value or None for for seconds to be rounded to
                    integer.
        ymd         True for year-month-day format; False for year plus
                    day-of-year format.
        suffix      "Z" to include the Zulu time zone indicator.
    """

    if ymd:
      return ymdhms_format_from_tai(tai, sep="T", digits=digits, suffix=suffix)
    else:
      return ydhms_format_from_tai(tai, sep="T", digits=digits, suffix=suffix)

########################################
# UNIT TESTS
########################################

class Test_Formatting(unittest.TestCase):

    def runTest(self):

        # ymd_format_from_day()
        self.assertEqual(ymd_format_from_day(0), "2000-01-01")

        self.assertTrue(np.all(ymd_format_from_day(np.array([-365,0,366])) ==
                        np.array([b"1999-01-01", b"2000-01-01", b"2001-01-01"])))

        # yd_format_from_day()
        self.assertEqual(yd_format_from_day(0), "2000-001")

        self.assertTrue(np.all(yd_format_from_day(np.array([-365,0,366])) ==
                        np.array([b"1999-001", b"2000-001", b"2001-001"])))

        # Check if yd_format_from_day start from 2000-001
        self.assertEqual(yd_format_from_day(0), "2000-001")

        # Check if one day is 86400 seconds
        self.assertEqual(hms_format_from_sec(86400), "23:59:60")

        # Check if hms_format_from_sec end with 86410
        self.assertEqual(hms_format_from_sec(86410), "23:59:70")

        # Check if hms_format_from_sec returns the correct format.
        self.assertEqual(hms_format_from_sec(0), "00:00:00")
        self.assertEqual(hms_format_from_sec(0,3), "00:00:00.000")
        self.assertEqual(hms_format_from_sec(0,3,'Z'), "00:00:00.000Z")

        # Check if hms_format_from_sec accepts seconds over 86410
        self.assertRaises(ValueError, hms_format_from_sec, 86411) #!!!

        # Check if ymdhms_format_from_day_sec returns the correct format.
        self.assertEqual(ymdhms_format_from_day_sec(0,0),
                         "2000-01-01T00:00:00")
        self.assertEqual(ymdhms_format_from_day_sec(0,0,'T',3),
                         "2000-01-01T00:00:00.000")
        self.assertEqual(ymdhms_format_from_day_sec(0,0,'T',3,'Z'),
                         "2000-01-01T00:00:00.000Z")
        self.assertEqual(ymdhms_format_from_day_sec(0,0,'T',None),
                         "2000-01-01T00:00:00")
        self.assertEqual(ymdhms_format_from_day_sec(0,0,'T',None,'Z'),
                         "2000-01-01T00:00:00Z")

        ymdhms = ymdhms_format_from_day_sec(np.array(np.array([0,366])),
                                            np.array(np.array([0,43200])))
        self.assertTrue(np.all(ymdhms == np.array((b"2000-01-01T00:00:00",
                                                   b"2001-01-01T12:00:00"))))

        # Check TAI formatting
        # The 32's below are for the offset between TAI and UTC
        self.assertTrue(np.all(ydhms_format_from_tai(np.array([32.,366.*86400.+32.])) ==
                    np.array((b"2000-001T00:00:00", b"2001-001T00:00:00"))))

################################################################################
# ISO format parsers
################################################################################

def day_from_iso(strings, validate=True, strip=False):
    """Returns a day number based on a parsing of a date string in ISO format.
    The format is strictly required to be either "yyyy-mm-dd" or "yyyy-ddd".
    It works on bytestring arrays in addition to individual strings or
    bytestrings.

    Now revised to avoid the slow julian_isoparser routines. It should be very
    fast. It also works for lists or arrays of arbitrary shape, provided every
    item uses the same format. Note that syntax is no longer checked in detail.

    If validate=True, then the syntax and year/month/day values are checked more
    carefully.
    """

    # Convert to bytestring if necessary
    if isinstance(strings, str):
        strings = strings.encode()

    # Old, slow procedure...
    #
    # Give the list a zero month entry for the year and day-of-year case
    # list = [["MONTH",0]] + iso.ISO_DATE.parseString(string).asList()
    # dict = _dict_from_parselist(list)
    #
    # return _day_from_dict(dict)

    strings = np.array(strings).astype('S')

    first_index = len(strings.shape) * (0,)
    first = strings[first_index]

    # Locate indices that skip over leading and trailing blanks
    if strip:
        for k0 in range(len(first)):
            if first[k0:k0+1] != b' ':
                break
        for k1 in range(len(first)):
            if first[-k1-1] != b' ':
                break
    else:
        k0 = 0
        k1 = 0
        if validate:
            if b" " in bytearray(strings):
                raise ValueError("blank character in ISO date")

    # yyyy-mm-dd case:
    if strings.itemsize - k0 - k1 == 10:
        dtype_dict = {"y": ("|S4", k0 + 0),
                      "m": ("|S2", k0 + 5),
                      "d": ("|S2", k0 + 8)}
        if validate:
            dtype_dict["dash1"] = ("|S1", k0 + 4)
            dtype_dict["dash2"] = ("|S1", k0 + 7)

        strings.dtype = np.dtype(dtype_dict)

        y = strings["y"].astype("int")
        m = strings["m"].astype("int")
        d = strings["d"].astype("int")

        if validate:
            if (np.any(strings["dash1"] != b"-") or
                np.any(strings["dash2"] != b"-")):
                    raise ValueError("invalid ISO date punctuation")

            if (np.any(y <  1) or
                np.any(m <  1) or
                np.any(m > 12) or
                np.any(d <  1) or
                np.any(d > days_in_month(month_from_ym(y,m)))):
                    raise ValueError("invalid numeric value in ISO date")

        return day_from_ymd(y,m,d)

    # yyyy-ddd case:
    if strings.itemsize - k0  == 8:
        dtype_dict = {"y": ("|S4", k0 + 0),
                      "d": ("|S3", k0 + 5)}
        if validate:
            dtype_dict["dash"] = ("|S1", k0 + 4)

        strings.dtype = np.dtype(dtype_dict)

        y = strings["y"].astype("int")
        d = strings["d"].astype("int")

        if validate:
            if np.any(strings["dash"] != b"-"):
                raise ValueError("invalid ISO date punctuation")

            if (np.any(y < 1) or
                np.any(d < 1) or
                np.any(d > days_in_year(y))):
                    raise ValueError("invalid numeric value in ISO date")

        return day_from_yd(y,d)

    # Invalid string length
    raise ValueError("invalid ISO date format: " + strings.ravel()[0].decode())

########################################

def sec_from_iso(strings, validate=True, strip=False):
    """Returns a second value based on a parsing of a time string in ISO format.
    The format is strictly required to be "hh:mm:ss[.s...][Z]". It works on
    bytestring arrays in addition to individual strings or bytestrings.

    Now revised to avoid the slow julian_isoparser routines. It should be very
    fast. It also works for lists or arrays of arbitrary shape, provided every
    item uses the same format. By default, the syntax is no longer checked in
    detail.

    If validate=True, then the syntax and hour/minute/second values are checked
    more carefully.
    """

    # Old, slow procedure...
    #
    # list = iso.ISO_TIME.parseString(string).asList()
    # dict = _dict_from_parselist(list)
    #
    # return _sec_from_dict(dict)

    # Convert to an array of strings, replace Unicode
    strings = np.array(strings).astype('S')

    first_index = len(strings.shape) * (0,)
    first = strings[first_index]

    # Skip over leading and trailing blanks
    if strip:
        for k0 in range(len(first)):
            if first[k0:k0+1] != b' ':
                break
        for k1 in range(len(first)):
            if first[-k1-1:-k1] != b' ':
                break

        lstring = len(first) - k1
    else:
        k0 = 0
        k1 = 0
        lstring = len(first)

        if validate:
            merged = bytearray(strings)
            if b" " in merged or b"-" in merged:
                raise ValueError("blank character in ISO time")

    # Prepare a dictionary to define the string format
    dtype_dict = {"h": ("|S2", k0 + 0),
                  "m": ("|S2", k0 + 3),
                  "s": ("|S2", k0 + 6)}

    if validate:
        dtype_dict["colon1"] = ("|S1", k0 + 2)
        dtype_dict["colon2"] = ("|S1", k0 + 5)

    if k0 > 0:
        dtype_dict["white0"] = ("|S" + str(k0), 0)

    if k1 > 0:
        dtype_dict["white1"] = ("|S" + str(k1), lstring)

    # Check for a trailing "Z" to ignore
    has_z = (first[-1:] == b"Z")    # note first[-1] is an int in Python 3,
                                    # so equality always fails. first[-1:] works
    if has_z:
        lstring -= 1
        dtype_dict["z"] = ("|S1", lstring)

    # Check for a period
    has_dot = (lstring > 8)
    if has_dot:
        dtype_dict["dot"] = ("|S1", k0 + 8)

    # Check for fractional seconds
    lfrac = lstring - 9 - k0
    has_frac = lfrac > 0
    if has_frac:
        dtype_dict["f"] = ("|S" + str(lfrac), k0 + 9)

    # Extract hours, minutes, seconds
    dtype = np.dtype(dtype_dict)
    strings.dtype = np.dtype(dtype_dict)
    h = strings["h"].astype("int")
    m = strings["m"].astype("int")
    s = strings["s"].astype("int")

    # Extract the fractional part of the seconds if necessary
    if has_frac:
        f = strings["f"].astype("int")
        s = s + f / (10.**lfrac)
    elif has_dot:
        s = s.astype("float")

    if validate:
        if (np.any(strings["colon1"] != b":") or
            np.any(strings["colon2"] != b":")):
                raise ValueError("invalid ISO time punctuation")

        if "white1" in dtype_dict:
            if np.any(strings["white0"] != k0 * b" "):
                raise ValueError("invalid ISO time punctuation")

        if "white1" in dtype_dict:
            if np.any(strings["white1"] != k1 * b" "):
                raise ValueError("invalid ISO time punctuation")

        if has_z:
            if np.any(strings["z"] != b"Z"):
                raise ValueError("invalid ISO time punctuation")

        if has_dot:
            if np.any(strings["dot"] != b"."):
                raise ValueError("invalid ISO time punctuation")

        if (np.any(h >  23) or
            np.any(m >  59) or
            np.any(s >= 70)):
                raise ValueError("invalid numeric value in ISO time")

        if strings.shape == ():
            if s >= 60:
                if h != 23 or m != 59:
                    raise ValueError("invalid numeric value in ISO time")
        else:
            mask = (s >= 60)
            if np.any(h[mask] != 23) or np.any(m[mask] != 59):
                raise ValueError("invalid numeric value in ISO time")

    return sec_from_hms(h,m,s)

########################################

def day_sec_from_iso(strings, validate=True, strip=False):
    """Returns a day and second based on a parsing of the string in ISO
    date-time format. The format is strictly enforced to be an ISO date plus an
    ISO time, separated by a single space or a "T". It works for bytestring
    arrays in addition to individual strings or bytestrings."""

    # Old, slow procedure...
    #
    # Give the default entries in case they are needed
    # list = [["MONTH",0]] + iso.ISO_DATETIME.parseString(string).asList()
    # dict = _dict_from_parselist(list)
    #
    # day = _day_from_dict(dict)
    # sec = _sec_from_dict(dict, day, True, validate)
    #
    # return (day, sec)

    # Convert to an array of strings, replace Unicode
    strings = np.array(strings).astype('S')

    first_index = len(strings.shape) * (0,)
    first = strings[first_index]

    # Check for a T or blank separator
    csep = b"T"
    isep = first.find(csep)

    if isep == -1:
        if strip:
            for k0 in range(len(first)):
                if first[k0:k0+1] != b' ':
                    break
        else:
            k0 = 0

        csep = b" "
        isep = first.find(csep, k0)

    # If no separator is found, assume it is just a date
    if isep == -1:
        return (day_from_iso(strings, validate, strip), 0)

    # Otherwise, parse the date and time separately
    dtype_dict = {"day": ("|S" + str(isep), 0),
                  "sec": ("|S" + str(len(first) - isep - 1), isep + 1)}

    if validate:
        dtype_dict["sep"] = ("|S1", isep)

    strings.dtype = np.dtype(dtype_dict)
    day = day_from_iso(strings["day"], validate, strip)
    sec = sec_from_iso(strings["sec"], validate, strip)

    if validate:
        if (np.any(strings["sep"] != csep)):
            raise ValueError("invalid ISO date-time punctuation")

        if np.any(sec >= seconds_on_day(day)):
            raise ValueError("seconds value is outside allowed range")

    return (day, sec)

########################################

def tai_from_iso(strings, validate=True, strip=False):
    """Returns the elapsed seconds TAI from January 1, 2000 given an ISO date
    or date-time string. Works for individual strings or bytestrings and also
    for arrays of bytestrings."""

    (day, sec) = day_sec_from_iso(strings, validate, strip)
    return tai_from_day(day) + sec

########################################
# UNIT TESTS
########################################

class Test_ISO_Parsing(unittest.TestCase):

    def runTest(self):

        # day_from_iso()
        self.assertEqual(day_from_iso( "2001-01-01"), 366)

        strings = ["1999-01-01", "2000-01-01", "2001-01-01"]
        days    = [       -365 ,           0 ,         366 ]
        self.assertTrue(np.all(day_from_iso(strings) == np.array(days)))

        strings = [["2000-001", "2000-002"], ["2000-003", "2000-004"]]
        days    = [[        0 ,         1 ], [        2 ,         3 ]]
        self.assertTrue(np.all(day_from_iso(strings) == np.array(days)))

        strings = ["1999-01-01", "2000-01-01", "2001-01+01"]
        self.assertRaises(ValueError, day_from_iso, strings)

        strings = ["1999-01-01", "2000-01-01", "2001-01-aa"]
        self.assertRaises(ValueError, day_from_iso, strings)

        strings = ["1999-01-01", "2000-01-01", "2001-01- 1"]
        self.assertRaises(ValueError, day_from_iso, strings)

        strings = ["1999-01-01", "2000-01-01", "2001-01-00"]
        self.assertRaises(ValueError, day_from_iso, strings)

        strings = ["1999-01-01", "2000-01-01", "2001-00-01"]
        self.assertRaises(ValueError, day_from_iso, strings)

        strings = ["1999-01-01", "2000-01-01", "2001-13-01"]
        self.assertRaises(ValueError, day_from_iso, strings)

        strings = ["1999-01-01", "2000-01-01", "2001-02-29"]
        self.assertRaises(ValueError, day_from_iso, strings)

        # sec_from_iso()
        self.assertEqual(sec_from_iso("01:00:00"),     3600)
        self.assertEqual(sec_from_iso("23:59:60"),    86400)
        self.assertEqual(sec_from_iso("23:59:69"),    86409)
        self.assertEqual(sec_from_iso("23:59:69Z"),   86409)
        self.assertEqual(sec_from_iso("23:59:69.10"), 86409.10)
        self.assertEqual(sec_from_iso("23:59:69.5Z"), 86409.5)

        strings = ["00:00:00", "00:01:00", "00:02:00"]
        secs    = [        0 ,        60 ,       120 ]
        self.assertTrue(np.all(sec_from_iso(strings) == np.array(secs)))

        strings = [["00:02:00Z", "00:04:00Z"], ["00:06:00Z", "00:08:01Z"]]
        secs    = [[      120  ,       240  ], [       360 ,        481 ]]
        self.assertTrue(np.all(sec_from_iso(strings) == np.array(secs)))

        strings = ["00:00:00.01", "00:01:00.02", "23:59:69.03"]
        secs    = [        0.01 ,        60.02 ,     86409.03 ]
        self.assertTrue(np.all(sec_from_iso(strings) == np.array(secs)))

        strings = ["00:00:00.01", "00:01:00.02", "00:02+00.03"]
        self.assertRaises(ValueError, sec_from_iso, strings)

        strings = ["00:00:00.01", "00:01:00.02", "00:02: 0.03"]
        self.assertRaises(ValueError, sec_from_iso, strings)

        strings = ["00:02:00.1Z", "00:04:00.2Z", "00:06:00.3z"]
        self.assertRaises(ValueError, sec_from_iso, strings)

        strings = ["00:00:00.01", "00:01:00.02", "00:02:00+03"]
        self.assertRaises(ValueError, sec_from_iso, strings)

        strings = ["00:00:00.01", "00:01:00.02", "-0:02:00.03"]
        self.assertRaises(ValueError, sec_from_iso, strings)

        strings = ["00:00:00.01", "00:01:00.02", "24:02:00.03"]
        self.assertRaises(ValueError, sec_from_iso, strings)

        strings = ["00:00:00.01", "00:01:00.02", "00:60:00.03"]
        self.assertRaises(ValueError, sec_from_iso, strings)

        strings = ["00:00:00", "00:01:00", "00:00:70"]
        self.assertRaises(ValueError, sec_from_iso, strings)

        strings = ["00:00:00.01", "00:01:00.02", "00:00:69.00"]
        self.assertRaises(ValueError, sec_from_iso, strings)

        # day_sec_from_iso()
        self.assertEqual(day_sec_from_iso( "2001-01-01 01:00:00"), (366,3600))
        self.assertEqual(day_sec_from_iso( "2001-01-01T01:00:00"), (366,3600))

        self.assertEqual(day_sec_from_iso("1998-12-31 23:59:60"), (-366, 86400))

        self.assertRaises(ValueError, day_sec_from_iso, "2000-01-01 23:59:60")
        self.assertRaises(ValueError, day_sec_from_iso, "1999-12-31 23:59:61")

        strings = ["1999-01-01", "2000-01-01", "2001-01-01"]
        days    = [       -365 ,           0 ,         366 ]
        self.assertTrue(np.all(day_sec_from_iso(strings)[0] == np.array(days)))
        self.assertTrue(np.all(day_sec_from_iso(strings)[1] == 0))

        strings = [["2000-001", "2000-002"], ["2000-003", "2000-004"]]
        days    = [[        0 ,         1 ], [        2 ,         3 ]]
        self.assertTrue(np.all(day_sec_from_iso(strings)[0] == np.array(days)))
        self.assertTrue(np.all(day_sec_from_iso(strings)[1] == 0))

        strings = ["1998-12-31 23:59:60", "2001-01-01 01:00:01"]
        days    = [       -366          ,         366          ]
        secs    = [               86400 ,                 3601 ]
        self.assertTrue(np.all(day_sec_from_iso(strings)[0] == np.array(days)))
        self.assertTrue(np.all(day_sec_from_iso(strings)[1] == np.array(secs)))

        strings = ["1998-12-31T23:59:60", "2001-01-01T01:00:01"]
        days    = [       -366          ,         366          ]
        secs    = [               86400 ,                 3601 ]
        self.assertTrue(np.all(day_sec_from_iso(strings)[0] == np.array(days)))
        self.assertTrue(np.all(day_sec_from_iso(strings)[1] == np.array(secs)))

        strings = ["1998-12-31 23:59:60Z", "2001-01-01 01:00:01Z"]
        days    = [       -366           ,         366           ]
        secs    = [               86400  ,                 3601  ]
        self.assertTrue(np.all(day_sec_from_iso(strings)[0] == np.array(days)))
        self.assertTrue(np.all(day_sec_from_iso(strings)[1] == np.array(secs)))

        strings = ["1998-12-31 23:59:60Z", "2001-01-01x01:00:01Z"]
        self.assertRaises(ValueError, day_sec_from_iso, strings)

        strings = ["1998-12-31 23:59:60Z", "1998-12-31 23:59:61Z"]
        self.assertRaises(ValueError, day_sec_from_iso, strings)

################################################################################
# General Parsing Routines
#
# The grammar is defined in julian_dateparser.py, abbreviated jdp here.
#
# Note: Unlike all other julian library routines, these do not support
# array or array-like arguments.
################################################################################

global DATE_PARSER_DICT, DATETIME_PARSER_DICT

DATE_PARSER_DICT = {"YMD":jdp.YMD_PREF_DATE + pyparsing.StringEnd(),
                    "MDY":jdp.MDY_PREF_DATE + pyparsing.StringEnd(),
                    "DMY":jdp.DMY_PREF_DATE + pyparsing.StringEnd()}

DATETIME_PARSER_DICT = {"YMD":jdp.YMD_PREF_DATETIME + pyparsing.StringEnd(),
                        "MDY":jdp.MDY_PREF_DATETIME + pyparsing.StringEnd(),
                        "DMY":jdp.DMY_PREF_DATETIME + pyparsing.StringEnd()}

########################################

def day_from_string(string, order="YMD"):
    """Returns a day number based on a parsing of the string. Input parameter
    order is one of 'YMD', 'MDY' or 'DMY', and defines the preferred order for
    date, month and year in situations where it might be ambiguous."""

    parser = DATE_PARSER_DICT[order]

    # Give the list a zero month entry for the year and day-of-year case
    list = [["MONTH",0]] + parser.parseString(string).asList()
    dict = _dict_from_parselist(list)

    return _day_from_dict(dict)

########################################

def sec_from_string(string):
    """Returns a second value based on a parsing of a time string."""

    # Give the list zero values for each parameter, in case they are missing
    # from the list returned
    parser = jdp.TIME + pyparsing.StringEnd()
    list = ([["HOUR",0],["MINUTE",0],["SECOND",0]] +
            parser.parseString(string).asList())
    dict = _dict_from_parselist(list)

    return _sec_from_dict(dict)

########################################

def day_sec_type_from_string(string, order="YMD", validate=True):
    """Returns a day and second based on a parsing of the string. Input
    parameter order is one of 'YMD', 'MDY' or 'DMY', and defines the preferred
    order for date, month and year in situations where it might be ambiguous.
    """

    parser = DATETIME_PARSER_DICT[order]

    # Give the default entries in case they are needed
    list = ([["TYPE","UTC"],["MONTH",0],["HOUR",0],["MINUTE",0],["SECOND",0]] +
            parser.parseString(string).asList())
    dict = _dict_from_parselist(list)

    # Get the time type
    time_type = dict["TYPE"]
    leapseconds = (time_type == "UTC")

    # Handle the case of a Julian date
    try:
        mjd = dict["MJD"]
        (day, sec) = day_sec_from_mjd(mjd)
        return (day, sec, time_type)
    except KeyError: pass

    # Handle the case of a fractional day
    dvalue = dict["DAY"]
    if type(dvalue) == type(0.):
        day = _day_from_dict(dict)
        dfrac = dvalue - day
        sec = (dvalue - day) * seconds_of_day(day, leapseconds)
        return (day, sec, time_type)

    # Otherwise, it is a calendar date plus time
    day = _day_from_dict(dict)
    sec = _sec_from_dict(dict, day, leapseconds, validate)

    return (day, sec, time_type)

####################
# internals...
####################

def _dict_from_parselist(list):
    dict = {}
    for pair in list:
        dict[pair[0]] = pair[1]

    return dict

####################

def _day_from_dict(dict):
    """Returns a day number based on the contents of a dictionary."""

    # First check for MJD date
    try:
        mjd = dict["MJD"]
        return day_from_mjd(mjd)
    except KeyError: pass

    # Look up year, month and day
    y = dict["YEAR"]
    m = dict["MONTH"]
    d = dict["DAY"]
    d = int(d)

    # Year and day-of-year case
    if m == 0:
        if d > days_in_year(y):
            raise ValueError("Day value out of range in year: " +
                              "{:04d}-{:03d}".format(y,d))

        return day_from_yd(y,d)

    # Year-month-day case
    else:
        month = month_from_ym(y,m)
        if d > days_in_month(month):
            raise ValueError("Day value out of range for month: "
                             "{:04d}-{:02d}-{:02d}".format(y,m,d))

    return day_from_ymd(y,m,d)

#####################

def _sec_from_dict(dict, day=None, leapseconds=True, validate=True):
    """Returns a seconds value based on the contents of a dictionary."""

    h = dict["HOUR"]
    m = dict["MINUTE"]
    s = dict["SECOND"]

    sec = h * 3600 + m * 60 + s

    if validate and (day is not None):
        if sec >= seconds_on_day(day, leapseconds):
            raise ValueError("Seconds value out of range on day " +
                             ymd_format_from_day(day) + ": " + str(sec))

    return sec

################################################################################
# UNIT TESTS
################################################################################

class Test_General_Parsing(unittest.TestCase):

    def runTest(self):

        # Note: julian_dateparser.py has more extensive unit tests

        # Check if day_from_string works like day_from_ymd
        self.assertEqual(day_from_string("2000-01-01"),
                         day_from_ymd(2000,1,1))

        # Check if other parsers work
        self.assertEqual(day_from_string("01-02-2000", "MDY"),
                         day_from_ymd(2000,1,2))
        self.assertEqual(day_from_string("01-02-00", "MDY"),
                         day_from_ymd(2000,1,2))
        self.assertEqual(day_from_string("02-01-2000", "DMY"),
                         day_from_ymd(2000,1,2))
        self.assertEqual(day_from_string("02-01-00", "DMY"),
                         day_from_ymd(2000,1,2))
        self.assertEqual(day_from_string("2000-02-29","DMY"),
                         day_from_ymd(2000,2,29))

        # Check date validator
        self.assertRaises(ValueError, day_from_string, "2001-11-31")
        self.assertRaises(ValueError, day_from_string, "2001-02-29")

        # Check sec_from_string
        self.assertEqual(sec_from_string("00:00:00.000"), 0.0)
        self.assertEqual(sec_from_string("00:00:00"), 0)
        self.assertEqual(sec_from_string("00:00:59.000"), 59.0)
        self.assertEqual(sec_from_string("00:00:59"), 59)

        # Check leap seconds
        self.assertEqual(sec_from_string("23:59:60.000"), 86400.0)
        self.assertEqual(sec_from_string("23:59:69.000"), 86409.0)
        self.assertRaises(pyparsing.ParseException, sec_from_string,
                                                    "23:59:70.000")

        # Check day_sec_type_from_string
        self.assertEqual(day_sec_type_from_string("2000-01-01 00:00:00.00"),
                         (0, 0.0, "UTC"))
        self.assertEqual(day_sec_type_from_string("2000-01-01 00:00:00.00 tai"),
                         (0, 0.0, "TAI"))
        self.assertEqual(day_sec_type_from_string("2000-01-01 00:00:00.00 Z"),
                         (0, 0.0, "UTC"))
        self.assertEqual(day_sec_type_from_string("2000-01-01 00:00:00.00 TDB"),
                         (0, 0.0, "TDB"))

        # Check if DMY is same as MDY
        self.assertEqual(day_sec_type_from_string("31-12-2000 12:34:56", "DMY"),
                         day_sec_type_from_string("12-31-2000 12:34:56", "MDY"))

        # Check leap second validator
        self.assertEqual(day_sec_type_from_string("1998-12-31 23:59:60"),
                         (-366, 86400, "UTC"))
        self.assertEqual(day_sec_type_from_string("1998-12-31 23:59:60.99"),
                         (-366, 86400.99, "UTC"))

        self.assertRaises(ValueError, day_sec_type_from_string,
                                      "1998-12-31 23:59:60 TDB")
        self.assertRaises(ValueError, day_sec_type_from_string,
                                      "1998-12-31 23:59:60.99 TAI")

        self.assertRaises(ValueError, day_sec_type_from_string,
                                      "2000-01-01 23:59:60")
        self.assertRaises(ValueError, day_sec_type_from_string,
                                      "1999-12-31 23:59:61")

################################################################################
# Perform unit testing if executed from the command line
################################################################################

if __name__ == '__main__':
    unittest.main()

################################################################################